diff --git a/.gitignore b/.gitignore index ebaae10..1ba12bd 100644 --- a/.gitignore +++ b/.gitignore @@ -143,4 +143,7 @@ vite.config.ts.timestamp-* .vite/ # AI -.claude \ No newline at end of file +.claude + +# UE5 source code +.ue5 diff --git a/.vscode/settings.json b/.vscode/settings.json index 089d874..af7f1fa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,9 +57,21 @@ // CSpell "cSpell.words": [ "asar", + "ASSETDATA", "asynciterable", "browsable", - "scrollview" + "CHUNKID", + "CHUNKIDS", + "EXPORTMAP", + "fguid", + "gatherable", + "scrollview", + "SERIALSIZES", + "SOFTOBJECTPATH", + "TEMPLATEINDEX", + "uasset", + "uassets", + "UPACKAGE" ], "[json]": { "editor.defaultFormatter": "biomejs.biome" diff --git a/README.md b/README.md index 25956c1..d7cd07c 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,65 @@

@cybearl/gitgame

A small tool with a web interface to simplify UE5-based game development with Git LFS.

+ +## Installation +### Prerequisites +- [Node.js](https://nodejs.org) 22 or newer +- [Yarn](https://classic.yarnpkg.com) 1.x (classic) + +### App +```bash +# From the root of the repo +$ yarn install + +# Run the app in development mode +$ yarn dev +``` + +## Dev Notes +### Keeping the UObject enum in sync with UE5 source +The enums at `src/main/lib/uasset/enums.ts` mirror a handful of headers from +[EpicGames/UnrealEngine](https://github.com/EpicGames/UnrealEngine) (private repo, requires an +Epic-linked GitHub account). A full build of the engine is ~400GB and even a plain clone is +10-15GB, so we keep a sparse checkout of just the `UObject` headers we care about. + +The source lives at `.ue5/UnrealEngine/` (already in `.gitignore`). + +One-time setup, from the repo root: +```bash +$ git clone --filter=blob:none --sparse https://github.com/EpicGames/UnrealEngine.git .ue5/UnrealEngine +$ cd .ue5/UnrealEngine +$ git sparse-checkout set Engine/Source/Runtime/Core/Public/UObject Engine/Source/Runtime/CoreUObject/Public/UObject +``` + +**Important:** always run `git sparse-checkout` commands from **inside** `.ue5/UnrealEngine/`, or +pass `-C .ue5/UnrealEngine` explicitly. If you run one from the gitgame repo root by accident, +git will silently enable sparse-checkout on the **gitgame** repo (because that's the enclosing +`.git` it finds) and hide 99% of your working tree. Recover with: +```bash +$ git sparse-checkout disable +``` +Files are never lost, they're still in `HEAD`/index, just filtered from the working tree. + +If the working tree stays empty after `set` on the UE clone (blob fetch silently failing on the +private repo), force a re-materialize: +```bash +$ git -C .ue5/UnrealEngine sparse-checkout reapply +$ git -C .ue5/UnrealEngine checkout HEAD -- . +``` + +To add more UE paths later (e.g. when we start writing type-specific shapers and need +`Engine/Classes/Engine/Texture.h`): +```bash +$ git -C .ue5/UnrealEngine sparse-checkout add Engine/Source/Runtime/Engine/Classes/Engine +``` + +To pull upstream updates before resyncing enum values: +```bash +$ git -C .ue5/UnrealEngine pull +``` + +Files that map to `enums.ts`: +- `Engine/Source/Runtime/Core/Public/UObject/ObjectVersion.h`: `EUnrealEngineObjectUE4Version`, `EUnrealEngineObjectUE5Version` +- `Engine/Source/Runtime/CoreUObject/Public/UObject/ObjectMacros.h`: `PackageFlags`, `EObjectFlags` +- `Engine/Source/Runtime/CoreUObject/Public/UObject/PackageFileSummary.h`: header layout the parser reads diff --git a/biome.json b/biome.json index c24b0ab..7ea90ca 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json", "files": { "ignoreUnknown": true, "includes": ["**", "!node_modules", "!dist", "!.vscode"] diff --git a/package.json b/package.json index be1c6ae..60f25c0 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "build": "electron-vite build", "preview": "electron-vite preview", "typecheck": "tsc --noEmit", + "test": "npm run test:unit", + "test:unit": "vitest", "lint": "biome check", "lint:fix": "biome check --fix && biome format --write", "dist": "electron-vite build && electron-builder", @@ -35,6 +37,7 @@ "dependencies": { "@cybearl/cypack": "^1.11.10", "@react95/icons": "^2.5.0", + "dedent": "^1.7.2", "electron-updater": "^6.3.9", "picomatch": "^4.0.5", "react-window": "^2.2.7", @@ -56,7 +59,9 @@ "react-dom": "^19.2.5", "tailwindcss": "^4.3.1", "typescript": "^5.8.3", - "vite": "^7.0.0" + "vite": "^7.0.0", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" }, "resolutions": { "@types/node": "^24.12.2" diff --git a/src/main/assets/sounds/error.mp3 b/src/main/assets/sounds/error.mp3 new file mode 100644 index 0000000..cbf30a0 Binary files /dev/null and b/src/main/assets/sounds/error.mp3 differ diff --git a/src/main/config/dialogs.ts b/src/main/config/dialogs.ts index 26e676d..09eac0b 100644 --- a/src/main/config/dialogs.ts +++ b/src/main/config/dialogs.ts @@ -1,11 +1,11 @@ /** * The main configuration for dialogs. */ -const DIALOG_CONFIG = { +const DIALOGS_CONFIG = { sizes: { confirm: { width: 420, height: 230 }, error: { width: 480, height: 300 }, }, } -export default DIALOG_CONFIG +export default DIALOGS_CONFIG diff --git a/src/main/config/windows.ts b/src/main/config/windows.ts index 020732c..e3c3722 100644 --- a/src/main/config/windows.ts +++ b/src/main/config/windows.ts @@ -10,6 +10,7 @@ const WEB_PREFERENCES: WebPreferences = { sandbox: false, contextIsolation: true, nodeIntegration: false, + autoplayPolicy: "no-user-gesture-required", } /** diff --git a/src/main/index.ts b/src/main/index.ts index bdc758e..648545b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,14 +1,14 @@ import path from "node:path" import WINDOWS_CONFIG from "@main/config/windows" import { registerAppHandlers } from "@main/lib/app/handlers" -import { registerDialogHandlers } from "@main/lib/dialog" -import { registerGitHandlers } from "@main/lib/git/handlers" -import { registerLfsHandlers } from "@main/lib/lfs/handlers" -import { registerProjectHandlers } from "@main/lib/project/handlers" -import { registerShellHandlers } from "@main/lib/shell/handlers" -import { registerTreeHandlers } from "@main/lib/tree/handlers" +import { registerDialogsHandlers } from "@main/lib/dialogs" +import { registerFileTreeHandlers } from "@main/lib/fileTree/handlers" +import { registerGitCommandsHandlers } from "@main/lib/gitCommands/handlers" +import { registerLfsCommandsHandlers } from "@main/lib/lfsCommands/handlers" +import { registerProjectsHandlers } from "@main/lib/projects/handlers" +import { registerShellsHandlers } from "@main/lib/shells/handlers" import { startAutoUpdater } from "@main/lib/updater" -import { attachWindowStateBroadcaster, registerWindowControlHandlers } from "@main/lib/window" +import { attachWindowStateBroadcaster, registerWindowsControlHandlers } from "@main/lib/windows" import { app, BrowserWindow, shell } from "electron" /** @@ -44,13 +44,13 @@ function createMainWindow(): BrowserWindow { // Create the main application window when Electron is ready app.whenReady().then(() => { registerAppHandlers() - registerWindowControlHandlers() - registerDialogHandlers() - registerGitHandlers() - registerLfsHandlers() - registerTreeHandlers() - registerProjectHandlers() - registerShellHandlers() + registerWindowsControlHandlers() + registerDialogsHandlers() + registerGitCommandsHandlers() + registerLfsCommandsHandlers() + registerFileTreeHandlers() + registerProjectsHandlers() + registerShellsHandlers() createMainWindow() startAutoUpdater() diff --git a/src/main/lib/constants.ts b/src/main/lib/constants.ts index 48ec0d7..4b32745 100644 --- a/src/main/lib/constants.ts +++ b/src/main/lib/constants.ts @@ -8,48 +8,77 @@ const CONSTANTS = { ipc: { // App appGetVersion: "app:get-version", - // Window - windowGetState: "window:get-state", - windowStateChanged: "window:state-changed", - windowMinimize: "window:minimize", - windowMaximizeToggle: "window:maximize-toggle", - windowClose: "window:close", - // Git - gitIsRepository: "git:is-repository", - gitGetRepositoryRoot: "git:get-repository-root", - gitGetStatus: "git:get-status", - gitListBranches: "git:list-branches", - gitGetLog: "git:get-log", - gitGetRemoteUrl: "git:get-remote-url", - // LFS - lfsListLocks: "lfs:list-locks", - lfsGetCachedLocks: "lfs:get-cached-locks", - lfsGetLockableFiles: "lfs:get-lockable-files", - lfsLockPaths: "lfs:lock-paths", - lfsUnlockPaths: "lfs:unlock-paths", - // Tree - treeGetFileTree: "tree:get-file-tree", - // Project - projectAddLocal: "project:add-local", - projectOpen: "project:open", - projectGetRecent: "project:get-recent", - projectRemoveRecent: "project:remove-recent", - projectClearRecent: "project:clear-recent", - projectGetPreferences: "project:get-preferences", - projectSetPreferences: "project:set-preferences", - // Shell - shellOpenExternal: "shell:open-external", - // Dialog - dialogConfirm: "dialog:confirm", - dialogError: "dialog:error", - dialogGetOptions: "dialog:get-options", - dialogRespond: "dialog:respond", + // Windows + windowsGetState: "windows:get-state", + windowsStateChanged: "windows:state-changed", + windowsMinimize: "windows:minimize", + windowsMaximizeToggle: "windows:maximize-toggle", + windowsClose: "windows:close", + // Git commands + gitCommandsIsRepository: "git-commands:is-repository", + gitCommandsGetRepositoryRoot: "git-commands:get-repository-root", + gitCommandsGetStatus: "git-commands:get-status", + gitCommandsListBranches: "git-commands:list-branches", + gitCommandsGetLog: "git-commands:get-log", + gitCommandsGetRemoteUrl: "git-commands:get-remote-url", + // LFS commands + lfsCommandsListLocks: "lfs-commands:list-locks", + lfsCommandsGetCachedLocks: "lfs-commands:get-cached-locks", + lfsCommandsGetLockableFiles: "lfs-commands:get-lockable-files", + lfsCommandsLockPaths: "lfs-commands:lock-paths", + lfsCommandsUnlockPaths: "lfs-commands:unlock-paths", + lfsCommandsLockProgress: "lfs-commands:lock-progress", + lfsCommandsMigrateLocks: "lfs-commands:migrate-locks", + // File tree + fileTreeGet: "file-tree:get", + // Projects + projectsAddLocal: "projects:add-local", + projectsOpen: "projects:open", + projectsGetRecent: "projects:get-recent", + projectsRemoveRecent: "projects:remove-recent", + projectsClearRecent: "projects:clear-recent", + projectsGetPreferences: "projects:get-preferences", + projectsSetPreferences: "projects:set-preferences", + // Shells + shellsOpenExternal: "shells:open-external", + // Dialogs + dialogsConfirm: "dialogs:confirm", + dialogsError: "dialogs:error", + dialogsGetOptions: "dialogs:get-options", + dialogsRespond: "dialogs:respond", }, git: { logFieldSeparator: "\x1f", logRecordSeparator: "\x1e", logFormat: ["%H", "%h", "%s", "%an", "%ae", "%aI"], // Joined with field separator and ends with record separator }, + uasset: { + // Highest "LegacyFileVersion" the summary reader handles, anything below (i.e. more + // negative) uses a summary layout we haven't verified against UE source + currentLegacyFileVersion: -9, + // Prefix that marks user-created (game content) asset packages, imports whose outer + // chain terminates outside this prefix are engine/plugin classes + gamePackagePrefix: "/Game/", + // Bit values of "EPropertyTagFlags", the uint8 bitmask on every property tag header + propertyTagFlag: { + hasArrayIndex: 0x01, + hasPropertyGuid: 0x02, + hasPropertyExtensions: 0x04, + hasBinaryOrNativeSerialize: 0x08, + boolTrue: 0x10, + skippedSerialize: 0x20, + }, + // Bit values of "EPropertyTagExtension", the uint8 bitmask written when "hasPropertyExtensions" is set + propertyTagExtension: { + overridableInformation: 0x02, + hasExternalsObjects: 0x04, + }, + // Blueprint-specific magic strings the shaper matches on + blueprint: { + componentSuffix: "_GEN_VARIABLE", + compiledClassSuffix: "_C", + }, + }, } as const export default CONSTANTS diff --git a/src/main/lib/dialog/index.ts b/src/main/lib/dialogs/index.ts similarity index 83% rename from src/main/lib/dialog/index.ts rename to src/main/lib/dialogs/index.ts index 99fb8e7..80ce8e5 100644 --- a/src/main/lib/dialog/index.ts +++ b/src/main/lib/dialogs/index.ts @@ -1,10 +1,10 @@ import path from "node:path" import CONSTANTS from "@main/lib/constants" -import { attachWindowStateBroadcaster } from "@main/lib/window" +import { attachWindowStateBroadcaster } from "@main/lib/windows" import { BrowserWindow, ipcMain } from "electron" -import DIALOG_CONFIG from "@/main/config/dialogs" +import DIALOGS_CONFIG from "@/main/config/dialogs" import WINDOWS_CONFIG from "@/main/config/windows" -import type { ConfirmDialogOptions, DialogOptions } from "@/main/types/dialog" +import type { ConfirmDialogOptions, DialogOptions } from "@/main/types/dialogs" /** * A dialog window awaiting the user's response. @@ -40,7 +40,7 @@ function loadDialog(window: BrowserWindow) { */ export function openDialog(parent: BrowserWindow | null, options: DialogOptions): Promise { return new Promise(resolve => { - const size = DIALOG_CONFIG.sizes[options.variant] + const size = DIALOGS_CONFIG.sizes[options.variant] const window = new BrowserWindow({ ...WINDOWS_CONFIG.dialog, @@ -75,21 +75,21 @@ export function openDialog(parent: BrowserWindow | null, options: DialogOptions) * Registers the IPC handlers that open Win95-styled dialog windows and relay * their options and responses between the main and renderer processes. */ -export function registerDialogHandlers() { - ipcMain.handle(CONSTANTS.ipc.dialogConfirm, (event, options: ConfirmDialogOptions) => +export function registerDialogsHandlers() { + ipcMain.handle(CONSTANTS.ipc.dialogsConfirm, (event, options: ConfirmDialogOptions) => openDialog(BrowserWindow.fromWebContents(event.sender), { ...options, variant: "confirm" }), ) - ipcMain.on(CONSTANTS.ipc.dialogError, (event, title: string, content: string) => { - openDialog(BrowserWindow.fromWebContents(event.sender), { variant: "error", title, message: content }) + ipcMain.on(CONSTANTS.ipc.dialogsError, (event, title: string, message: string, detail?: string) => { + openDialog(BrowserWindow.fromWebContents(event.sender), { variant: "error", title, message, detail }) }) - ipcMain.handle(CONSTANTS.ipc.dialogGetOptions, event => { + ipcMain.handle(CONSTANTS.ipc.dialogsGetOptions, event => { const window = BrowserWindow.fromWebContents(event.sender) return window ? (pendingDialogs.get(window.id)?.options ?? null) : null }) - ipcMain.on(CONSTANTS.ipc.dialogRespond, (event, result: boolean) => { + ipcMain.on(CONSTANTS.ipc.dialogsRespond, (event, result: boolean) => { const window = BrowserWindow.fromWebContents(event.sender) if (!window) return diff --git a/src/main/lib/tree/build.ts b/src/main/lib/fileTree/build.ts similarity index 97% rename from src/main/lib/tree/build.ts rename to src/main/lib/fileTree/build.ts index b000b95..973d211 100644 --- a/src/main/lib/tree/build.ts +++ b/src/main/lib/fileTree/build.ts @@ -1,4 +1,4 @@ -import type { FileTreeNode } from "@/main/types/tree" +import type { FileTreeNode } from "@/main/types/fileTree" /** * A mutable tree node used while assembling the tree, keyed children included. diff --git a/src/main/lib/fileTree/handlers.ts b/src/main/lib/fileTree/handlers.ts new file mode 100644 index 0000000..5ccff46 --- /dev/null +++ b/src/main/lib/fileTree/handlers.ts @@ -0,0 +1,10 @@ +import CONSTANTS from "@main/lib/constants" +import { getFileTree } from "@main/lib/fileTree/service" +import { safeHandle } from "@main/lib/ipc" + +/** + * Registers the IPC handler for the repository file tree. + */ +export function registerFileTreeHandlers() { + safeHandle(CONSTANTS.ipc.fileTreeGet, (_event, dir: string) => getFileTree(dir)) +} diff --git a/src/main/lib/fileTree/service.ts b/src/main/lib/fileTree/service.ts new file mode 100644 index 0000000..b41e7fe --- /dev/null +++ b/src/main/lib/fileTree/service.ts @@ -0,0 +1,31 @@ +import { buildFileTree } from "@main/lib/fileTree/build" +import { runGit } from "@main/lib/gitCommands/exec" +import { filterLockable } from "@main/lib/lfsCommands/service" +import type { FileTreeNode } from "@/main/types/fileTree" + +/** + * Builds the repository file tree, annotating each node with its `lockable` state. + * @param dir A path inside the repository. + * @returns The root-level file tree nodes. + * @throws If listing the repository files fails. + */ +export async function getFileTree(dir: string): Promise { + const listed = await runGit(["ls-files", "--cached", "--others", "--exclude-standard", "-z"], { cwd: dir }) + if (listed.exitCode !== 0) { + throw new Error(listed.stderr.trim() || "Failed to list repository files.") + } + + const deleted = await runGit(["ls-files", "--deleted", "-z"], { cwd: dir }) + if (deleted.exitCode !== 0) { + throw new Error(deleted.stderr.trim() || "Failed to list deleted repository files.") + } + + const deletedSet = new Set(deleted.stdout.split("\0").filter(Boolean)) + + const files = [...new Set(listed.stdout.split("\0").filter(Boolean))].filter(file => !deletedSet.has(file)) + if (files.length === 0) return [] + + const lockable = new Set(await filterLockable(dir, files)) + + return buildFileTree(files, lockable) +} diff --git a/src/main/lib/git/handlers.ts b/src/main/lib/git/handlers.ts deleted file mode 100644 index 2677cbc..0000000 --- a/src/main/lib/git/handlers.ts +++ /dev/null @@ -1,16 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import { getLog, getRemoteUrl, getRepositoryRoot, getStatus, isRepository, listBranches } from "@main/lib/git/service" -import { ipcMain } from "electron" - -/** - * Registers the IPC handlers that expose the read-only Git service to the - * renderer process. - */ -export function registerGitHandlers() { - ipcMain.handle(CONSTANTS.ipc.gitIsRepository, (_event, dir: string) => isRepository(dir)) - ipcMain.handle(CONSTANTS.ipc.gitGetRepositoryRoot, (_event, dir: string) => getRepositoryRoot(dir)) - ipcMain.handle(CONSTANTS.ipc.gitGetStatus, (_event, dir: string) => getStatus(dir)) - ipcMain.handle(CONSTANTS.ipc.gitListBranches, (_event, dir: string) => listBranches(dir)) - ipcMain.handle(CONSTANTS.ipc.gitGetLog, (_event, dir: string, limit?: number) => getLog(dir, limit)) - ipcMain.handle(CONSTANTS.ipc.gitGetRemoteUrl, (_event, dir: string) => getRemoteUrl(dir)) -} diff --git a/src/main/lib/git/exec.ts b/src/main/lib/gitCommands/exec.ts similarity index 96% rename from src/main/lib/git/exec.ts rename to src/main/lib/gitCommands/exec.ts index 5bb5ab9..aaa103b 100644 --- a/src/main/lib/git/exec.ts +++ b/src/main/lib/gitCommands/exec.ts @@ -1,6 +1,6 @@ import { execFile } from "node:child_process" import { promisify } from "node:util" -import type { GitResult } from "@/main/types/git" +import type { GitResult } from "@/main/types/gitCommands" const execFileAsync = promisify(execFile) diff --git a/src/main/lib/gitCommands/handlers.ts b/src/main/lib/gitCommands/handlers.ts new file mode 100644 index 0000000..0375907 --- /dev/null +++ b/src/main/lib/gitCommands/handlers.ts @@ -0,0 +1,22 @@ +import CONSTANTS from "@main/lib/constants" +import { + getLog, + getRemoteUrl, + getRepositoryRoot, + getStatus, + isRepository, + listBranches, +} from "@main/lib/gitCommands/service" +import { safeHandle } from "@main/lib/ipc" + +/** + * Registers the IPC handlers for the read-only Git service. + */ +export function registerGitCommandsHandlers() { + safeHandle(CONSTANTS.ipc.gitCommandsIsRepository, (_event, dir: string) => isRepository(dir)) + safeHandle(CONSTANTS.ipc.gitCommandsGetRepositoryRoot, (_event, dir: string) => getRepositoryRoot(dir)) + safeHandle(CONSTANTS.ipc.gitCommandsGetStatus, (_event, dir: string) => getStatus(dir)) + safeHandle(CONSTANTS.ipc.gitCommandsListBranches, (_event, dir: string) => listBranches(dir)) + safeHandle(CONSTANTS.ipc.gitCommandsGetLog, (_event, dir: string, limit?: number) => getLog(dir, limit)) + safeHandle(CONSTANTS.ipc.gitCommandsGetRemoteUrl, (_event, dir: string) => getRemoteUrl(dir)) +} diff --git a/src/main/lib/git/parse.ts b/src/main/lib/gitCommands/parse.ts similarity index 99% rename from src/main/lib/git/parse.ts rename to src/main/lib/gitCommands/parse.ts index 72197bc..f3aabdb 100644 --- a/src/main/lib/git/parse.ts +++ b/src/main/lib/gitCommands/parse.ts @@ -1,5 +1,5 @@ import CONSTANTS from "@/main/lib/constants" -import type { GitBranch, GitCommit, GitFileChange, GitStatus, GitStatusCode } from "@/main/types/git" +import type { GitBranch, GitCommit, GitFileChange, GitStatus, GitStatusCode } from "@/main/types/gitCommands" /** * Normalizes a single porcelain status character into a {@link GitStatusCode}. diff --git a/src/main/lib/git/service.ts b/src/main/lib/gitCommands/service.ts similarity index 97% rename from src/main/lib/git/service.ts rename to src/main/lib/gitCommands/service.ts index e5956ce..4365fa6 100644 --- a/src/main/lib/git/service.ts +++ b/src/main/lib/gitCommands/service.ts @@ -1,8 +1,8 @@ import CONSTANTS from "@main/lib/constants" -import { runGit } from "@main/lib/git/exec" -import { parseBranches, parseLog, parseStatus } from "@main/lib/git/parse" +import { runGit } from "@main/lib/gitCommands/exec" +import { parseBranches, parseLog, parseStatus } from "@main/lib/gitCommands/parse" import GIT_CONFIG from "@/main/config/git" -import type { GitBranch, GitCommit, GitStatus } from "@/main/types/git" +import type { GitBranch, GitCommit, GitStatus } from "@/main/types/gitCommands" /** * The `git log` pretty-format string built from the shared format fields, diff --git a/src/main/lib/ipc.ts b/src/main/lib/ipc.ts new file mode 100644 index 0000000..bfbfdf9 --- /dev/null +++ b/src/main/lib/ipc.ts @@ -0,0 +1,30 @@ +import { type IpcMainInvokeEvent, ipcMain } from "electron" +import type { IpcResult } from "@/main/types/ipc" + +/** + * Registers an IPC handler that catches its own errors and returns an + * `IpcResult`, so `ipcMain.handle` never rejects. + * @param channel The IPC channel to register. + * @param fn The handler body. + */ +export function safeHandle( + channel: string, + fn: (event: IpcMainInvokeEvent, ...args: Args) => Promise | T, +): void { + ipcMain.handle(channel, async (event, ...args): Promise> => { + try { + return { + ok: true, + value: await fn(event, ...(args as Args)), + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + console.error(`[ipc] ${channel} failed:`, message) + + return { + ok: false, + error: message, + } + } + }) +} diff --git a/src/main/lib/lfs/handlers.ts b/src/main/lib/lfs/handlers.ts deleted file mode 100644 index 702f383..0000000 --- a/src/main/lib/lfs/handlers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import { getLockableFiles, listLocks, lockPaths, unlockPaths } from "@main/lib/lfs/service" -import { getConfig, updateConfig } from "@main/lib/store" -import { ipcMain } from "electron" - -/** - * Registers the IPC handlers that expose the Git LFS locking service to the - * renderer process. - */ -export function registerLfsHandlers() { - ipcMain.handle(CONSTANTS.ipc.lfsListLocks, async (_event, dir: string) => { - const locks = await listLocks(dir) - - updateConfig(config => { - config.lfsLockCache[dir] = locks - return config - }) - - return locks - }) - - ipcMain.handle(CONSTANTS.ipc.lfsGetCachedLocks, async (_event, dir: string) => { - const config = await getConfig() - return config.lfsLockCache[dir] ?? [] - }) - - ipcMain.handle(CONSTANTS.ipc.lfsGetLockableFiles, (_event, dir: string) => getLockableFiles(dir)) - - ipcMain.handle(CONSTANTS.ipc.lfsLockPaths, (_event, dir: string, paths: string[]) => lockPaths(dir, paths)) - - ipcMain.handle(CONSTANTS.ipc.lfsUnlockPaths, (_event, dir: string, paths: string[], force?: boolean) => - unlockPaths(dir, paths, force), - ) -} diff --git a/src/main/lib/lfsCommands/handlers.ts b/src/main/lib/lfsCommands/handlers.ts new file mode 100644 index 0000000..fd1b340 --- /dev/null +++ b/src/main/lib/lfsCommands/handlers.ts @@ -0,0 +1,61 @@ +import CONSTANTS from "@main/lib/constants" +import { safeHandle } from "@main/lib/ipc" +import { getLockableFiles, listLocks, lockPaths, migrateLocks, unlockPaths } from "@main/lib/lfsCommands/service" +import { getConfig, updateConfig } from "@main/lib/store" +import type { LfsLockProgress } from "@/main/types/lfsCommands" + +/** + * Registers the IPC handlers for the Git LFS locking service. + */ +export function registerLfsCommandsHandlers() { + safeHandle(CONSTANTS.ipc.lfsCommandsListLocks, async (_event, dir: string) => { + const locks = await listLocks(dir) + + updateConfig(config => { + config.lfsLockCache[dir] = locks + return config + }) + + return locks + }) + + safeHandle(CONSTANTS.ipc.lfsCommandsGetCachedLocks, async (_event, dir: string) => { + const config = await getConfig() + return config.lfsLockCache[dir] ?? [] + }) + + safeHandle(CONSTANTS.ipc.lfsCommandsGetLockableFiles, (_event, dir: string) => getLockableFiles(dir)) + + safeHandle(CONSTANTS.ipc.lfsCommandsLockPaths, (event, dir: string, paths: string[], requestId: string | null) => + lockPaths(dir, paths, (done, total) => { + if (!requestId || event.sender.isDestroyed()) return + const payload: LfsLockProgress = { requestId, done, total } + event.sender.send(CONSTANTS.ipc.lfsCommandsLockProgress, payload) + }), + ) + + safeHandle( + CONSTANTS.ipc.lfsCommandsUnlockPaths, + (event, dir: string, paths: string[], force: boolean | undefined, requestId: string | null) => + unlockPaths(dir, paths, force, (done, total) => { + if (!requestId || event.sender.isDestroyed()) return + const payload: LfsLockProgress = { requestId, done, total } + event.sender.send(CONSTANTS.ipc.lfsCommandsLockProgress, payload) + }), + ) + + safeHandle(CONSTANTS.ipc.lfsCommandsMigrateLocks, async (_event, dir: string) => { + const migrations = await migrateLocks(dir) + + // Any migration that actually touched a lock invalidates the cached lock list + if (migrations.some(m => m.status === "migrated" || m.status === "failed-unlock")) { + const locks = await listLocks(dir) + updateConfig(config => { + config.lfsLockCache[dir] = locks + return config + }) + } + + return migrations + }) +} diff --git a/src/main/lib/lfs/parse.ts b/src/main/lib/lfsCommands/parse.ts similarity index 96% rename from src/main/lib/lfs/parse.ts rename to src/main/lib/lfsCommands/parse.ts index d46d587..44cea5f 100644 --- a/src/main/lib/lfs/parse.ts +++ b/src/main/lib/lfsCommands/parse.ts @@ -1,4 +1,4 @@ -import type { LfsLock } from "@/main/types/lfs" +import type { LfsLock } from "@/main/types/lfsCommands" /** * The shape of a single lock entry in `git lfs locks --json` output. diff --git a/src/main/lib/lfs/service.ts b/src/main/lib/lfsCommands/service.ts similarity index 50% rename from src/main/lib/lfs/service.ts rename to src/main/lib/lfsCommands/service.ts index 81ab15c..3ea4f5b 100644 --- a/src/main/lib/lfs/service.ts +++ b/src/main/lib/lfsCommands/service.ts @@ -1,7 +1,8 @@ -import { runGit } from "@main/lib/git/exec" -import { parseLockablePaths, parseLocks } from "@main/lib/lfs/parse" +import { runGit } from "@main/lib/gitCommands/exec" +import { getStatus } from "@main/lib/gitCommands/service" +import { parseLockablePaths, parseLocks } from "@main/lib/lfsCommands/parse" import GIT_CONFIG from "@/main/config/git" -import type { LfsLock, LfsLockResult } from "@/main/types/lfs" +import type { LfsLock, LfsLockMigration, LfsLockResult } from "@/main/types/lfsCommands" /** * Reads the configured Git user name, used to tell apart locks owned by the @@ -125,30 +126,179 @@ async function runLockCommand(dir: string, args: string[], file: string): Promis } /** - * Locks every lockable file covered by the given paths, in parallel. + * Locks every lockable file covered by the given paths, in parallel, reporting + * per-file completion through the optional `onProgress` callback so a caller + * can render a live progress bar. * @param dir A path inside the repository. * @param paths The repository-relative paths to lock (files or folders). + * @param onProgress Called once with `(0, total)` after expansion, then again + * as each per-file command settles, until `done === total`. * @returns The per-file results. */ -export async function lockPaths(dir: string, paths: string[]): Promise { +export async function lockPaths( + dir: string, + paths: string[], + onProgress?: (done: number, total: number) => void, +): Promise { const files = await expandToLockableFiles(dir, paths) - return Promise.all(files.map(file => runLockCommand(dir, ["lfs", "lock", file], file))) + const total = files.length + + onProgress?.(0, total) + + let done = 0 + return Promise.all( + files.map(async file => { + const result = await runLockCommand(dir, ["lfs", "lock", file], file) + done += 1 + onProgress?.(done, total) + return result + }), + ) } /** - * Unlocks every lockable file covered by the given paths, in parallel. + * Unlocks every lockable file covered by the given paths, in parallel, reporting + * per-file completion through the optional `onProgress` callback so a caller + * can render a live progress bar. * @param dir A path inside the repository. * @param paths The repository-relative paths to unlock (files or folders). * @param force Whether to force-unlock files locked by other users. + * @param onProgress Called once with `(0, total)` after expansion, then again + * as each per-file command settles, until `done === total`. * @returns The per-file results. */ -export async function unlockPaths(dir: string, paths: string[], force = false): Promise { +export async function unlockPaths( + dir: string, + paths: string[], + force = false, + onProgress?: (done: number, total: number) => void, +): Promise { const files = await expandToLockableFiles(dir, paths) + const total = files.length + + onProgress?.(0, total) + let done = 0 return Promise.all( - files.map(file => { + files.map(async file => { const args = force ? ["lfs", "unlock", "--force", file] : ["lfs", "unlock", file] - return runLockCommand(dir, args, file) + const result = await runLockCommand(dir, args, file) + done += 1 + onProgress?.(done, total) + return result }), ) } + +/** + * Migrates the LFS lock of a single staged rename from the old path to the new + * one: locks the new path first so a failure preserves the existing lock, then + * unlocks the old path only if the new lock succeeded. + * @param dir A path inside the repository. + * @param from The old repository-relative path (source of the rename). + * @param to The new repository-relative path (target of the rename). + * @param oldLock The active lock on `from`, or `undefined` if none. + * @param lockable Whether the new path carries the `lockable` attribute. + * @returns The migration outcome for this rename. + */ +async function migrateOneLock( + dir: string, + from: string, + to: string, + oldLock: LfsLock | undefined, + lockable: boolean, +): Promise { + if (!oldLock) { + return { + from, + to, + status: "skipped-not-locked", + } + } + + if (!oldLock.isMine) { + return { + from, + to, + status: "skipped-not-mine", + } + } + + if (!lockable) { + return { + from, + to, + status: "skipped-not-lockable", + } + } + + const lockResult = await runGit(["lfs", "lock", to], { cwd: dir }) + if (lockResult.exitCode !== 0) { + return { + from, + to, + status: "failed-lock", + error: lockResult.stderr.trim() || undefined, + } + } + + const unlockResult = await runGit(["lfs", "unlock", from], { cwd: dir }) + if (unlockResult.exitCode !== 0) { + return { + from, + to, + status: "failed-unlock", + error: unlockResult.stderr.trim() || undefined, + } + } + + return { + from, + to, + status: "migrated", + } +} + +/** + * Carries LFS locks across staged renames so a locked file keeps its lock + * under its new name. Only locks owned by the current user are migrated, locks + * held by teammates are reported back untouched. + * @param dir A path inside the repository. + * @returns One entry per detected staged rename, describing the migration outcome. + * @throws If reading the status or the locks fails. + */ +export async function migrateLocks(dir: string): Promise { + const status = await getStatus(dir) + + // Filter for renamed files + const renames = status.changes.filter( + (change): change is typeof change & { renamedFrom: string } => typeof change.renamedFrom === "string", + ) + + if (renames.length === 0) return [] + + const locks = await listLocks(dir) + const locksByPath = new Map(locks.map(lock => [lock.path, lock])) + + const lockable = new Set( + await filterLockable( + dir, + renames.map(change => change.path), + ), + ) + + const results: LfsLockMigration[] = [] + for (const change of renames) { + results.push( + await migrateOneLock( + dir, + change.renamedFrom, + change.path, + locksByPath.get(change.renamedFrom), + lockable.has(change.path), + ), + ) + } + + return results +} diff --git a/src/main/lib/project/handlers.ts b/src/main/lib/project/handlers.ts deleted file mode 100644 index 37f0f8c..0000000 --- a/src/main/lib/project/handlers.ts +++ /dev/null @@ -1,28 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import { - addLocalProject, - clearRecentProjects, - getPreferences, - getRecentProjects, - openProject, - removeRecentProject, - setPreferences, -} from "@main/lib/project/service" -import { BrowserWindow, ipcMain } from "electron" -import type { AppPreferences } from "@/main/types/store" - -/** - * Registers the IPC handlers that expose the project service (folder picker, - * recent projects, and preferences) to the renderer process. - */ -export function registerProjectHandlers() { - ipcMain.handle(CONSTANTS.ipc.projectAddLocal, event => addLocalProject(BrowserWindow.fromWebContents(event.sender))) - ipcMain.handle(CONSTANTS.ipc.projectOpen, (_event, dir: string) => openProject(dir)) - ipcMain.handle(CONSTANTS.ipc.projectGetRecent, () => getRecentProjects()) - ipcMain.handle(CONSTANTS.ipc.projectRemoveRecent, (_event, dir: string) => removeRecentProject(dir)) - ipcMain.handle(CONSTANTS.ipc.projectClearRecent, () => clearRecentProjects()) - ipcMain.handle(CONSTANTS.ipc.projectGetPreferences, () => getPreferences()) - ipcMain.handle(CONSTANTS.ipc.projectSetPreferences, (_event, preferences: Partial) => - setPreferences(preferences), - ) -} diff --git a/src/main/lib/projects/handlers.ts b/src/main/lib/projects/handlers.ts new file mode 100644 index 0000000..9576388 --- /dev/null +++ b/src/main/lib/projects/handlers.ts @@ -0,0 +1,29 @@ +import CONSTANTS from "@main/lib/constants" +import { safeHandle } from "@main/lib/ipc" +import { + addLocalProject, + clearRecentProjects, + getPreferences, + getRecentProjects, + openProject, + removeRecentProject, + setPreferences, +} from "@main/lib/projects/service" +import { BrowserWindow } from "electron" +import type { AppPreferences } from "@/main/types/store" + +/** + * Registers the IPC handlers for the project service (folder picker, recent + * projects, and preferences). + */ +export function registerProjectsHandlers() { + safeHandle(CONSTANTS.ipc.projectsAddLocal, event => addLocalProject(BrowserWindow.fromWebContents(event.sender))) + safeHandle(CONSTANTS.ipc.projectsOpen, (_event, dir: string) => openProject(dir)) + safeHandle(CONSTANTS.ipc.projectsGetRecent, () => getRecentProjects()) + safeHandle(CONSTANTS.ipc.projectsRemoveRecent, (_event, dir: string) => removeRecentProject(dir)) + safeHandle(CONSTANTS.ipc.projectsClearRecent, () => clearRecentProjects()) + safeHandle(CONSTANTS.ipc.projectsGetPreferences, () => getPreferences()) + safeHandle(CONSTANTS.ipc.projectsSetPreferences, (_event, preferences: Partial) => + setPreferences(preferences), + ) +} diff --git a/src/main/lib/project/service.ts b/src/main/lib/projects/service.ts similarity index 89% rename from src/main/lib/project/service.ts rename to src/main/lib/projects/service.ts index edfaeb6..b6b3593 100644 --- a/src/main/lib/project/service.ts +++ b/src/main/lib/projects/service.ts @@ -1,25 +1,11 @@ -import { access } from "node:fs/promises" import path from "node:path" import STORE_CONFIG from "@main/config/store" -import { getRepositoryRoot, isRepository } from "@main/lib/git/service" +import { getRepositoryRoot, isRepository } from "@main/lib/gitCommands/service" import { getConfig, updateConfig } from "@main/lib/store" +import { pathExists } from "@main/lib/utils/fs" import { app, type BrowserWindow, dialog } from "electron" -import type { OpenProjectResult } from "@/main/types/project" -import type { AppPreferences, Project } from "@/main/types/store" - -/** - * Checks whether a path currently exists on disk. - * @param target The absolute path to check. - * @returns True if the path exists, `false` otherwise. - */ -async function pathExists(target: string): Promise { - try { - await access(target) - return true - } catch { - return false - } -} +import type { OpenProjectResult, Project } from "@/main/types/projects" +import type { AppPreferences } from "@/main/types/store" /** * Records a repository as the most recently opened project, moving it to the diff --git a/src/main/lib/shell/handlers.ts b/src/main/lib/shells/handlers.ts similarity index 85% rename from src/main/lib/shell/handlers.ts rename to src/main/lib/shells/handlers.ts index ca1f340..49f9e29 100644 --- a/src/main/lib/shell/handlers.ts +++ b/src/main/lib/shells/handlers.ts @@ -13,8 +13,8 @@ const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]) * Registers the IPC handler that opens an external URL in the user's default * browser, ignoring any URL that is malformed or uses a disallowed protocol. */ -export function registerShellHandlers() { - ipcMain.on(CONSTANTS.ipc.shellOpenExternal, (_event, url: string) => { +export function registerShellsHandlers() { + ipcMain.on(CONSTANTS.ipc.shellsOpenExternal, (_event, url: string) => { let parsed: URL try { diff --git a/src/main/lib/tree/handlers.ts b/src/main/lib/tree/handlers.ts deleted file mode 100644 index 2eb7bd0..0000000 --- a/src/main/lib/tree/handlers.ts +++ /dev/null @@ -1,11 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import { getFileTree } from "@main/lib/tree/service" -import { ipcMain } from "electron" - -/** - * Registers the IPC handler that exposes the repository file tree to the - * renderer process. - */ -export function registerTreeHandlers() { - ipcMain.handle(CONSTANTS.ipc.treeGetFileTree, (_event, dir: string) => getFileTree(dir)) -} diff --git a/src/main/lib/tree/service.ts b/src/main/lib/tree/service.ts deleted file mode 100644 index 1e523a7..0000000 --- a/src/main/lib/tree/service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { runGit } from "@main/lib/git/exec" -import { filterLockable } from "@main/lib/lfs/service" -import { buildFileTree } from "@main/lib/tree/build" -import type { FileTreeNode } from "@/main/types/tree" - -/** - * Builds the repository file tree, annotating each node with its `lockable` - * state. Lock ownership is intentionally left out: it is dynamic and overlaid - * separately by the renderer from `listLocks`. - * @param dir A path inside the repository. - * @returns The root-level file tree nodes. - * @throws If listing the repository files fails. - */ -export async function getFileTree(dir: string): Promise { - const listed = await runGit(["ls-files", "-z"], { cwd: dir }) - if (listed.exitCode !== 0) { - throw new Error(listed.stderr.trim() || "Failed to list repository files.") - } - - const files = listed.stdout.split("\0").filter(Boolean) - if (files.length === 0) return [] - - const lockable = new Set(await filterLockable(dir, files)) - - return buildFileTree(files, lockable) -} diff --git a/src/main/lib/uassets/bufferReader.ts b/src/main/lib/uassets/bufferReader.ts new file mode 100644 index 0000000..c693716 --- /dev/null +++ b/src/main/lib/uassets/bufferReader.ts @@ -0,0 +1,217 @@ +/** + * A position-tracking binary reader for `.uasset` files. + */ +export class UAssetBufferReader { + private readonly view: DataView + private cursor = 0 + private isLittleEndian: boolean + + /** + * @param source Raw buffer or a view over one, the view's `byteOffset`/`byteLength` are respected. + * @param isLittleEndian Endianness for subsequent multi-byte reads, defaults to `true`. + */ + constructor(source: ArrayBuffer | ArrayBufferView, isLittleEndian = true) { + if (source instanceof ArrayBuffer) { + this.view = new DataView(source) + } else { + this.view = new DataView(source.buffer, source.byteOffset, source.byteLength) + } + + this.isLittleEndian = isLittleEndian + } + + /** + * Current byte offset from the start of the buffer. + * @returns The current byte offset. + */ + tell(): number { + return this.cursor + } + + /** + * Move the cursor to an absolute byte position. + */ + seek(position: number): void { + this.cursor = position + } + + /** + * Advance the cursor by `byteCount` bytes without reading. + */ + skip(byteCount: number): void { + this.cursor += byteCount + } + + /** + * Bytes left between the cursor and the end of the buffer. + * @returns The number of bytes remaining. + */ + remaining(): number { + return this.view.byteLength - this.cursor + } + + /** + * True when the cursor has reached or passed the end of the buffer. + * @returns Whether the reader is at end-of-buffer. + */ + atEnd(): boolean { + return this.cursor >= this.view.byteLength + } + + /** + * Toggle endianness for subsequent multi-byte reads. + */ + setLittleEndian(isLittleEndian: boolean): void { + this.isLittleEndian = isLittleEndian + } + + /** + * Read one unsigned byte. + * @returns The unsigned byte value. + */ + uint8(): number { + const value = this.view.getUint8(this.cursor) + this.cursor += 1 + return value + } + + /** + * Read one signed byte. + * @returns The signed byte value. + */ + int8(): number { + const value = this.view.getInt8(this.cursor) + this.cursor += 1 + return value + } + + /** + * Read a 16-bit unsigned integer at the current endianness. + * @returns The 16-bit unsigned integer. + */ + uint16(): number { + const value = this.view.getUint16(this.cursor, this.isLittleEndian) + this.cursor += 2 + return value + } + + /** + * Read a 16-bit signed integer at the current endianness. + * @returns The 16-bit signed integer. + */ + int16(): number { + const value = this.view.getInt16(this.cursor, this.isLittleEndian) + this.cursor += 2 + return value + } + + /** + * Read a 32-bit unsigned integer at the current endianness. + * @returns The 32-bit unsigned integer. + */ + uint32(): number { + const value = this.view.getUint32(this.cursor, this.isLittleEndian) + this.cursor += 4 + return value + } + + /** + * Read a 32-bit signed integer at the current endianness. + * @returns The 32-bit signed integer. + */ + int32(): number { + const value = this.view.getInt32(this.cursor, this.isLittleEndian) + this.cursor += 4 + return value + } + + /** + * Read a 64-bit unsigned integer as `bigint` at the current endianness. + * @returns The 64-bit unsigned integer as `bigint`. + */ + uint64(): bigint { + const value = this.view.getBigUint64(this.cursor, this.isLittleEndian) + this.cursor += 8 + return value + } + + /** + * Read a 64-bit signed integer as `bigint` at the current endianness. + * @returns The 64-bit signed integer as `bigint`. + */ + int64(): bigint { + const value = this.view.getBigInt64(this.cursor, this.isLittleEndian) + this.cursor += 8 + return value + } + + /** + * Read a 32-bit IEEE-754 float at the current endianness. + * @returns The 32-bit float value. + */ + float32(): number { + const value = this.view.getFloat32(this.cursor, this.isLittleEndian) + this.cursor += 4 + return value + } + + /** + * Read a 64-bit IEEE-754 float at the current endianness. + * @returns The 64-bit float value. + */ + float64(): number { + const value = this.view.getFloat64(this.cursor, this.isLittleEndian) + this.cursor += 8 + return value + } + + /** + * Read a UE boolean, serialized as int32 (0 or 1). + * @returns The decoded boolean value. + */ + bool32(): boolean { + return this.int32() !== 0 + } + + /** + * Read `byteCount` bytes into a fresh (non-aliasing) `Uint8Array`. + * @returns A fresh `Uint8Array` containing the read bytes. + */ + bytes(byteCount: number): Uint8Array { + const start = this.view.byteOffset + this.cursor + const slice = new Uint8Array(this.view.buffer.slice(start, start + byteCount)) + this.cursor += byteCount + return slice + } + + /** + * Read a UE `FString`, an int32 length prefix followed by string bytes, ANSI when positive + * or UTF-16LE when negative, the stored length includes the null terminator which is stripped. + * @returns The decoded string, or the empty string when the length prefix is zero. + */ + fstring(): string { + const length = this.int32() + if (length === 0) return "" + + if (length > 0) { + const chars = this.bytes(length) + return new TextDecoder("ascii").decode(chars.subarray(0, length - 1)) + } + + const charCount = -length + const chars = this.bytes(charCount * 2) + return new TextDecoder("utf-16le").decode(chars.subarray(0, (charCount - 1) * 2)) + } + + /** + * Read a UE `FGuid` (16 bytes as four `uint32`) as its canonical 32-char upper-hex string. + * @returns The GUID formatted as 32 upper-case hex characters. + */ + fguid(): string { + const a = this.uint32().toString(16).padStart(8, "0") + const b = this.uint32().toString(16).padStart(8, "0") + const c = this.uint32().toString(16).padStart(8, "0") + const d = this.uint32().toString(16).padStart(8, "0") + return (a + b + c + d).toUpperCase() + } +} diff --git a/src/main/lib/uassets/enums/index.ts b/src/main/lib/uassets/enums/index.ts new file mode 100644 index 0000000..bfff42b --- /dev/null +++ b/src/main/lib/uassets/enums/index.ts @@ -0,0 +1,4 @@ +export { UAssetObjectVersionUE4 } from "@main/lib/uassets/enums/objectVersionUE4" +export { UAssetObjectVersionUE5 } from "@main/lib/uassets/enums/objectVersionUE5" +export { UAssetPackageFileTag } from "@main/lib/uassets/enums/packageFileTag" +export { UAssetPackageFlags } from "@main/lib/uassets/enums/packageFlags" diff --git a/src/main/lib/uassets/enums/objectVersionUE4.ts b/src/main/lib/uassets/enums/objectVersionUE4.ts new file mode 100644 index 0000000..1267f49 --- /dev/null +++ b/src/main/lib/uassets/enums/objectVersionUE4.ts @@ -0,0 +1,951 @@ +import type { UAssetEnumEntry } from "@/main/types/uassets" + +/** + * UE4 package format version, read from `PackageFileSummary.FileVersionUE4`, frozen at 522 since UE5. + * @see /Engine/Source/Runtime/Core/Public/UObject/ObjectVersion.h + */ +export const UAssetObjectVersionUE4 = { + VER_UE4_OLDEST_LOADABLE_PACKAGE: { value: 214, comment: "Oldest package managed by Unreal Engine" }, + VER_UE4_BLUEPRINT_VARS_NOT_READ_ONLY: { + value: 215, + comment: "Removed restriction on blueprint-exposed variables from being read-only", + }, + VER_UE4_STATIC_MESH_STORE_NAV_COLLISION: { + value: 216, + comment: "Added manually serialized element to UStaticMesh (precalculated nav collision)", + }, + VER_UE4_ATMOSPHERIC_FOG_DECAY_NAME_CHANGE: { value: 217, comment: "Changed property name for atmospheric fog" }, + VER_UE4_SCENECOMP_TRANSLATION_TO_LOCATION: { + value: 218, + comment: "Change many properties/functions from Translation to Location", + }, + VER_UE4_MATERIAL_ATTRIBUTES_REORDERING: { value: 219, comment: "Material attributes reordering" }, + VER_UE4_COLLISION_PROFILE_SETTING: { + value: 220, + comment: "Collision Profile setting has been added, and all components that exists has to be properly upgraded", + }, + VER_UE4_BLUEPRINT_SKEL_TEMPORARY_TRANSIENT: { + value: 221, + comment: "Making the blueprint's skeleton class transient", + }, + VER_UE4_BLUEPRINT_SKEL_SERIALIZED_AGAIN: { + value: 222, + comment: "Making the blueprint's skeleton class serialized again", + }, + VER_UE4_BLUEPRINT_SETS_REPLICATION: { value: 223, comment: "Blueprint now controls replication settings again" }, + VER_UE4_WORLD_LEVEL_INFO: { value: 224, comment: "Added level info used by World browser" }, + VER_UE4_AFTER_CAPSULE_HALF_HEIGHT_CHANGE: { + value: 225, + comment: "Changed capsule height to capsule half-height (afterwards)", + }, + VER_UE4_ADDED_NAMESPACE_AND_KEY_DATA_TO_FTEXT: { + value: 226, + comment: "Added Namepace, GUID (Key) and Flags to FText", + }, + VER_UE4_ATTENUATION_SHAPES: { value: 227, comment: "Attenuation shapes" }, + VER_UE4_LIGHTCOMPONENT_USE_IES_TEXTURE_MULTIPLIER_ON_NON_IES_BRIGHTNESS: { + value: 228, + comment: "Use IES texture multiplier even when IES brightness is not being used", + }, + VER_UE4_REMOVE_INPUT_COMPONENTS_FROM_BLUEPRINTS: { + value: 229, + comment: "Removed InputComponent as a blueprint addable component", + }, + VER_UE4_VARK2NODE_USE_MEMBERREFSTRUCT: { + value: 230, + comment: "Use an FMemberReference struct in UK2Node_Variable", + }, + VER_UE4_REFACTOR_MATERIAL_EXPRESSION_SCENECOLOR_AND_SCENEDEPTH_INPUTS: { + value: 231, + comment: + "Refactored material expression inputs for UMaterialExpressionSceneColor and UMaterialExpressionSceneDepth", + }, + VER_UE4_SPLINE_MESH_ORIENTATION: { value: 232, comment: "Spline meshes changed from Z forwards to configurable" }, + VER_UE4_REVERB_EFFECT_ASSET_TYPE: { value: 233, comment: "Added ReverbEffect asset type" }, + VER_UE4_MAX_TEXCOORD_INCREASED: { value: 234, comment: "changed max texcoords from 4 to 8" }, + VER_UE4_SPEEDTREE_STATICMESH: { value: 235, comment: "static meshes changed to support SpeedTrees" }, + VER_UE4_LANDSCAPE_COMPONENT_LAZY_REFERENCES: { + value: 236, + comment: "Landscape component reference between landscape component and collision component", + }, + VER_UE4_SWITCH_CALL_NODE_TO_USE_MEMBER_REFERENCE: { + value: 237, + comment: "Refactored UK2Node_CallFunction to use FMemberReference", + }, + VER_UE4_ADDED_SKELETON_ARCHIVER_REMOVAL: { + value: 238, + comment: "Added fixup step to remove skeleton class references from blueprint objects", + }, + VER_UE4_ADDED_SKELETON_ARCHIVER_REMOVAL_SECOND_TIME: { value: 239, comment: "See above, take 2." }, + VER_UE4_BLUEPRINT_SKEL_CLASS_TRANSIENT_AGAIN: { + value: 240, + comment: "Making the skeleton class on blueprints transient", + }, + VER_UE4_ADD_COOKED_TO_UCLASS: { value: 241, comment: "UClass knows if it's been cooked" }, + VER_UE4_DEPRECATED_STATIC_MESH_THUMBNAIL_PROPERTIES_REMOVED: { + value: 242, + comment: "Deprecated static mesh thumbnail properties were removed", + }, + VER_UE4_COLLECTIONS_IN_SHADERMAPID: { value: 243, comment: "Added collections in material shader map ids" }, + VER_UE4_REFACTOR_MOVEMENT_COMPONENT_HIERARCHY: { + value: 244, + comment: "Renamed some Movement Component properties, added PawnMovementComponent", + }, + VER_UE4_FIX_TERRAIN_LAYER_SWITCH_ORDER: { + value: 245, + comment: "Swap UMaterialExpressionTerrainLayerSwitch::LayerUsed/LayerNotUsed the correct way round", + }, + VER_UE4_ALL_PROPS_TO_CONSTRAINTINSTANCE: { value: 246, comment: "Remove URB_ConstraintSetup" }, + VER_UE4_LOW_QUALITY_DIRECTIONAL_LIGHTMAPS: { value: 247, comment: "Low quality directional lightmaps" }, + VER_UE4_ADDED_NOISE_EMITTER_COMPONENT: { + value: 248, + comment: "Added NoiseEmitterComponent and removed related Pawn properties.", + }, + VER_UE4_ADD_TEXT_COMPONENT_VERTICAL_ALIGNMENT: { value: 249, comment: "Add text component vertical alignment" }, + VER_UE4_ADDED_FBX_ASSET_IMPORT_DATA: { + value: 250, + comment: "Added AssetImportData for FBX asset types, deprecating SourceFilePath and SourceFileTimestamp", + }, + VER_UE4_REMOVE_LEVELBODYSETUP: { value: 251, comment: "Remove LevelBodySetup from ULevel" }, + VER_UE4_REFACTOR_CHARACTER_CROUCH: { value: 252, comment: "Refactor character crouching" }, + VER_UE4_SMALLER_DEBUG_MATERIALSHADER_UNIFORM_EXPRESSIONS: { + value: 253, + comment: "Trimmed down material shader debug information.", + }, + VER_UE4_APEX_CLOTH: { value: 254, comment: "APEX Clothing" }, + VER_UE4_SAVE_COLLISIONRESPONSE_PER_CHANNEL: { + value: 255, + comment: + "@note!!! once we pass this CL, we can rename FCollisionResponseContainer enum values. we should rename to match ECollisionChannel", + }, + VER_UE4_ADDED_LANDSCAPE_SPLINE_EDITOR_MESH: { value: 256, comment: "Added Landscape Spline editor meshes" }, + VER_UE4_CHANGED_MATERIAL_REFACTION_TYPE: { + value: 257, + comment: "Fixup input expressions for reading from refraction material attributes.", + }, + VER_UE4_REFACTOR_PROJECTILE_MOVEMENT: { + value: 258, + comment: "Refactor projectile movement, along with some other movement component work.", + }, + VER_UE4_REMOVE_PHYSICALMATERIALPROPERTY: { + value: 259, + comment: "Remove PhysicalMaterialProperty and replace with user defined enum", + }, + VER_UE4_PURGED_FMATERIAL_COMPILE_OUTPUTS: { value: 260, comment: "Removed all compile outputs from FMaterial" }, + VER_UE4_ADD_COOKED_TO_LANDSCAPE: { value: 261, comment: "Ability to save cooked PhysX meshes to Landscape" }, + VER_UE4_CONSUME_INPUT_PER_BIND: { value: 262, comment: "Change how input component consumption works" }, + VER_UE4_SOUND_CLASS_GRAPH_EDITOR: { value: 263, comment: "Added new Graph based SoundClass Editor" }, + VER_UE4_FIXUP_TERRAIN_LAYER_NODES: { + value: 264, + comment: "Fixed terrain layer node guids which was causing artifacts", + }, + VER_UE4_RETROFIT_CLAMP_EXPRESSIONS_SWAP: { + value: 265, + comment: "Added clamp min/max swap check to catch older materials", + }, + VER_UE4_REMOVE_LIGHT_MOBILITY_CLASSES: { value: 266, comment: "Remove static/movable/stationary light classes" }, + VER_UE4_REFACTOR_PHYSICS_BLENDING: { + value: 267, + comment: "Refactor the way physics blending works to allow partial blending", + }, + VER_UE4_WORLD_LEVEL_INFO_UPDATED: { + value: 268, + comment: "WorldLevelInfo: Added reference to parent level and streaming distance", + }, + VER_UE4_STATIC_SKELETAL_MESH_SERIALIZATION_FIX: { + value: 269, + comment: "Fixed cooking of skeletal/static meshes due to bad serialization logic", + }, + VER_UE4_REMOVE_STATICMESH_MOBILITY_CLASSES: { value: 270, comment: "Removal of InterpActor and PhysicsActor" }, + VER_UE4_REFACTOR_PHYSICS_TRANSFORMS: { value: 271, comment: "Refactor physics transforms" }, + VER_UE4_REMOVE_ZERO_TRIANGLE_SECTIONS: { + value: 272, + comment: "Remove zero triangle sections from static meshes and compact material indices.", + }, + VER_UE4_CHARACTER_MOVEMENT_DECELERATION: { + value: 273, + comment: "Add param for deceleration in character movement instead of using acceleration.", + }, + VER_UE4_CAMERA_ACTOR_USING_CAMERA_COMPONENT: { + value: 274, + comment: "Made ACameraActor use a UCameraComponent for parameter storage, etc...", + }, + VER_UE4_CHARACTER_MOVEMENT_DEPRECATE_PITCH_ROLL: { + value: 275, + comment: "Deprecated some pitch/roll properties in CharacterMovementComponent", + }, + VER_UE4_REBUILD_TEXTURE_STREAMING_DATA_ON_LOAD: { + value: 276, + comment: "Rebuild texture streaming data on load for uncooked builds", + }, + VER_UE4_SUPPORT_32BIT_STATIC_MESH_INDICES: { + value: 277, + comment: "Add support for 32 bit index buffers for static meshes.", + }, + VER_UE4_ADDED_CHUNKID_TO_ASSETDATA_AND_UPACKAGE: { + value: 278, + comment: "Added streaming install ChunkID to AssetData and UPackage", + }, + VER_UE4_CHARACTER_DEFAULT_MOVEMENT_BINDINGS: { + value: 279, + comment: "Add flag to control whether Character blueprints receive default movement bindings.", + }, + VER_UE4_APEX_CLOTH_LOD: { value: 280, comment: "APEX Clothing LOD Info" }, + VER_UE4_ATMOSPHERIC_FOG_CACHE_DATA: { value: 281, comment: "Added atmospheric fog texture data to be general" }, + VAR_UE4_ARRAY_PROPERTY_INNER_TAGS: { value: 282, comment: "Arrays serialize their inner's tags" }, + VER_UE4_KEEP_SKEL_MESH_INDEX_DATA: { + value: 283, + comment: "Skeletal mesh index data is kept in memory in game to support mesh merging.", + }, + VER_UE4_BODYSETUP_COLLISION_CONVERSION: { + value: 284, + comment: "Added compatibility for the body instance collision change", + }, + VER_UE4_REFLECTION_CAPTURE_COOKING: { value: 285, comment: "Reflection capture cooking" }, + VER_UE4_REMOVE_DYNAMIC_VOLUME_CLASSES: { + value: 286, + comment: "Removal of DynamicTriggerVolume, DynamicBlockingVolume, DynamicPhysicsVolume", + }, + VER_UE4_STORE_HASCOOKEDDATA_FOR_BODYSETUP: { + value: 287, + comment: "Store an additional flag in the BodySetup to indicate whether there is any cooked data to load", + }, + VER_UE4_REFRACTION_BIAS_TO_REFRACTION_DEPTH_BIAS: { + value: 288, + comment: "Changed name of RefractionBias to RefractionDepthBias.", + }, + VER_UE4_REMOVE_SKELETALPHYSICSACTOR: { value: 289, comment: "Removal of SkeletalPhysicsActor" }, + VER_UE4_PC_ROTATION_INPUT_REFACTOR: { value: 290, comment: "PlayerController rotation input refactor" }, + VER_UE4_LANDSCAPE_PLATFORMDATA_COOKING: { value: 291, comment: "Landscape Platform Data cooking" }, + VER_UE4_CREATEEXPORTS_CLASS_LINKING_FOR_BLUEPRINTS: { + value: 292, + comment: "Added call for linking classes in CreateExport to ensure memory is initialized properly", + }, + VER_UE4_REMOVE_NATIVE_COMPONENTS_FROM_BLUEPRINT_SCS: { + value: 293, + comment: "Remove native component nodes from the blueprint SimpleConstructionScript", + }, + VER_UE4_REMOVE_SINGLENODEINSTANCE: { value: 294, comment: "Removal of Single Node Instance" }, + VER_UE4_CHARACTER_BRAKING_REFACTOR: { value: 295, comment: "Character movement braking changes" }, + VER_UE4_VOLUME_SAMPLE_LOW_QUALITY_SUPPORT: { + value: 296, + comment: "Supported low quality lightmaps in volume samples", + }, + VER_UE4_SPLIT_TOUCH_AND_CLICK_ENABLES: { + value: 297, + comment: "Split bEnableTouchEvents out from bEnableClickEvents", + }, + VER_UE4_HEALTH_DEATH_REFACTOR: { value: 298, comment: "Health/Death refactor" }, + VER_UE4_SOUND_NODE_ENVELOPER_CURVE_CHANGE: { + value: 299, + comment: "Moving USoundNodeEnveloper from UDistributionFloatConstantCurve to FRichCurve", + }, + VER_UE4_POINT_LIGHT_SOURCE_RADIUS: { value: 300, comment: "Moved SourceRadius to UPointLightComponent" }, + VER_UE4_SCENE_CAPTURE_CAMERA_CHANGE: { value: 301, comment: "Scene capture actors based on camera actors." }, + VER_UE4_MOVE_SKELETALMESH_SHADOWCASTING: { + value: 302, + comment: "Moving SkeletalMesh shadow casting flag from LoD details to material", + }, + VER_UE4_CHANGE_SETARRAY_BYTECODE: { value: 303, comment: "Changing bytecode operators for creating arrays" }, + VER_UE4_MATERIAL_INSTANCE_BASE_PROPERTY_OVERRIDES: { + value: 304, + comment: "Material Instances overriding base material properties.", + }, + VER_UE4_COMBINED_LIGHTMAP_TEXTURES: { value: 305, comment: "Combined top/bottom lightmap textures" }, + VER_UE4_BUMPED_MATERIAL_EXPORT_GUIDS: { value: 306, comment: "Forced material lightmass guids to be regenerated" }, + VER_UE4_BLUEPRINT_INPUT_BINDING_OVERRIDES: { + value: 307, + comment: "Allow overriding of parent class input bindings", + }, + VER_UE4_FIXUP_BODYSETUP_INVALID_CONVEX_TRANSFORM: { value: 308, comment: "Fix up convex invalid transform" }, + VER_UE4_FIXUP_STIFFNESS_AND_DAMPING_SCALE: { + value: 309, + comment: "Fix up scale of physics stiffness and damping value", + }, + VER_UE4_REFERENCE_SKELETON_REFACTOR: { + value: 310, + comment: "Convert USkeleton and FBoneContrainer to using FReferenceSkeleton.", + }, + VER_UE4_K2NODE_REFERENCEGUIDS: { + value: 311, + comment: "Adding references to variable, function, and macro nodes to be able to update to renamed values", + }, + VER_UE4_FIXUP_ROOTBONE_PARENT: { value: 312, comment: "Fix up the 0th bone's parent bone index." }, + VER_UE4_TEXT_RENDER_COMPONENTS_WORLD_SPACE_SIZING: { + value: 313, + comment: "Allow setting of TextRenderComponents size in world space.", + }, + VER_UE4_MATERIAL_INSTANCE_BASE_PROPERTY_OVERRIDES_PHASE_2: { + value: 314, + comment: "Material Instances overriding base material properties #2.", + }, + VER_UE4_CLASS_NOTPLACEABLE_ADDED: { value: 315, comment: "CLASS_Placeable becomes CLASS_NotPlaceable" }, + VER_UE4_WORLD_LEVEL_INFO_LOD_LIST: { value: 316, comment: "Added LOD info list to a world tile description" }, + VER_UE4_CHARACTER_MOVEMENT_VARIABLE_RENAMING_1: { + value: 317, + comment: "CharacterMovement variable naming refactor", + }, + VER_UE4_FSLATESOUND_CONVERSION: { + value: 318, + comment: "FName properties containing sound names converted to FSlateSound properties", + }, + VER_UE4_WORLD_LEVEL_INFO_ZORDER: { value: 319, comment: "Added ZOrder to a world tile description" }, + VER_UE4_PACKAGE_REQUIRES_LOCALIZATION_GATHER_FLAGGING: { + value: 320, + comment: "Added flagging of localization gather requirement to packages", + }, + VER_UE4_BP_ACTOR_VARIABLE_DEFAULT_PREVENTING: { + value: 321, + comment: "Preventing Blueprint Actor variables from having default values", + }, + VER_UE4_TEST_ANIMCOMP_CHANGE: { + value: 322, + comment: "Preventing Blueprint Actor variables from having default values", + }, + VER_UE4_EDITORONLY_BLUEPRINTS: { value: 323, comment: "Class as primary asset, name convention changed" }, + VER_UE4_EDGRAPHPINTYPE_SERIALIZATION: { value: 324, comment: "Custom serialization for FEdGraphPinType" }, + VER_UE4_NO_MIRROR_BRUSH_MODEL_COLLISION: { + value: 325, + comment: "Stop generating 'mirrored' cooked mesh for Brush and Model components", + }, + VER_UE4_CHANGED_CHUNKID_TO_BE_AN_ARRAY_OF_CHUNKIDS: { + value: 326, + comment: "Changed ChunkID to be an array of IDs.", + }, + VER_UE4_WORLD_NAMED_AFTER_PACKAGE: { + value: 327, + comment: 'Worlds have been renamed from "TheWorld" to be named after the package containing them', + }, + VER_UE4_SKY_LIGHT_COMPONENT: { value: 328, comment: "Added sky light component" }, + VER_UE4_WORLD_LAYER_ENABLE_DISTANCE_STREAMING: { + value: 329, + comment: "Added Enable distance streaming flag to FWorldTileLayer", + }, + VER_UE4_REMOVE_ZONES_FROM_MODEL: { value: 330, comment: "Remove visibility/zone information from UModel" }, + VER_UE4_FIX_ANIMATIONBASEPOSE_SERIALIZATION: { value: 331, comment: "Fix base pose serialization" }, + VER_UE4_SUPPORT_8_BONE_INFLUENCES_SKELETAL_MESHES: { + value: 332, + comment: "Support for up to 8 skinning influences per vertex on skeletal meshes (on non-gpu vertices)", + }, + VER_UE4_ADD_OVERRIDE_GRAVITY_FLAG: { value: 333, comment: "Add explicit bOverrideGravity to world settings" }, + VER_UE4_SUPPORT_GPUSKINNING_8_BONE_INFLUENCES: { + value: 334, + comment: "Support for up to 8 skinning influences per vertex on skeletal meshes (on gpu vertices)", + }, + VER_UE4_ANIM_SUPPORT_NONUNIFORM_SCALE_ANIMATION: { value: 335, comment: "Supporting nonuniform scale animation" }, + VER_UE4_ENGINE_VERSION_OBJECT: { + value: 336, + comment: "Engine version is stored as a FEngineVersion object rather than changelist number", + }, + VER_UE4_PUBLIC_WORLDS: { value: 337, comment: "World assets now have RF_Public" }, + VER_UE4_SKELETON_GUID_SERIALIZATION: { value: 338, comment: "Skeleton Guid" }, + VER_UE4_CHARACTER_MOVEMENT_WALKABLE_FLOOR_REFACTOR: { + value: 339, + comment: "Character movement WalkableFloor refactor", + }, + VER_UE4_INVERSE_SQUARED_LIGHTS_DEFAULT: { value: 340, comment: "Lights default to inverse squared" }, + VER_UE4_DISABLED_SCRIPT_LIMIT_BYTECODE: { value: 341, comment: "Disabled SCRIPT_LIMIT_BYTECODE_TO_64KB" }, + VER_UE4_PRIVATE_REMOTE_ROLE: { value: 342, comment: "Made remote role private, exposed bReplicates" }, + VER_UE4_FOLIAGE_STATIC_MOBILITY: { + value: 343, + comment: + "Fix up old foliage components to have static mobility (superseded by VER_UE4_FOLIAGE_MOVABLE_MOBILITY)", + }, + VER_UE4_BUILD_SCALE_VECTOR: { value: 344, comment: "Change BuildScale from a float to a vector" }, + VER_UE4_FOLIAGE_COLLISION: { + value: 345, + comment: "After implementing foliage collision, need to disable collision on old foliage instances", + }, + VER_UE4_SKY_BENT_NORMAL: { value: 346, comment: "Added sky bent normal to indirect lighting cache" }, + VER_UE4_LANDSCAPE_COLLISION_DATA_COOKING: { value: 347, comment: "Added cooking for landscape collision data" }, + VER_UE4_MORPHTARGET_CPU_TANGENTZDELTA_FORMATCHANGE: { + value: 348, + comment: "we still convert all to FVector in CPU time whenever any calculation", + }, + VER_UE4_SOFT_CONSTRAINTS_USE_MASS: { + value: 349, + comment: "Soft constraint limits will implicitly use the mass of the bodies", + }, + VER_UE4_REFLECTION_DATA_IN_PACKAGES: { value: 350, comment: "Reflection capture data saved in packages" }, + VER_UE4_FOLIAGE_MOVABLE_MOBILITY: { + value: 351, + comment: + "Fix up old foliage components to have movable mobility (superseded by VER_UE4_FOLIAGE_STATIC_LIGHTING_SUPPORT)", + }, + VER_UE4_UNDO_BREAK_MATERIALATTRIBUTES_CHANGE: { + value: 352, + comment: "Undo BreakMaterialAttributes changes as it broke old content", + }, + VER_UE4_ADD_CUSTOMPROFILENAME_CHANGE: { + value: 353, + comment: "Now Default custom profile name isn't NONE anymore due to copy/paste not working properly with it", + }, + VER_UE4_FLIP_MATERIAL_COORDS: { value: 354, comment: "Permanently flip and scale material expression coordinates" }, + VER_UE4_MEMBERREFERENCE_IN_PINTYPE: { + value: 355, + comment: "PinSubCategoryMemberReference added to FEdGraphPinType", + }, + VER_UE4_VEHICLES_UNIT_CHANGE: { + value: 356, + comment: "Vehicles use Nm for Torque instead of cm and RPM instead of rad/s", + }, + VER_UE4_ANIMATION_REMOVE_NANS: { + value: 357, + comment: "now importing should detect NaNs, so we should not have NaNs in source data", + }, + VER_UE4_SKELETON_ASSET_PROPERTY_TYPE_CHANGE: { + value: 358, + comment: "Change skeleton preview attached assets property type", + }, + VER_UE4_FIX_BLUEPRINT_VARIABLE_FLAGS: { + value: 359, + comment: "Fix some blueprint variables that have the CPF_DisableEditOnTemplate flag set when they shouldn't", + }, + VER_UE4_VEHICLES_UNIT_CHANGE2: { + value: 360, + comment: + "Vehicles use Nm for Torque instead of cm and RPM instead of rad/s part two (missed conversion for some variables", + }, + VER_UE4_UCLASS_SERIALIZE_INTERFACES_AFTER_LINKING: { + value: 361, + comment: "Changed order of interface class serialization", + }, + VER_UE4_STATIC_MESH_SCREEN_SIZE_LODS: { value: 362, comment: "Change from LOD distances to display factors" }, + VER_UE4_FIX_MATERIAL_COORDS: { + value: 363, + comment: "Requires test of material coords to ensure they're saved correctly", + }, + VER_UE4_SPEEDTREE_WIND_V7: { value: 364, comment: "Changed SpeedTree wind presets to v7" }, + VER_UE4_LOAD_FOR_EDITOR_GAME: { value: 365, comment: "NeedsLoadForEditorGame added" }, + VER_UE4_SERIALIZE_RICH_CURVE_KEY: { value: 366, comment: "Manual serialization of FRichCurveKey to save space" }, + VER_UE4_MOVE_LANDSCAPE_MICS_AND_TEXTURES_WITHIN_LEVEL: { + value: 367, + comment: + "Change the outer of ULandscapeMaterialInstanceConstants and Landscape-related textures to the level in which they reside", + }, + VER_UE4_FTEXT_HISTORY: { + value: 368, + comment: "FTexts have creation history data, removed Key, Namespaces, and SourceString", + }, + VER_UE4_FIX_MATERIAL_COMMENTS: { + value: 369, + comment: "Shift comments to the left to contain expressions properly", + }, + VER_UE4_STORE_BONE_EXPORT_NAMES: { + value: 370, + comment: + "Bone names stored as FName means that we can't guarantee the correct case on export, now we store a separate string for export purposes only", + }, + VER_UE4_MESH_EMITTER_INITIAL_ORIENTATION_DISTRIBUTION: { + value: 371, + comment: "changed mesh emitter initial orientation to distribution", + }, + VER_UE4_DISALLOW_FOLIAGE_ON_BLUEPRINTS: { value: 372, comment: "Foliage on blueprints causes crashes" }, + VER_UE4_FIXUP_MOTOR_UNITS: { + value: 373, + comment: "change motors to use revolutions per second instead of rads/second", + }, + VER_UE4_DEPRECATED_MOVEMENTCOMPONENT_MODIFIED_SPEEDS: { + value: 374, + comment: 'deprecated MovementComponent functions including "ModifiedMaxSpeed" et al', + }, + VER_UE4_RENAME_CANBECHARACTERBASE: { value: 375, comment: "rename CanBeCharacterBase" }, + VER_UE4_GAMEPLAY_TAG_CONTAINER_TAG_TYPE_CHANGE: { + value: 376, + comment: + "Change GameplayTagContainers to have FGameplayTags instead of FNames; Required to fix-up native serialization", + }, + VER_UE4_FOLIAGE_SETTINGS_TYPE: { + value: 377, + comment: + "Change from UInstancedFoliageSettings to UFoliageType, and change the api from being keyed on UStaticMesh* to UFoliageType*", + }, + VER_UE4_STATIC_SHADOW_DEPTH_MAPS: { value: 378, comment: "Lights serialize static shadow depth maps" }, + VER_UE4_ADD_TRANSACTIONAL_TO_DATA_ASSETS: { + value: 379, + comment: "Add RF_Transactional to data assets, fixing undo problems when editing them", + }, + VER_UE4_ADD_LB_WEIGHTBLEND: { + value: 380, + comment: "Change LB_AlphaBlend to LB_WeightBlend in ELandscapeLayerBlendType", + }, + VER_UE4_ADD_ROOTCOMPONENT_TO_FOLIAGEACTOR: { + value: 381, + comment: "Add root component to an foliage actor, all foliage cluster components will be attached to a root", + }, + VER_UE4_FIX_MATERIAL_PROPERTY_OVERRIDE_SERIALIZE: { + value: 382, + comment: "FMaterialInstanceBasePropertyOverrides didn't use proper UObject serialize", + }, + VER_UE4_ADD_LINEAR_COLOR_SAMPLER: { + value: 383, + comment: + "Addition of linear color sampler. color sample type is changed to linear sampler if source texture !sRGB", + }, + VER_UE4_ADD_STRING_ASSET_REFERENCES_MAP: { + value: 384, + comment: "Added StringAssetReferencesMap to support renames of FStringAssetReference properties.", + }, + VER_UE4_BLUEPRINT_USE_SCS_ROOTCOMPONENT_SCALE: { + value: 385, + comment: + "Apply scale from SCS RootComponent details in the Blueprint Editor to new actor instances at construction time", + }, + VER_UE4_LEVEL_STREAMING_DRAW_COLOR_TYPE_CHANGE: { + value: 386, + comment: "Changed level streaming to have a linear color since the visualization doesn't gamma correct.", + }, + VER_UE4_CLEAR_NOTIFY_TRIGGERS: { value: 387, comment: "Cleared end triggers from non-state anim notifies" }, + VER_UE4_SKELETON_ADD_SMARTNAMES: { + value: 388, + comment: "Convert old curve names stored in anim assets into skeleton smartnames", + }, + VER_UE4_ADDED_CURRENCY_CODE_TO_FTEXT: { + value: 389, + comment: "Added the currency code field to FTextHistory_AsCurrency", + }, + VER_UE4_ENUM_CLASS_SUPPORT: { value: 390, comment: "Added support for C++11 enum classes" }, + VER_UE4_FIXUP_WIDGET_ANIMATION_CLASS: { value: 391, comment: "Fixup widget animation class" }, + VER_UE4_SOUND_COMPRESSION_TYPE_ADDED: { + value: 392, + comment: "USoundWave objects now contain details about compression scheme used.", + }, + VER_UE4_AUTO_WELDING: { value: 393, comment: "Bodies will automatically weld when attached" }, + VER_UE4_RENAME_CROUCHMOVESCHARACTERDOWN: { + value: 394, + comment: "Rename UCharacterMovementComponent::bCrouchMovesCharacterDown", + }, + VER_UE4_LIGHTMAP_MESH_BUILD_SETTINGS: { value: 395, comment: "Lightmap parameters in FMeshBuildSettings" }, + VER_UE4_RENAME_SM3_TO_ES3_1: { + value: 396, + comment: "Rename SM3 to ES3_1 and updates featurelevel material node selector", + }, + VER_UE4_DEPRECATE_UMG_STYLE_ASSETS: { value: 397, comment: "Deprecated separate style assets for use in UMG" }, + VER_UE4_POST_DUPLICATE_NODE_GUID: { + value: 398, + comment: "Duplicating Blueprints will regenerate NodeGuids after this version", + }, + VER_UE4_RENAME_CAMERA_COMPONENT_VIEW_ROTATION: { + value: 399, + comment: + "Rename UCameraComponent::bUseControllerViewRotation to bUsePawnViewRotation (and change the default value)", + }, + VER_UE4_CASE_PRESERVING_FNAME: { value: 400, comment: "Changed FName to be case preserving" }, + VER_UE4_RENAME_CAMERA_COMPONENT_CONTROL_ROTATION: { + value: 401, + comment: "Rename UCameraComponent::bUsePawnViewRotation to bUsePawnControlRotation", + }, + VER_UE4_FIX_REFRACTION_INPUT_MASKING: { value: 402, comment: "Fix bad refraction material attribute masks" }, + VER_UE4_GLOBAL_EMITTER_SPAWN_RATE_SCALE: { value: 403, comment: "A global spawn rate for emitters." }, + VER_UE4_CLEAN_DESTRUCTIBLE_SETTINGS: { value: 404, comment: "Cleanup destructible mesh settings" }, + VER_UE4_CHARACTER_MOVEMENT_UPPER_IMPACT_BEHAVIOR: { + value: 405, + comment: + "CharacterMovementComponent refactor of AdjustUpperHemisphereImpact and deprecation of some associated vars.", + }, + VER_UE4_BP_MATH_VECTOR_EQUALITY_USES_EPSILON: { + value: 406, + comment: + 'Changed Blueprint math equality functions for vectors and rotators to operate as a "nearly" equals rather than "exact"', + }, + VER_UE4_FOLIAGE_STATIC_LIGHTING_SUPPORT: { + value: 407, + comment: "Static lighting support was re-added to foliage, and mobility was returned to static", + }, + VER_UE4_SLATE_COMPOSITE_FONTS: { value: 408, comment: "Added composite fonts to Slate font info" }, + VER_UE4_REMOVE_SAVEGAMESUMMARY: { + value: 409, + comment: "Remove UDEPRECATED_SaveGameSummary, required for UWorld::Serialize", + }, + VER_UE4_REMOVE_SKELETALMESH_COMPONENT_BODYSETUP_SERIALIZATION: { + value: 410, + comment: "Remove bodyseutp serialization from skeletal mesh component", + }, + VER_UE4_SLATE_BULK_FONT_DATA: { + value: 411, + comment: "Made Slate font data use bulk data to store the embedded font data", + }, + VER_UE4_ADD_PROJECTILE_FRICTION_BEHAVIOR: { + value: 412, + comment: "Add new friction behavior in ProjectileMovementComponent.", + }, + VER_UE4_MOVEMENTCOMPONENT_AXIS_SETTINGS: { value: 413, comment: "Add axis settings enum to MovementComponent." }, + VER_UE4_GRAPH_INTERACTIVE_COMMENTBUBBLES: { + value: 414, + comment: "Switch to new interactive comments, requires boundry conversion to preserve previous states", + }, + VER_UE4_LANDSCAPE_SERIALIZE_PHYSICS_MATERIALS: { + value: 415, + comment: "Landscape serializes physical materials for collision objects", + }, + VER_UE4_RENAME_WIDGET_VISIBILITY: { value: 416, comment: "Rename Visiblity on widgets to Visibility" }, + VER_UE4_ANIMATION_ADD_TRACKCURVES: { value: 417, comment: "add track curves for animation" }, + VER_UE4_MONTAGE_BRANCHING_POINT_REMOVAL: { + value: 418, + comment: "Removed BranchingPoints from AnimMontages and converted them to regular AnimNotifies.", + }, + VER_UE4_BLUEPRINT_ENFORCE_CONST_IN_FUNCTION_OVERRIDES: { + value: 419, + comment: "Enforce const-correctness in Blueprint implementations of native C++ const class methods", + }, + VER_UE4_ADD_PIVOT_TO_WIDGET_COMPONENT: { + value: 420, + comment: "Added pivot to widget components, need to load old versions as a 0,0 pivot, new default is 0.5,0.5", + }, + VER_UE4_PAWN_AUTO_POSSESS_AI: { + value: 421, + comment: + "Added finer control over when AI Pawns are automatically possessed. Also renamed Pawn.AutoPossess to Pawn.AutoPossessPlayer indicate this was a setting for players and not AI.", + }, + VER_UE4_FTEXT_HISTORY_DATE_TIMEZONE: { + value: 422, + comment: "Added serialization of timezone to FTextHistory for AsDate operations.", + }, + VER_UE4_SORT_ACTIVE_BONE_INDICES: { + value: 423, + comment: "Sort ActiveBoneIndices on lods so that we can avoid doing it at run time", + }, + VER_UE4_PERFRAME_MATERIAL_UNIFORM_EXPRESSIONS: { + value: 424, + comment: "Added per-frame material uniform expressions", + }, + VER_UE4_MIKKTSPACE_IS_DEFAULT: { + value: 425, + comment: "Make MikkTSpace the default tangent space calculation method for static meshes.", + }, + VER_UE4_LANDSCAPE_GRASS_COOKING: { value: 426, comment: "Only applies to cooked files, grass cooking support." }, + VER_UE4_FIX_SKEL_VERT_ORIENT_MESH_PARTICLES: { + value: 427, + comment: "Fixed code for using the bOrientMeshEmitters property.", + }, + VER_UE4_LANDSCAPE_STATIC_SECTION_OFFSET: { + value: 428, + comment: "Do not change landscape section offset on load under world composition", + }, + VER_UE4_ADD_MODIFIERS_RUNTIME_GENERATION: { + value: 429, + comment: "New options for navigation data runtime generation (static, modifiers only, dynamic)", + }, + VER_UE4_MATERIAL_MASKED_BLENDMODE_TIDY: { + value: 430, + comment: "Tidied up material's handling of masked blend mode.", + }, + VER_UE4_MERGED_ADD_MODIFIERS_RUNTIME_GENERATION_TO_4_7_DEPRECATED: { + value: 431, + comment: + "Original version of VER_UE4_MERGED_ADD_MODIFIERS_RUNTIME_GENERATION_TO_4_7; renumbered to prevent blocking promotion in main.", + }, + VER_UE4_AFTER_MERGED_ADD_MODIFIERS_RUNTIME_GENERATION_TO_4_7_DEPRECATED: { + value: 432, + comment: + "Original version of VER_UE4_AFTER_MERGED_ADD_MODIFIERS_RUNTIME_GENERATION_TO_4_7; renumbered to prevent blocking promotion in main.", + }, + VER_UE4_MERGED_ADD_MODIFIERS_RUNTIME_GENERATION_TO_4_7: { + value: 433, + comment: "After merging VER_UE4_ADD_MODIFIERS_RUNTIME_GENERATION into 4.7 branch", + }, + VER_UE4_AFTER_MERGING_ADD_MODIFIERS_RUNTIME_GENERATION_TO_4_7: { + value: 434, + comment: "After merging VER_UE4_ADD_MODIFIERS_RUNTIME_GENERATION into 4.7 branch", + }, + VER_UE4_SERIALIZE_LANDSCAPE_GRASS_DATA: { + value: 435, + comment: "Landscape grass weightmap data is now generated in the editor and serialized.", + }, + VER_UE4_OPTIONALLY_CLEAR_GPU_EMITTERS_ON_INIT: { + value: 436, + comment: "New property to optionally prevent gpu emitters clearing existing particles on Init().", + }, + VER_UE4_SERIALIZE_LANDSCAPE_GRASS_DATA_MATERIAL_GUID: { + value: 437, + comment: "Also store the Material guid with the landscape grass data", + }, + VER_UE4_BLUEPRINT_GENERATED_CLASS_COMPONENT_TEMPLATES_PUBLIC: { + value: 438, + comment: "Make sure that all template components from blueprint generated classes are flagged as public", + }, + VER_UE4_ACTOR_COMPONENT_CREATION_METHOD: { + value: 439, + comment: + "Split out creation method on ActorComponents to distinguish between native, instance, and simple or user construction script", + }, + VER_UE4_K2NODE_EVENT_MEMBER_REFERENCE: { + value: 440, + comment: "K2Node_Event now uses FMemberReference for handling references", + }, + VER_UE4_STRUCT_GUID_IN_PROPERTY_TAG: { value: 441, comment: "FPropertyTag stores GUID of struct" }, + VER_UE4_REMOVE_UNUSED_UPOLYS_FROM_UMODEL: { + value: 442, + comment: "Remove unused UPolys from UModel cooked content", + }, + VER_UE4_REBUILD_HIERARCHICAL_INSTANCE_TREES: { + value: 443, + comment: + 'This doesn\'t do anything except trigger a rebuild on HISMC cluster trees, in this case to get a good "occlusion query" level', + }, + VER_UE4_PACKAGE_SUMMARY_HAS_COMPATIBLE_ENGINE_VERSION: { + value: 444, + comment: + "Package summary includes an CompatibleWithEngineVersion field, separately to the version it's saved with", + }, + VER_UE4_TRACK_UCS_MODIFIED_PROPERTIES: { value: 445, comment: "Track UCS modified properties on Actor Components" }, + VER_UE4_LANDSCAPE_SPLINE_CROSS_LEVEL_MESHES: { + value: 446, + comment: + "Allowed landscape spline meshes to be stored into landscape streaming levels rather than the spline's level", + }, + VER_UE4_DEPRECATE_USER_WIDGET_DESIGN_SIZE: { + value: 447, + comment: "Deprecate the variables used for sizing in the designer on UUserWidget", + }, + VER_UE4_ADD_EDITOR_VIEWS: { value: 448, comment: "Make the editor views array dynamically sized" }, + VER_UE4_FOLIAGE_WITH_ASSET_OR_CLASS: { + value: 449, + comment: "Updated foliage to work with either FoliageType assets or blueprint classes", + }, + VER_UE4_BODYINSTANCE_BINARY_SERIALIZATION: { + value: 450, + comment: "Allows PhysicsSerializer to serialize shapes and actors for faster load times", + }, + VER_UE4_SERIALIZE_BLUEPRINT_EVENTGRAPH_FASTCALLS_IN_UFUNCTION: { + value: 451, + comment: "Added fastcall data serialization directly in UFunction", + }, + VER_UE4_INTERPCURVE_SUPPORTS_LOOPING: { value: 452, comment: "Changes to USplineComponent and FInterpCurve" }, + VER_UE4_MATERIAL_INSTANCE_BASE_PROPERTY_OVERRIDES_DITHERED_LOD_TRANSITION: { + value: 453, + comment: "Material Instances overriding base material LOD transitions", + }, + VER_UE4_SERIALIZE_LANDSCAPE_ES2_TEXTURES: { + value: 454, + comment: "Serialize ES2 textures separately rather than overwriting the properties used on other platforms", + }, + VER_UE4_CONSTRAINT_INSTANCE_MOTOR_FLAGS: { + value: 455, + comment: "Constraint motor velocity is broken into per-component", + }, + VER_UE4_SERIALIZE_PINTYPE_CONST: { value: 456, comment: "Serialize bIsConst in FEdGraphPinType" }, + VER_UE4_LIBRARY_CATEGORIES_AS_FTEXT: { + value: 457, + comment: + "Change UMaterialFunction::LibraryCategories to LibraryCategoriesText (old assets were saved before auto-conversion of FArrayProperty was possible)", + }, + VER_UE4_SKIP_DUPLICATE_EXPORTS_ON_SAVE_PACKAGE: { + value: 458, + comment: "Check for duplicate exports while saving packages.", + }, + VER_UE4_SERIALIZE_TEXT_IN_PACKAGES: { + value: 459, + comment: "Pre-gathering of gatherable, localizable text in packages to optimize text gathering operation times", + }, + VER_UE4_ADD_BLEND_MODE_TO_WIDGET_COMPONENT: { + value: 460, + comment: "Added pivot to widget components, need to load old versions as a 0,0 pivot, new default is 0.5,0.5", + }, + VER_UE4_NEW_LIGHTMASS_PRIMITIVE_SETTING: { value: 461, comment: "Added lightmass primitive setting" }, + VER_UE4_REPLACE_SPRING_NOZ_PROPERTY: { + value: 462, + comment: "Deprecate NoZSpring property on spring nodes to be replaced with TranslateZ property", + }, + VER_UE4_TIGHTLY_PACKED_ENUMS: { + value: 463, + comment: "Keep enums tight and serialize their values as pairs of FName and value. Don't insert dummy values.", + }, + VER_UE4_ASSET_IMPORT_DATA_AS_JSON: { + value: 464, + comment: "Changed Asset import data to serialize file meta data as JSON", + }, + VER_UE4_TEXTURE_LEGACY_GAMMA: { value: 465, comment: "Legacy gamma support for textures." }, + VER_UE4_ADDED_NATIVE_SERIALIZATION_FOR_IMMUTABLE_STRUCTURES: { + value: 466, + comment: + "Added WithSerializer for basic native structures like FVector, FColor etc to improve serialization performance", + }, + VER_UE4_DEPRECATE_UMG_STYLE_OVERRIDES: { + value: 467, + comment: "Deprecated attributes that override the style on UMG widgets", + }, + VER_UE4_STATIC_SHADOWMAP_PENUMBRA_SIZE: { value: 468, comment: "Shadowmap penumbra size stored" }, + VER_UE4_NIAGARA_DATA_OBJECT_DEV_UI_FIX: { + value: 469, + comment: "Fix BC on Niagara effects from the data object and dev UI changes.", + }, + VER_UE4_FIXED_DEFAULT_ORIENTATION_OF_WIDGET_COMPONENT: { + value: 470, + comment: "Fixed the default orientation of widget component so it faces down +x", + }, + VER_UE4_REMOVED_MATERIAL_USED_WITH_UI_FLAG: { + value: 471, + comment: "Removed bUsedWithUI flag from UMaterial and replaced it with a new material domain for UI", + }, + VER_UE4_CHARACTER_MOVEMENT_ADD_BRAKING_FRICTION: { + value: 472, + comment: "Added braking friction separate from turning friction.", + }, + VER_UE4_BSP_UNDO_FIX: { value: 473, comment: "Removed TTransArrays from UModel" }, + VER_UE4_DYNAMIC_PARAMETER_DEFAULT_VALUE: { value: 474, comment: "Added default value to dynamic parameter." }, + VER_UE4_STATIC_MESH_EXTENDED_BOUNDS: { value: 475, comment: "Added ExtendedBounds to StaticMesh" }, + VER_UE4_ADDED_NON_LINEAR_TRANSITION_BLENDS: { + value: 476, + comment: "Added non-linear blending to anim transitions, deprecating old types", + }, + VER_UE4_AO_MATERIAL_MASK: { value: 477, comment: "AO Material Mask texture" }, + VER_UE4_NAVIGATION_AGENT_SELECTOR: { + value: 478, + comment: "Replaced navigation agents selection with single structure", + }, + VER_UE4_MESH_PARTICLE_COLLISIONS_CONSIDER_PARTICLE_SIZE: { + value: 479, + comment: "Mesh particle collisions consider particle size.", + }, + VER_UE4_BUILD_MESH_ADJ_BUFFER_FLAG_EXPOSED: { + value: 480, + comment: "Adjacency buffer building no longer automatically handled based on triangle count, user-controlled", + }, + VER_UE4_MAX_ANGULAR_VELOCITY_DEFAULT: { value: 481, comment: "Change the default max angular velocity" }, + VER_UE4_APEX_CLOTH_TESSELLATION: { value: 482, comment: "Build Adjacency index buffer for clothing tessellation" }, + VER_UE4_DECAL_SIZE: { value: 483, comment: "Added DecalSize member, solved backward compatibility" }, + VER_UE4_KEEP_ONLY_PACKAGE_NAMES_IN_STRING_ASSET_REFERENCES_MAP: { + value: 484, + comment: "Keep only package names in StringAssetReferencesMap", + }, + VER_UE4_COOKED_ASSETS_IN_EDITOR_SUPPORT: { + value: 485, + comment: "Support sound cue not saving out editor only data", + }, + VER_UE4_DIALOGUE_WAVE_NAMESPACE_AND_CONTEXT_CHANGES: { + value: 486, + comment: "Updated dialogue wave localization gathering logic.", + }, + VER_UE4_MAKE_ROT_RENAME_AND_REORDER: { + value: 487, + comment: "Renamed MakeRot MakeRotator and rearranged parameters.", + }, + VER_UE4_K2NODE_VAR_REFERENCEGUIDS: { + value: 488, + comment: "K2Node_Variable will properly have the VariableReference Guid set if available", + }, + VER_UE4_SOUND_CONCURRENCY_PACKAGE: { + value: 489, + comment: "Added support for sound concurrency settings structure and overrides", + }, + VER_UE4_USERWIDGET_DEFAULT_FOCUSABLE_FALSE: { + value: 490, + comment: "Changing the default value for focusable user widgets to false", + }, + VER_UE4_BLUEPRINT_CUSTOM_EVENT_CONST_INPUT: { + value: 491, + comment: "Custom event nodes implicitly set 'const' on array and non-array pass-by-reference input params", + }, + VER_UE4_USE_LOW_PASS_FILTER_FREQ: { value: 492, comment: "Renamed HighFrequencyGain to LowPassFilterFrequency" }, + VER_UE4_NO_ANIM_BP_CLASS_IN_GAMEPLAY_CODE: { + value: 493, + comment: + "UAnimBlueprintGeneratedClass can be replaced by a dynamic class. Use TSubclassOf instead.", + }, + VER_UE4_SCS_STORES_ALLNODES_ARRAY: { + value: 494, + comment: + "The SCS keeps a list of all nodes in its hierarchy rather than recursively building it each time it is requested", + }, + VER_UE4_FBX_IMPORT_DATA_RANGE_ENCAPSULATION: { + value: 495, + comment: "Moved StartRange and EndRange in UFbxAnimSequenceImportData to use FInt32Interval", + }, + VER_UE4_CAMERA_COMPONENT_ATTACH_TO_ROOT: { + value: 496, + comment: "Adding a new root scene component to camera component", + }, + VER_UE4_INSTANCED_STEREO_UNIFORM_UPDATE: { + value: 497, + comment: "Updating custom material expression nodes for instanced stereo implementation", + }, + VER_UE4_STREAMABLE_TEXTURE_MIN_MAX_DISTANCE: { + value: 498, + comment: "Texture streaming min and max distance to handle HLOD", + }, + VER_UE4_INJECT_BLUEPRINT_STRUCT_PIN_CONVERSION_NODES: { + value: 499, + comment: "Fixing up invalid struct-to-struct pin connections by injecting available conversion nodes", + }, + VER_UE4_INNER_ARRAY_TAG_INFO: { value: 500, comment: "Saving tag data for Array Property's inner property" }, + VER_UE4_FIX_SLOT_NAME_DUPLICATION: { + value: 501, + comment: "Fixed duplicating slot node names in skeleton due to skeleton preload on compile", + }, + VER_UE4_STREAMABLE_TEXTURE_AABB: { value: 502, comment: "Texture streaming using AABBs instead of Spheres" }, + VER_UE4_PROPERTY_GUID_IN_PROPERTY_TAG: { value: 503, comment: "FPropertyTag stores GUID of property" }, + VER_UE4_NAME_HASHES_SERIALIZED: { + value: 504, + comment: "Name table hashes are calculated and saved out rather than at load time", + }, + VER_UE4_INSTANCED_STEREO_UNIFORM_REFACTOR: { + value: 505, + comment: "Updating custom material expression nodes for instanced stereo implementation refactor", + }, + VER_UE4_COMPRESSED_SHADER_RESOURCES: { + value: 506, + comment: "Added compression to the shader resource for memory savings", + }, + VER_UE4_PRELOAD_DEPENDENCIES_IN_COOKED_EXPORTS: { + value: 507, + comment: + "Cooked files contain the dependency graph for the event driven loader (the serialization is largely independent of the use of the new loader)", + }, + VER_UE4_TEMPLATEINDEX_IN_COOKED_EXPORTS: { + value: 508, + comment: + "Cooked files contain the TemplateIndex used by the event driven loader (the serialization is largely independent of the use of the new loader, i.e. this will be null if cooking for the old loader)", + }, + VER_UE4_PROPERTY_TAG_SET_MAP_SUPPORT: { + value: 509, + comment: "FPropertyTag includes contained type(s) for Set and Map properties:", + }, + VER_UE4_ADDED_SEARCHABLE_NAMES: { + value: 510, + comment: "Added SearchableNames to the package summary and asset registry", + }, + VER_UE4_64BIT_EXPORTMAP_SERIALSIZES: { + value: 511, + comment: + "Increased size of SerialSize and SerialOffset in export map entries to 64 bit, allow support for bigger files", + }, + VER_UE4_SKYLIGHT_MOBILE_IRRADIANCE_MAP: { + value: 512, + comment: "Sky light stores IrradianceMap for mobile renderer.", + }, + VER_UE4_ADDED_SWEEP_WHILE_WALKING_FLAG: { + value: 513, + comment: "Added flag to control sweep behavior while walking in UCharacterMovementComponent.", + }, + VER_UE4_ADDED_SOFT_OBJECT_PATH: { + value: 514, + comment: + "StringAssetReference changed to SoftObjectPath and swapped to serialize as a name+string instead of a string", + }, + VER_UE4_POINTLIGHT_SOURCE_ORIENTATION: { + value: 515, + comment: "Changed the source orientation of point lights to match spot lights (z axis)", + }, + VER_UE4_ADDED_PACKAGE_SUMMARY_LOCALIZATION_ID: { + value: 516, + comment: "LocalizationId has been added to the package summary (editor-only)", + }, + VER_UE4_FIX_WIDE_STRING_CRC: { + value: 517, + comment: "Fixed case insensitive hashes of wide strings containing character values from 128-255", + }, + VER_UE4_ADDED_PACKAGE_OWNER: { value: 518, comment: "Added package owner to allow private references" }, + VER_UE4_SKINWEIGHT_PROFILE_DATA_LAYOUT_CHANGES: { + value: 519, + comment: "Changed the data layout for skin weight profile data", + }, + VER_UE4_NON_OUTER_PACKAGE_IMPORT: { + value: 520, + comment: "Added import that can have package different than their outer", + }, + VER_UE4_ASSETREGISTRY_DEPENDENCYFLAGS: { value: 521, comment: "Added DependencyFlags to AssetRegistry" }, + VER_UE4_CORRECT_LICENSEE_FLAG: { value: 522, comment: "Fixed corrupt licensee flag in 4.26 assets" }, + VER_UE4_AUTOMATIC_VERSION_PLUS_ONE: { value: 523, comment: "Last version +1" }, + VER_UE4_AUTOMATIC_VERSION: { value: 522, comment: "Fixed corrupt licensee flag in 4.26 assets" }, +} as const satisfies Record diff --git a/src/main/lib/uassets/enums/objectVersionUE5.ts b/src/main/lib/uassets/enums/objectVersionUE5.ts new file mode 100644 index 0000000..8ea4fad --- /dev/null +++ b/src/main/lib/uassets/enums/objectVersionUE5.ts @@ -0,0 +1,70 @@ +import type { UAssetEnumEntry } from "@/main/types/uassets" + +/** + * UE5 package format version, read from `PackageFileSummary.FileVersionUE5`, entries drop the `VER_UE5_` prefix to match the current scoped `enum class`. + * @see /Engine/Source/Runtime/Core/Public/UObject/ObjectVersion.h + */ +export const UAssetObjectVersionUE5 = { + INITIAL_VERSION: { + value: 1000, + comment: + "The original UE5 version, at the time this was added the UE4 version was 522, so UE5 will start from 1000 to show a clear difference", + }, + NAMES_REFERENCED_FROM_EXPORT_DATA: { + value: 1001, + comment: "Support stripping names that are not referenced from export data", + }, + PAYLOAD_TOC: { value: 1002, comment: "Added a payload table of contents to the package summary" }, + OPTIONAL_RESOURCES: { + value: 1003, + comment: "Added data to identify references from and to optional package", + }, + LARGE_WORLD_COORDINATES: { + value: 1004, + comment: "Large world coordinates converts a number of core types to double components by default.", + }, + REMOVE_OBJECT_EXPORT_PACKAGE_GUID: { value: 1005, comment: "Remove package GUID from FObjectExport" }, + TRACK_OBJECT_EXPORT_IS_INHERITED: { value: 1006, comment: "Add IsInherited to the FObjectExport entry" }, + FSOFTOBJECTPATH_REMOVE_ASSET_PATH_FNAMES: { + value: 1007, + comment: "Replace FName asset path in FSoftObjectPath with (package name, asset name) pair FTopLevelAssetPath", + }, + ADD_SOFTOBJECTPATH_LIST: { + value: 1008, + comment: "Add a soft object path list to the package summary for fast remap", + }, + DATA_RESOURCES: { value: 1009, comment: "Added bulk/data resource table" }, + SCRIPT_SERIALIZATION_OFFSET: { + value: 1010, + comment: "Added script property serialization offset to export table entries for saved, versioned packages", + }, + PROPERTY_TAG_EXTENSION_AND_OVERRIDABLE_SERIALIZATION: { + value: 1011, + comment: + "Adding property tag extension, Support for overridable serialization on UObject, Support for overridable logic in containers", + }, + PROPERTY_TAG_COMPLETE_TYPE_NAME: { + value: 1012, + comment: "Added property tag complete type name and serialization type", + }, + ASSETREGISTRY_PACKAGEBUILDDEPENDENCIES: { + value: 1013, + comment: "Changed UE::AssetRegistry::WritePackageData to include PackageBuildDependencies", + }, + METADATA_SERIALIZATION_OFFSET: { + value: 1014, + comment: "Added meta data serialization offset to for saved, versioned packages", + }, + VERSE_CELLS: { value: 1015, comment: "Added VCells to the object graph" }, + PACKAGE_SAVED_HASH: { + value: 1016, + comment: "Changed PackageFileSummary to write FIoHash PackageSavedHash instead of FGuid Guid", + }, + OS_SUB_OBJECT_SHADOW_SERIALIZATION: { value: 1017, comment: "OS shadow serialization of subobjects" }, + IMPORT_TYPE_HIERARCHIES: { + value: 1018, + comment: "Adds a table of hierarchical type information for imports in a package", + }, + AUTOMATIC_VERSION_PLUS_ONE: { value: 1019, comment: "Last version +1" }, + AUTOMATIC_VERSION: { value: 1018, comment: "AUTOMATIC_VERSION_PLUS_ONE - 1" }, +} as const satisfies Record diff --git a/src/main/lib/uassets/enums/packageFileTag.ts b/src/main/lib/uassets/enums/packageFileTag.ts new file mode 100644 index 0000000..ed0eca2 --- /dev/null +++ b/src/main/lib/uassets/enums/packageFileTag.ts @@ -0,0 +1,8 @@ +/** + * Magic numbers at the start of every `.uasset`, the swapped variant flags opposite-endian files. + * @see /Engine/Source/Runtime/CoreUObject/Public/UObject/PackageFileSummary.h + */ +export const UAssetPackageFileTag = { + PACKAGE_FILE_TAG: 0x9e2a83c1, + PACKAGE_FILE_TAG_SWAPPED: 0xc1832a9e, +} as const diff --git a/src/main/lib/uassets/enums/packageFlags.ts b/src/main/lib/uassets/enums/packageFlags.ts new file mode 100644 index 0000000..697c2da --- /dev/null +++ b/src/main/lib/uassets/enums/packageFlags.ts @@ -0,0 +1,88 @@ +import type { UAssetEnumEntry } from "@/main/types/uassets" + +/** + * Bit flags in `PackageFileSummary.PackageFlags`, refreshed against UE 5.8 source. + * @see /Engine/Source/Runtime/CoreUObject/Public/UObject/ObjectMacros.h + */ +export const UAssetPackageFlags = { + PKG_None: { value: 0x00000000, comment: "No flags" }, + PKG_NewlyCreated: { value: 0x00000001, comment: "Newly created package, not saved yet. In editor only." }, + PKG_ClientOptional: { value: 0x00000002, comment: "Purely optional for clients." }, + PKG_ServerSideOnly: { value: 0x00000004, comment: "Only needed on the server side." }, + PKG_CompiledIn: { value: 0x00000010, comment: 'This package is from "compiled in" classes.' }, + PKG_ForDiffing: { value: 0x00000020, comment: "This package was loaded just for the purposes of diffing" }, + PKG_EditorOnly: { + value: 0x00000040, + comment: "This is editor-only package (for example: editor module script package)", + }, + PKG_Developer: { value: 0x00000080, comment: "Developer module" }, + PKG_UncookedOnly: { value: 0x00000100, comment: "Loaded only in uncooked builds (i.e. runtime in editor)" }, + PKG_Cooked: { value: 0x00000200, comment: "Package is cooked" }, + PKG_ContainsNoAsset: { + value: 0x00000400, + comment: "Package doesn't contain any asset object (although asset tags can be present)", + }, + PKG_NotExternallyReferenceable: { + value: 0x00000800, + comment: + "Objects in this package cannot be referenced in a different plugin or mount point (i.e /Game -> /Engine)", + }, + PKG_AccessSpecifierEpicInternal: { + value: 0x00001000, + comment: "Objects in this package can only be referenced in a different plugin or mount point by Epic", + }, + PKG_UnversionedProperties: { + value: 0x00002000, + comment: "Uses unversioned property serialization instead of versioned tagged property serialization", + }, + PKG_ContainsMapData: { + value: 0x00004000, + comment: "Contains map data (UObjects only referenced by a single ULevel) but is stored in a different package", + }, + PKG_IsSaving: { value: 0x00008000, comment: "Temporarily set on a package while it is being saved." }, + PKG_Compiling: { value: 0x00010000, comment: "package is currently being compiled" }, + PKG_ContainsMap: { value: 0x00020000, comment: "Set if the package contains a ULevel/ UWorld object" }, + PKG_RequiresLocalizationGather: { + value: 0x00040000, + comment: "Set if the package contains any data to be gathered by localization", + }, + PKG_LoadUncooked: { + value: 0x00080000, + comment: + "This package must be loaded uncooked from IoStore/ZenStore (set only when cooking, to communicate to zenstore via the PackageStoreOptimizer)", + }, + PKG_PlayInEditor: { value: 0x00100000, comment: "Set if the package was created for the purpose of PIE" }, + PKG_ContainsScript: { value: 0x00200000, comment: "Package is allowed to contain UClass objects" }, + PKG_DisallowExport: { value: 0x00400000, comment: "Editor should not export asset in this package" }, + PKG_Unused_800000: { value: 0x00800000, comment: "Unused" }, + PKG_Unused_1000000: { value: 0x01000000, comment: "Unused" }, + PKG_Unused_2000000: { value: 0x02000000, comment: "Unused" }, + PKG_Unused_4000000: { value: 0x04000000, comment: "Unused" }, + PKG_CookGenerated: { + value: 0x08000000, + comment: "This package was generated by the cooker and does not exist in the WorkspaceDomain", + }, + PKG_DynamicImports: { + value: 0x10000000, + comment: "Obsolete flag, deprecated in UE 5.8.", + }, + PKG_RuntimeGenerated: { + value: 0x20000000, + comment: + "This package contains elements that are runtime generated, and may not follow standard loading order rules", + }, + PKG_ReloadingForCooker: { + value: 0x40000000, + comment: + "This package is reloading in the cooker, try to avoid getting data we will never need. We won't save this package.", + }, + PKG_FilterEditorOnly: { value: 0x80000000, comment: "Package has editor-only data filtered out" }, + PKG_TransientFlags: { + value: 0x00000001 | 0x00008000 | 0x40000000, + comment: "Transient Flags are cleared when serializing to or from PackageFileSummary", + }, + PKG_InMemoryOnly: { + value: 0x00000010 | 0x00000001, + comment: "Flag mask that indicates if this package is a package that exists in memory only.", + }, +} as const satisfies Record diff --git a/src/main/lib/uassets/parseUAsset.ts b/src/main/lib/uassets/parseUAsset.ts new file mode 100644 index 0000000..3a17cfa --- /dev/null +++ b/src/main/lib/uassets/parseUAsset.ts @@ -0,0 +1,30 @@ +import { UAssetBufferReader } from "@main/lib/uassets/bufferReader" +import { readUAssetNameTable } from "@main/lib/uassets/parser/nameTable" +import { readUAssetExportMap, readUAssetImportMap } from "@main/lib/uassets/parser/objectResource" +import { readUAssetPackageFileSummary } from "@main/lib/uassets/parser/packageFileSummary" +import type { UAssetPackage } from "@/main/types/uassets" + +/** + * Parse a raw `.uasset` file buffer into its decoded Layer 1 parts, the package summary, name + * table, import map, and export map, per-export tagged property streams are not decoded here + * since their location inside an export depends on the export's class (a Layer 2 concern). + * @param source Raw `.uasset` bytes as an `ArrayBuffer` or a view over one. + * @returns The fully decoded package. + * @throws When the file is not a valid `.uasset`, uses a summary format outside the reader's + * supported range, is stored with legacy compressed chunks, or was saved unversioned. + */ +export function parseUAsset(source: ArrayBuffer | ArrayBufferView): UAssetPackage { + const reader = new UAssetBufferReader(source) + + const summary = readUAssetPackageFileSummary(reader) + const names = readUAssetNameTable(reader, summary) + const imports = readUAssetImportMap(reader, summary, names) + const exports = readUAssetExportMap(reader, summary, names) + + return { + summary, + names, + imports, + exports, + } +} diff --git a/src/main/lib/uassets/parser/nameTable.ts b/src/main/lib/uassets/parser/nameTable.ts new file mode 100644 index 0000000..95a6bca --- /dev/null +++ b/src/main/lib/uassets/parser/nameTable.ts @@ -0,0 +1,27 @@ +import type { UAssetBufferReader } from "@main/lib/uassets/bufferReader" +import { UAssetObjectVersionUE4 as UE4 } from "@main/lib/uassets/enums/objectVersionUE4" +import { readNameEntry } from "@main/lib/uassets/parser/utils/names" +import type { UAssetPackageFileSummary } from "@/main/types/uassets" + +/** + * Read the entire name table into an array of strings, indexed by FName index, imports, + * exports, property tags and every other in-package identifier reference names by their + * zero-based index into this table so it must be loaded before any of them. + * @param reader Buffer reader, will be seeked to `summary.nameOffset`. + * @param summary Package summary providing `nameOffset`, `nameCount`, and the UE4 version + * used to gate the trailing hash bytes. + * @returns Array of length `summary.nameCount` where index `i` is the `i`-th FName string. + * @see /Engine/Source/Runtime/CoreUObject/Private/UObject/LinkerLoad.cpp `SerializeNameMap` + */ +export function readUAssetNameTable(reader: UAssetBufferReader, summary: UAssetPackageFileSummary): string[] { + reader.seek(summary.nameOffset) + + const hasHashes = summary.fileVersionUE4 >= UE4.VER_UE4_NAME_HASHES_SERIALIZED.value + const names: string[] = new Array(summary.nameCount) + + for (let i = 0; i < summary.nameCount; i++) { + names[i] = readNameEntry(reader, hasHashes) + } + + return names +} diff --git a/src/main/lib/uassets/parser/objectResource.ts b/src/main/lib/uassets/parser/objectResource.ts new file mode 100644 index 0000000..898ec50 --- /dev/null +++ b/src/main/lib/uassets/parser/objectResource.ts @@ -0,0 +1,204 @@ +import type { UAssetBufferReader } from "@main/lib/uassets/bufferReader" +import { UAssetObjectVersionUE4 as UE4 } from "@main/lib/uassets/enums/objectVersionUE4" +import { UAssetObjectVersionUE5 as UE5 } from "@main/lib/uassets/enums/objectVersionUE5" +import { UAssetPackageFlags } from "@main/lib/uassets/enums/packageFlags" +import { readFName } from "@main/lib/uassets/parser/utils/names" +import type { UAssetExport, UAssetImport, UAssetPackageFileSummary } from "@/main/types/uassets" + +/** + * Read a single `FObjectImport` from the reader's current position. + * @param reader Buffer reader positioned at the start of the entry. + * @param summary Package summary, supplies the file versions used to gate optional fields. + * @param names Name table used to resolve embedded FName references. + * @returns The decoded import entry. + */ +function readImport(reader: UAssetBufferReader, summary: UAssetPackageFileSummary, names: string[]): UAssetImport { + const classPackage = readFName(reader, names) + const className = readFName(reader, names) + const outerIndex = reader.int32() + const objectName = readFName(reader, names) + + let packageName = "" + if (summary.fileVersionUE4 >= UE4.VER_UE4_NON_OUTER_PACKAGE_IMPORT.value) { + packageName = readFName(reader, names) + } + + let bImportOptional = false + if (summary.fileVersionUE5 >= UE5.OPTIONAL_RESOURCES.value) { + bImportOptional = reader.bool32() + } + + return { + classPackage, + className, + outerIndex, + objectName, + packageName, + bImportOptional, + } +} + +/** + * Read every entry in the import map into an array of resolved imports, FName fields inside + * each import are pre-resolved against `names` so the returned strings are ready to feed + * into commit-message context without further lookup, `FPackageIndex` fields (only + * `outerIndex` on imports) stay as raw signed integers. + * @param reader Buffer reader, will be seeked to `summary.importOffset`. + * @param summary Package summary providing `importOffset`, `importCount`, and gating versions. + * @param names Fully resolved name table from `readUAssetNameTable`. + * @returns Array of length `summary.importCount`. + * @see /Engine/Source/Runtime/CoreUObject/Private/UObject/ObjectResource.cpp `operator<<(FStructuredArchive::FSlot, FObjectImport&)` + */ +export function readUAssetImportMap( + reader: UAssetBufferReader, + summary: UAssetPackageFileSummary, + names: string[], +): UAssetImport[] { + reader.seek(summary.importOffset) + + const imports: UAssetImport[] = new Array(summary.importCount) + for (let i = 0; i < summary.importCount; i++) { + imports[i] = readImport(reader, summary, names) + } + + return imports +} + +/** + * Read a single `FObjectExport` from the reader's current position. + * @param reader Buffer reader positioned at the start of the entry. + * @param summary Package summary, supplies file versions and the unversioned-properties flag. + * @param names Name table used to resolve embedded FName references. + * @returns The decoded export entry. + */ +function readExport(reader: UAssetBufferReader, summary: UAssetPackageFileSummary, names: string[]): UAssetExport { + const classIndex = reader.int32() + const superIndex = reader.int32() + + let templateIndex = 0 + if (summary.fileVersionUE4 >= UE4.VER_UE4_TEMPLATEINDEX_IN_COOKED_EXPORTS.value) { + templateIndex = reader.int32() + } + + const outerIndex = reader.int32() + const objectName = readFName(reader, names) + const objectFlags = reader.uint32() + + // "SerialSize" / "SerialOffset" widened from int32 to int64 in "VER_UE4_64BIT_EXPORTMAP_SERIALSIZES" (511) + let serialSize: bigint + let serialOffset: bigint + if (summary.fileVersionUE4 >= UE4.VER_UE4_64BIT_EXPORTMAP_SERIALSIZES.value) { + serialSize = reader.int64() + serialOffset = reader.int64() + } else { + serialSize = BigInt(reader.int32()) + serialOffset = BigInt(reader.int32()) + } + + const bForcedExport = reader.bool32() + const bNotForClient = reader.bool32() + const bNotForServer = reader.bool32() + + // Pre-REMOVE_OBJECT_EXPORT_PACKAGE_GUID (1005) files carry a 16-byte dummy FGuid here + if (summary.fileVersionUE5 < UE5.REMOVE_OBJECT_EXPORT_PACKAGE_GUID.value) { + reader.skip(16) + } + + let bIsInheritedInstance = false + if (summary.fileVersionUE5 >= UE5.TRACK_OBJECT_EXPORT_IS_INHERITED.value) { + bIsInheritedInstance = reader.bool32() + } + + const packageFlags = reader.uint32() + + let bNotAlwaysLoadedForEditorGame = false + if (summary.fileVersionUE4 >= UE4.VER_UE4_LOAD_FOR_EDITOR_GAME.value) { + bNotAlwaysLoadedForEditorGame = reader.bool32() + } + + let bIsAsset = false + if (summary.fileVersionUE4 >= UE4.VER_UE4_COOKED_ASSETS_IN_EDITOR_SUPPORT.value) { + bIsAsset = reader.bool32() + } + + let bGeneratePublicHash = false + if (summary.fileVersionUE5 >= UE5.OPTIONAL_RESOURCES.value) { + bGeneratePublicHash = reader.bool32() + } + + let firstExportDependency = -1 + let serializationBeforeSerializationDependencies = 0 + let createBeforeSerializationDependencies = 0 + let serializationBeforeCreateDependencies = 0 + let createBeforeCreateDependencies = 0 + if (summary.fileVersionUE4 >= UE4.VER_UE4_PRELOAD_DEPENDENCIES_IN_COOKED_EXPORTS.value) { + firstExportDependency = reader.int32() + serializationBeforeSerializationDependencies = reader.int32() + createBeforeSerializationDependencies = reader.int32() + serializationBeforeCreateDependencies = reader.int32() + createBeforeCreateDependencies = reader.int32() + } + + // "ScriptSerializationStartOffset" / "EndOffset" only present when the package uses versioned + // property serialization and is at least "SCRIPT_SERIALIZATION_OFFSET" (1010) + const usesUnversionedProps = (summary.packageFlags & UAssetPackageFlags.PKG_UnversionedProperties.value) !== 0 + let scriptSerializationStartOffset = 0n + let scriptSerializationEndOffset = 0n + if (!usesUnversionedProps && summary.fileVersionUE5 >= UE5.SCRIPT_SERIALIZATION_OFFSET.value) { + scriptSerializationStartOffset = reader.int64() + scriptSerializationEndOffset = reader.int64() + } + + return { + classIndex, + superIndex, + templateIndex, + outerIndex, + objectName, + objectFlags, + serialSize, + serialOffset, + bForcedExport, + bNotForClient, + bNotForServer, + bIsInheritedInstance, + packageFlags, + bNotAlwaysLoadedForEditorGame, + bIsAsset, + bGeneratePublicHash, + firstExportDependency, + serializationBeforeSerializationDependencies, + createBeforeSerializationDependencies, + serializationBeforeCreateDependencies, + createBeforeCreateDependencies, + scriptSerializationStartOffset, + scriptSerializationEndOffset, + } +} + +/** + * Read every entry in the export map into an array of resolved exports, `objectName` is + * pre-resolved, all `FPackageIndex` fields (`classIndex`, `superIndex`, `templateIndex`, + * `outerIndex`) stay as raw signed integers using UE's convention, `0` is null, positive `N` + * points at export `N - 1`, negative `-N` points at import `N - 1`. + * @param reader Buffer reader, will be seeked to `summary.exportOffset`. + * @param summary Package summary providing `exportOffset`, `exportCount`, and gating versions. + * @param names Fully resolved name table from `readUAssetNameTable`. + * @returns Array of length `summary.exportCount`. + * @see /Engine/Source/Runtime/CoreUObject/Private/UObject/ObjectResource.cpp `operator<<(FStructuredArchive::FSlot, FObjectExport&)` + */ +export function readUAssetExportMap( + reader: UAssetBufferReader, + summary: UAssetPackageFileSummary, + names: string[], +): UAssetExport[] { + reader.seek(summary.exportOffset) + + const exports: UAssetExport[] = new Array(summary.exportCount) + + for (let i = 0; i < summary.exportCount; i++) { + exports[i] = readExport(reader, summary, names) + } + + return exports +} diff --git a/src/main/lib/uassets/parser/packageFileSummary.ts b/src/main/lib/uassets/parser/packageFileSummary.ts new file mode 100644 index 0000000..598cff2 --- /dev/null +++ b/src/main/lib/uassets/parser/packageFileSummary.ts @@ -0,0 +1,316 @@ +import CONSTANTS from "@main/lib/constants" +import type { UAssetBufferReader } from "@main/lib/uassets/bufferReader" +import { UAssetObjectVersionUE4 as UE4 } from "@main/lib/uassets/enums/objectVersionUE4" +import { UAssetObjectVersionUE5 as UE5 } from "@main/lib/uassets/enums/objectVersionUE5" +import { UAssetPackageFileTag } from "@main/lib/uassets/enums/packageFileTag" +import { UAssetPackageFlags } from "@main/lib/uassets/enums/packageFlags" +import { readEngineVersion, readIoHashHex } from "@main/lib/uassets/parser/utils/structs" +import dedent from "dedent" +import type { + UAssetCustomVersion, + UAssetEngineVersion, + UAssetGenerationInfo, + UAssetPackageFileSummary, +} from "@/main/types/uassets" + +/** + * Zeroed `UAssetEngineVersion`, used as a default when the summary predates engine-version tracking. + * @returns An all-zero engine version with empty branch string. + */ +function emptyEngineVersion(): UAssetEngineVersion { + return { + major: 0, + minor: 0, + patch: 0, + changelist: 0, + branch: "", + } +} + +/** + * Read the `FPackageFileSummary` at the reader's current position. + * @param reader Buffer reader positioned at the start of the file. + * @returns The decoded summary. + * @throws When the tag is invalid, the summary format is too old/new to parse, the file is + * saved unversioned (not supported), or the package is stored with legacy compression. + */ +export function readUAssetPackageFileSummary(reader: UAssetBufferReader): UAssetPackageFileSummary { + const tag = reader.uint32() + + if (tag === UAssetPackageFileTag.PACKAGE_FILE_TAG_SWAPPED) { + reader.setLittleEndian(false) + } else if (tag !== UAssetPackageFileTag.PACKAGE_FILE_TAG) { + throw new Error(`Not a .uasset file: tag 0x${tag.toString(16)} does not match PACKAGE_FILE_TAG.`) + } + + const legacyFileVersion = reader.int32() + + if (legacyFileVersion >= 0) { + throw new Error(`Unsupported .uasset: legacy UE3-era file (LegacyFileVersion=${legacyFileVersion}).`) + } + + if (legacyFileVersion < CONSTANTS.uasset.currentLegacyFileVersion) { + throw new Error( + dedent`Unsupported .uasset: LegacyFileVersion=${legacyFileVersion} is newer than this + reader supports (max ${CONSTANTS.uasset.currentLegacyFileVersion}).`, + ) + } + + // LegacyFileVersion == -4 is the one revision where LegacyUE3Version was dropped from the wire + let legacyUE3Version = 0 + if (legacyFileVersion !== -4) { + legacyUE3Version = reader.int32() + } + + const fileVersionUE4 = reader.int32() + const fileVersionUE5 = legacyFileVersion <= -8 ? reader.int32() : 0 + const fileVersionLicenseeUE = reader.int32() + + if (fileVersionUE4 === 0 && fileVersionUE5 === 0 && fileVersionLicenseeUE === 0) { + throw new Error("Unsupported .uasset: unversioned packages are not supported by this reader.") + } + if (fileVersionUE4 < UE4.VER_UE4_OLDEST_LOADABLE_PACKAGE.value) { + throw new Error( + `Unsupported .uasset: FileVersionUE4=${fileVersionUE4} is older than the oldest loadable package.`, + ) + } + + // Post-PACKAGE_SAVED_HASH the SavedHash + TotalHeaderSize block moves up before custom versions + const hasEarlySavedHash = fileVersionUE5 >= UE5.PACKAGE_SAVED_HASH.value + let savedHash = "" + let totalHeaderSize = 0 + if (hasEarlySavedHash) { + savedHash = readIoHashHex(reader) + totalHeaderSize = reader.int32() + } + + // LegacyFileVersion <= -2 always writes a custom version container + // For our supported range (>= -9) the format is `Optimized`: int32 count followed by (FGuid Key, int32 Version) pairs + const customVersions: UAssetCustomVersion[] = [] + const customVersionCount = reader.int32() + for (let i = 0; i < customVersionCount; i++) { + const key = reader.fguid() + const version = reader.int32() + customVersions.push({ key, version }) + } + + if (!hasEarlySavedHash) { + totalHeaderSize = reader.int32() + } + + const packageName = reader.fstring() + const packageFlags = reader.uint32() + const filterEditorOnly = (packageFlags & UAssetPackageFlags.PKG_FilterEditorOnly.value) !== 0 + + const nameCount = reader.int32() + const nameOffset = reader.int32() + + let softObjectPathsCount = 0 + let softObjectPathsOffset = -1 + if (fileVersionUE5 >= UE5.ADD_SOFTOBJECTPATH_LIST.value) { + softObjectPathsCount = reader.int32() + softObjectPathsOffset = reader.int32() + } + + let localizationId = "" + if (!filterEditorOnly && fileVersionUE4 >= UE4.VER_UE4_ADDED_PACKAGE_SUMMARY_LOCALIZATION_ID.value) { + localizationId = reader.fstring() + } + + let gatherableTextDataCount = 0 + let gatherableTextDataOffset = -1 + if (fileVersionUE4 >= UE4.VER_UE4_SERIALIZE_TEXT_IN_PACKAGES.value) { + gatherableTextDataCount = reader.int32() + gatherableTextDataOffset = reader.int32() + } + + const exportCount = reader.int32() + const exportOffset = reader.int32() + const importCount = reader.int32() + const importOffset = reader.int32() + + let cellExportCount = 0 + let cellExportOffset = -1 + let cellImportCount = 0 + let cellImportOffset = -1 + if (fileVersionUE5 >= UE5.VERSE_CELLS.value) { + cellExportCount = reader.int32() + cellExportOffset = reader.int32() + cellImportCount = reader.int32() + cellImportOffset = reader.int32() + } + + let metaDataOffset = -1 + if (fileVersionUE5 >= UE5.METADATA_SERIALIZATION_OFFSET.value) { + metaDataOffset = reader.int32() + } + + const dependsOffset = reader.int32() + + let softPackageReferencesCount = 0 + let softPackageReferencesOffset = -1 + if (fileVersionUE4 >= UE4.VER_UE4_ADD_STRING_ASSET_REFERENCES_MAP.value) { + softPackageReferencesCount = reader.int32() + softPackageReferencesOffset = reader.int32() + } + + let searchableNamesOffset = -1 + if (fileVersionUE4 >= UE4.VER_UE4_ADDED_SEARCHABLE_NAMES.value) { + searchableNamesOffset = reader.int32() + } + + const thumbnailTableOffset = reader.int32() + + let importTypeHierarchiesCount = 0 + let importTypeHierarchiesOffset = -1 + if (fileVersionUE5 >= UE5.IMPORT_TYPE_HIERARCHIES.value) { + importTypeHierarchiesCount = reader.int32() + importTypeHierarchiesOffset = reader.int32() + } + + // Pre-PACKAGE_SAVED_HASH files stored a legacy FGuid here instead of the FIoHash above + if (!hasEarlySavedHash) { + savedHash = reader.fguid() + } + + let persistentGuid = "" + if (!filterEditorOnly && fileVersionUE4 >= UE4.VER_UE4_ADDED_PACKAGE_OWNER.value) { + persistentGuid = reader.fguid() + // OwnerPersistentGuid existed for one UE4 version between VER_UE4_ADDED_PACKAGE_OWNER (518) and VER_UE4_NON_OUTER_PACKAGE_IMPORT (520) + // Read and discard when present + if (fileVersionUE4 < UE4.VER_UE4_NON_OUTER_PACKAGE_IMPORT.value) { + reader.fguid() + } + } + + const generationCount = reader.int32() + const generations: UAssetGenerationInfo[] = [] + for (let i = 0; i < generationCount; i++) { + generations.push({ exportCount: reader.int32(), nameCount: reader.int32() }) + } + + let savedByEngineVersion: UAssetEngineVersion = emptyEngineVersion() + if (fileVersionUE4 >= UE4.VER_UE4_ENGINE_VERSION_OBJECT.value) { + savedByEngineVersion = readEngineVersion(reader) + } else { + const engineChangelist = reader.int32() + if (engineChangelist !== 0) { + savedByEngineVersion = { major: 4, minor: 0, patch: 0, changelist: engineChangelist, branch: "" } + } + } + + let compatibleWithEngineVersion: UAssetEngineVersion = savedByEngineVersion + if (fileVersionUE4 >= UE4.VER_UE4_PACKAGE_SUMMARY_HAS_COMPATIBLE_ENGINE_VERSION.value) { + compatibleWithEngineVersion = readEngineVersion(reader) + } + + const compressionFlags = reader.uint32() + const compressedChunkCount = reader.int32() + if (compressedChunkCount !== 0) { + throw new Error( + `Unsupported .uasset: package uses legacy compressed chunks (count=${compressedChunkCount}), only uncompressed source assets are readable.`, + ) + } + + const packageSource = reader.uint32() + + // AdditionalPackagesToCook is unused since ages, but still serialized as TArray + const additionalPackagesCount = reader.int32() + for (let i = 0; i < additionalPackagesCount; i++) { + reader.fstring() + } + + // Legacy texture allocation info was removed in LegacyFileVersion -7 + // Our supported range never hits the read path since CONSTANTS.uasset.currentLegacyFileVersion is -9 + + const assetRegistryDataOffset = reader.int32() + const bulkDataStartOffset = reader.int64() + + let worldTileInfoDataOffset = -1 + if (fileVersionUE4 >= UE4.VER_UE4_WORLD_LEVEL_INFO.value) { + worldTileInfoDataOffset = reader.int32() + } + + const chunkIds: number[] = [] + if (fileVersionUE4 >= UE4.VER_UE4_CHANGED_CHUNKID_TO_BE_AN_ARRAY_OF_CHUNKIDS.value) { + const chunkIdCount = reader.int32() + for (let i = 0; i < chunkIdCount; i++) chunkIds.push(reader.int32()) + } else if (fileVersionUE4 >= UE4.VER_UE4_ADDED_CHUNKID_TO_ASSETDATA_AND_UPACKAGE.value) { + const singleChunkId = reader.int32() + if (singleChunkId >= 0) chunkIds.push(singleChunkId) + } + + let preloadDependencyCount = -1 + let preloadDependencyOffset = 0 + if (fileVersionUE4 >= UE4.VER_UE4_PRELOAD_DEPENDENCIES_IN_COOKED_EXPORTS.value) { + preloadDependencyCount = reader.int32() + preloadDependencyOffset = reader.int32() + } + + let namesReferencedFromExportDataCount = nameCount + if (fileVersionUE5 >= UE5.NAMES_REFERENCED_FROM_EXPORT_DATA.value) { + namesReferencedFromExportDataCount = reader.int32() + } + + let payloadTocOffset = -1n + if (fileVersionUE5 >= UE5.PAYLOAD_TOC.value) { + payloadTocOffset = reader.int64() + } + + let dataResourceOffset = -1 + if (fileVersionUE5 >= UE5.DATA_RESOURCES.value) { + dataResourceOffset = reader.int32() + } + + return { + tag, + legacyFileVersion, + legacyUE3Version, + fileVersionUE4, + fileVersionUE5, + fileVersionLicenseeUE, + customVersions, + savedHash, + totalHeaderSize, + packageName, + packageFlags, + nameCount, + nameOffset, + softObjectPathsCount, + softObjectPathsOffset, + localizationId, + gatherableTextDataCount, + gatherableTextDataOffset, + exportCount, + exportOffset, + importCount, + importOffset, + cellExportCount, + cellExportOffset, + cellImportCount, + cellImportOffset, + metaDataOffset, + dependsOffset, + softPackageReferencesCount, + softPackageReferencesOffset, + searchableNamesOffset, + thumbnailTableOffset, + importTypeHierarchiesCount, + importTypeHierarchiesOffset, + persistentGuid, + generations, + savedByEngineVersion, + compatibleWithEngineVersion, + compressionFlags, + packageSource, + assetRegistryDataOffset, + bulkDataStartOffset, + worldTileInfoDataOffset, + chunkIds, + preloadDependencyCount, + preloadDependencyOffset, + namesReferencedFromExportDataCount, + payloadTocOffset, + dataResourceOffset, + } +} diff --git a/src/main/lib/uassets/parser/propertyTag.ts b/src/main/lib/uassets/parser/propertyTag.ts new file mode 100644 index 0000000..70f5161 --- /dev/null +++ b/src/main/lib/uassets/parser/propertyTag.ts @@ -0,0 +1,163 @@ +import CONSTANTS from "@main/lib/constants" +import type { UAssetBufferReader } from "@main/lib/uassets/bufferReader" +import { UAssetObjectVersionUE5 as UE5 } from "@main/lib/uassets/enums/objectVersionUE5" +import { UAssetPackageFlags } from "@main/lib/uassets/enums/packageFlags" +import { readFName } from "@main/lib/uassets/parser/utils/names" +import dedent from "dedent" +import type { UAssetPackageFileSummary, UAssetPropertyTag } from "@/main/types/uassets" + +/** + * Advance past the optional property-extensions block written when `HasPropertyExtensions` is + * set, the two currently defined extension groups (`OverridableInformation`, + * `HasExternalsObjects`) are consumed but their values are discarded. + * @param reader Buffer reader positioned at the start of the extensions block. + */ +function skipPropertyExtensions(reader: UAssetBufferReader): void { + const extensions = reader.uint8() + + if ((extensions & CONSTANTS.uasset.propertyTagExtension.overridableInformation) !== 0) { + reader.uint8() // "OverriddenPropertyOperation" + reader.bool32() // "ExperimentalOverridableLogic" + } + + if ((extensions & CONSTANTS.uasset.propertyTagExtension.hasExternalsObjects) !== 0) { + reader.bool32() // "ExperimentalExternalObjects" + } +} + +/** + * Recursively render a `FPropertyTypeName` subtree into `Name(child1,child2,...)` form. + * @param nodes Full node array from `readPropertyTypeName`. + * @param index Starting index of the subtree to render. + * @returns The subtree's rendered text and the index of the next sibling node. + */ +function renderTypeName( + nodes: { + name: string + innerCount: number + }[], + index: number, +): { text: string; next: number } { + const node = nodes[index] + if (node.innerCount === 0) { + return { + text: node.name, + next: index + 1, + } + } + + const parts: string[] = [] + let next = index + 1 + for (let i = 0; i < node.innerCount; i++) { + const child = renderTypeName(nodes, next) + parts.push(child.text) + next = child.next + } + + return { + text: `${node.name}(${parts.join(",")})`, + next, + } +} + +/** + * Read an `FPropertyTypeName` (a pre-order-encoded tree of `{FName, int32 innerCount}` nodes) + * and render it as a human-readable dotted string, e.g. `"StructProperty(Vector)"`. + * @param reader Buffer reader positioned at the start of the type name tree. + * @param names Name table used to resolve node FName references. + * @returns The rendered type name string. + */ +function readPropertyTypeName(reader: UAssetBufferReader, names: string[]): string { + const nodes: { name: string; innerCount: number }[] = [] + + let remaining = 1 + do { + const name = readFName(reader, names) + const innerCount = reader.int32() + nodes.push({ name, innerCount }) + remaining += innerCount - 1 + } while (remaining > 0) + + return renderTypeName(nodes, 0).text +} + +/** + * Read a single tag header from the reader's current position, or return `null` when the + * stream terminator (a None-named tag) is encountered. + * @param reader Buffer reader positioned at the start of the tag. + * @param names Name table used to resolve embedded FName references. + * @returns The decoded tag header, or `null` at end-of-stream. + */ +function readTagHeader(reader: UAssetBufferReader, names: string[]): UAssetPropertyTag | null { + const name = readFName(reader, names) + if (name === "None") return null + + const typeName = readPropertyTypeName(reader, names) + const size = reader.int32() + const flags = reader.uint8() + + const arrayIndex = (flags & CONSTANTS.uasset.propertyTagFlag.hasArrayIndex) === 0 ? 0 : reader.int32() + const propertyGuid = (flags & CONSTANTS.uasset.propertyTagFlag.hasPropertyGuid) === 0 ? "" : reader.fguid() + + if ((flags & CONSTANTS.uasset.propertyTagFlag.hasPropertyExtensions) !== 0) { + skipPropertyExtensions(reader) + } + + const valueOffset = BigInt(reader.tell()) + + return { + name, + typeName, + size, + arrayIndex, + boolValue: (flags & CONSTANTS.uasset.propertyTagFlag.boolTrue) !== 0, + skipped: (flags & CONSTANTS.uasset.propertyTagFlag.skippedSerialize) !== 0, + binaryOrNativeSerialize: (flags & CONSTANTS.uasset.propertyTagFlag.hasBinaryOrNativeSerialize) !== 0, + propertyGuid, + valueOffset, + } +} + +/** + * Read the property tag stream starting at `startOffset` and stop when the None terminator + * is reached, value payloads are skipped over (not decoded), each returned tag carries the + * absolute `valueOffset` and byte `size` so callers can locate the raw payload later, only + * the modern tag format (post-`PROPERTY_TAG_COMPLETE_TYPE_NAME`, UE 5.4+) is supported. + * @param reader Buffer reader, will be seeked to `startOffset`. + * @param summary Package summary, provides version gates and the unversioned-properties flag. + * @param names Fully resolved name table from `readUAssetNameTable`. + * @param startOffset Byte offset of the tag stream, typically `export.serialOffset`. + * @returns Array of decoded tag headers in stream order (terminator excluded). + * @throws When the package uses unversioned property serialization (no tags emitted) or when + * the file predates `PROPERTY_TAG_COMPLETE_TYPE_NAME` (legacy format not supported). + * @see /Engine/Source/Runtime/CoreUObject/Private/UObject/PropertyTag.cpp `operator<<(FStructuredArchive::FSlot, FPropertyTag&)` + */ +export function readUAssetPropertyTags( + reader: UAssetBufferReader, + summary: UAssetPackageFileSummary, + names: string[], + startOffset: number, +): UAssetPropertyTag[] { + if ((summary.packageFlags & UAssetPackageFlags.PKG_UnversionedProperties.value) !== 0) { + throw new Error("Cannot read tagged properties: package uses unversioned property serialization.") + } + if (summary.fileVersionUE5 < UE5.PROPERTY_TAG_COMPLETE_TYPE_NAME.value) { + throw new Error( + dedent`Legacy property tag format (pre-PROPERTY_TAG_COMPLETE_TYPE_NAME, + UE5 version ${summary.fileVersionUE5}) is not supported by this reader.`, + ) + } + + reader.seek(startOffset) + + const tags: UAssetPropertyTag[] = [] + while (true) { + const tag = readTagHeader(reader, names) + if (tag === null) break + + tags.push(tag) + reader.skip(tag.size) + } + + return tags +} diff --git a/src/main/lib/uassets/parser/utils/names.ts b/src/main/lib/uassets/parser/utils/names.ts new file mode 100644 index 0000000..4c62d2b --- /dev/null +++ b/src/main/lib/uassets/parser/utils/names.ts @@ -0,0 +1,44 @@ +import type { UAssetBufferReader } from "@main/lib/uassets/bufferReader" + +/** + * Read a single `FNameEntrySerialized` from the reader's current position, the wire format + * is an `FString` (length prefix + string bytes + null terminator) followed by two `uint16` + * hashes for post-`VER_UE4_NAME_HASHES_SERIALIZED` files, UE writes both hashes out of habit + * but ignores them on load so we skip past them without decoding. + * @param reader Buffer reader positioned at the start of the entry. + * @param hasHashes True when the entry ends with `NonCasePreservingHash` + `CasePreservingHash`. + * @returns The decoded name string. + * @see /Engine/Source/Runtime/Core/Private/UObject/UnrealNames.cpp `operator<<(FArchive&, FNameEntrySerialized&)` + */ +export function readNameEntry(reader: UAssetBufferReader, hasHashes: boolean): string { + const name = reader.fstring() + + if (hasHashes) { + // NonCasePreservingHash + CasePreservingHash, both uint16, unused at load time + reader.uint16() + reader.uint16() + } + + return name +} + +/** + * Read an on-disk `FName` reference (int32 name index + int32 numeric suffix) and resolve it + * against the given name table, UE stores the suffix as `ExternalNumber + 1` so `0` means + * "no suffix", `1` means `_0`, `2` means `_1`, and so on. + * @param reader Buffer reader positioned at the start of the FName pair. + * @param names Name table produced by `readUAssetNameTable`. + * @returns The base name string with `_N` suffix appended when the on-disk number is positive. + * @throws When the name index is out of range for `names`, which indicates a corrupt file. + */ +export function readFName(reader: UAssetBufferReader, names: string[]): string { + const nameIndex = reader.int32() + const number = reader.int32() + + if (nameIndex < 0 || nameIndex >= names.length) { + throw new Error(`Invalid FName index ${nameIndex} (name table has ${names.length} entries).`) + } + + const base = names[nameIndex] + return number > 0 ? `${base}_${number - 1}` : base +} diff --git a/src/main/lib/uassets/parser/utils/structs.ts b/src/main/lib/uassets/parser/utils/structs.ts new file mode 100644 index 0000000..d3140e8 --- /dev/null +++ b/src/main/lib/uassets/parser/utils/structs.ts @@ -0,0 +1,31 @@ +import type { UAssetBufferReader } from "@main/lib/uassets/bufferReader" +import type { UAssetEngineVersion } from "@/main/types/uassets" + +/** + * Read a 20-byte `FIoHash` and return it as an upper-case hex string. + * @param reader Buffer reader positioned at the start of the hash. + * @returns The 40-character upper-case hex hash. + * @see /Engine/Source/Runtime/Core/Public/IO/IoHash.h + */ +export function readIoHashHex(reader: UAssetBufferReader): string { + const bytes = reader.bytes(20) + let hex = "" + for (const byte of bytes) hex += byte.toString(16).padStart(2, "0") + return hex.toUpperCase() +} + +/** + * Read an `FEngineVersion` in the on-disk order (Major, Minor, Patch, Changelist, Branch). + * @param reader Buffer reader positioned at the start of the version block. + * @returns The decoded engine version. + * @see /Engine/Source/Runtime/Core/Private/Misc/EngineVersion.cpp `operator<<(FStructuredArchive::FSlot, FEngineVersion&)` + */ +export function readEngineVersion(reader: UAssetBufferReader): UAssetEngineVersion { + return { + major: reader.uint16(), + minor: reader.uint16(), + patch: reader.uint16(), + changelist: reader.uint32(), + branch: reader.fstring(), + } +} diff --git a/src/main/lib/uassets/resolveMainAsset.ts b/src/main/lib/uassets/resolveMainAsset.ts new file mode 100644 index 0000000..9ce0488 --- /dev/null +++ b/src/main/lib/uassets/resolveMainAsset.ts @@ -0,0 +1,119 @@ +import type { UAssetMainAsset, UAssetPackage } from "@/main/types/uassets" + +/** + * Walk an import's outer chain up to the top-level entry (where `outerIndex === 0`) and + * return its `objectName`, which for a class import is the module path like `"/Script/Engine"`, + * `"None"` when the chain terminates without a proper package (rare, indicates a corrupt file). + * @param pkg Parsed package. + * @param startOuterIndex The `outerIndex` of the import whose enclosing package we want. + * @returns The `objectName` of the top-of-chain import. + */ +function resolveOuterPackage(pkg: UAssetPackage, startOuterIndex: number): string { + let outerIndex = startOuterIndex + let last = "" + + while (outerIndex < 0) { + const importIndex = -outerIndex - 1 + if (importIndex >= pkg.imports.length) return last + const parent = pkg.imports[importIndex] + last = parent.objectName + outerIndex = parent.outerIndex + } + + return last +} + +/** + * Resolve an `FPackageIndex` (the export's `classIndex`) to its `className` + `classPackage`, + * negative values point into the import map, positive values into the export map, zero is + * the null class (never valid for a main asset). + * @param pkg Parsed package. + * @param classIndex Raw `FPackageIndex` from the main export. + * @returns The class's name and outer package path. + * @throws When the index is `0` or out of range for the map it points into. + */ +function resolveClass(pkg: UAssetPackage, classIndex: number): { className: string; classPackage: string } { + if (classIndex < 0) { + const importIndex = -classIndex - 1 + + if (importIndex >= pkg.imports.length) { + throw new Error(`Main export classIndex=${classIndex} is out of range for the import map.`) + } + + const classImport = pkg.imports[importIndex] + return { + className: classImport.objectName, + classPackage: resolveOuterPackage(pkg, classImport.outerIndex), + } + } + + if (classIndex > 0) { + const exportIndex = classIndex - 1 + + if (exportIndex >= pkg.exports.length) { + throw new Error(`Main export classIndex=${classIndex} is out of range for the export map.`) + } + + return { + className: pkg.exports[exportIndex].objectName, + classPackage: "", + } + } + + throw new Error("Main export has a null classIndex (0), which is not a valid class reference.") +} + +/** + * Extract the short asset name from a full package path (`"/Game/Foo/Bar/BP_Enemy"` into `"BP_Enemy"`). + * @param packagePath Full UE package path from the summary. + * @returns The last `/`-separated segment, or the original string when it has no slashes. + */ +function shortNameOf(packagePath: string): string { + const slash = packagePath.lastIndexOf("/") + return slash < 0 ? packagePath : packagePath.slice(slash + 1) +} + +/** + * Locate the main export's index inside `pkg.exports`, matching by name first and falling + * back to the first top-level export. + * @param pkg Parsed package. + * @param shortName Expected `objectName` of the main asset. + * @returns Zero-based index into `pkg.exports`. + */ +function findMainExportIndex(pkg: UAssetPackage, shortName: string): number { + const byName = pkg.exports.findIndex(entry => entry.objectName === shortName) + if (byName !== -1) return byName + + const byOuter = pkg.exports.findIndex(entry => entry.outerIndex === 0) + if (byOuter !== -1) return byOuter + + return 0 +} + +/** + * Identify the "main" asset export inside a parsed package and resolve its class, the + * returned `className` is what Layer 2 shapers dispatch on (`"Blueprint"`, `"Material"`, + * `"Texture2D"`, ...), detection prefers the export whose `objectName` matches the last + * segment of `summary.packageName` and falls back to the first top-level export + * (`outerIndex === 0`) when no name match exists. + * @param pkg Package returned by `parseUAsset`. + * @returns The main asset descriptor with its class resolved. + * @throws When the package has no exports or the main export's `classIndex` is out of range. + */ +export function resolveMainAsset(pkg: UAssetPackage): UAssetMainAsset { + if (pkg.exports.length === 0) { + throw new Error("Cannot resolve main asset: package has no exports.") + } + + const shortName = shortNameOf(pkg.summary.packageName) + const index = findMainExportIndex(pkg, shortName) + const mainExport = pkg.exports[index] + const classInfo = resolveClass(pkg, mainExport.classIndex) + + return { + index, + export: mainExport, + className: classInfo.className, + classPackage: classInfo.classPackage, + } +} diff --git a/src/main/lib/uassets/shapers/blueprint.ts b/src/main/lib/uassets/shapers/blueprint.ts new file mode 100644 index 0000000..3a53e4a --- /dev/null +++ b/src/main/lib/uassets/shapers/blueprint.ts @@ -0,0 +1,171 @@ +import CONSTANTS from "@main/lib/constants" +import type { + UAssetBlueprintComponent, + UAssetBlueprintShape, + UAssetExport, + UAssetMainAsset, + UAssetPackage, +} from "@/main/types/uassets" + +/** + * Resolve an `FPackageIndex` to just the class/object name string, negative values point into + * the import map, positive values into the export map, `0` returns the empty string. + * @param pkg Parsed package. + * @param packageIndex Raw `FPackageIndex` (signed int32 from the disk format). + * @returns The referenced object's `objectName`, or the empty string when the index is null or out of range. + */ +function resolveClassName(pkg: UAssetPackage, packageIndex: number): string { + if (packageIndex < 0) { + const importIndex = -packageIndex - 1 + if (importIndex >= pkg.imports.length) return "" + return pkg.imports[importIndex].objectName + } + + if (packageIndex > 0) { + const exportIndex = packageIndex - 1 + if (exportIndex >= pkg.exports.length) return "" + return pkg.exports[exportIndex].objectName + } + + return "" +} + +/** + * Walk an import's outer chain up to the top-level entry and return that entry's `objectName`, + * which for a class import is the module path like `"/Script/Engine"` or `"/Game/Foo/Bar"`. + * @param pkg Parsed package. + * @param startOuterIndex The `outerIndex` of the import whose enclosing package we want. + * @returns The top-of-chain `objectName`, or the empty string when the chain is empty. + */ +function walkOuterToPackage(pkg: UAssetPackage, startOuterIndex: number): string { + let outerIndex = startOuterIndex + let last = "" + + while (outerIndex < 0) { + const importIndex = -outerIndex - 1 + if (importIndex >= pkg.imports.length) return last + const parent = pkg.imports[importIndex] + last = parent.objectName + outerIndex = parent.outerIndex + } + + return last +} + +/** + * Locate the `UBlueprintGeneratedClass` export associated with the main Blueprint, matched by + * the `_C`-suffixed name convention. + * @param pkg Parsed package. + * @param main Descriptor returned by `resolveMainAsset`. + * @returns The generated-class export, or `null` when no matching export exists. + */ +function findGeneratedClassExport(pkg: UAssetPackage, main: UAssetMainAsset): UAssetExport | null { + const expectedName = `${main.export.objectName}${CONSTANTS.uasset.blueprint.compiledClassSuffix}` + return pkg.exports.find(entry => entry.objectName === expectedName) ?? null +} + +/** + * Resolve the Blueprint's parent class by looking at the `UBlueprintGeneratedClass` export's + * `superIndex` and turning it into a class name. + * @param pkg Parsed package. + * @param main Descriptor returned by `resolveMainAsset`. + * @returns The parent class name, or the empty string when the generated class or its super can't be resolved. + */ +function resolveParentClass(pkg: UAssetPackage, main: UAssetMainAsset): string { + const generatedClass = findGeneratedClassExport(pkg, main) + if (!generatedClass) return "" + return resolveClassName(pkg, generatedClass.superIndex) +} + +/** + * Extract every SCS component template (exports whose name ends in `_GEN_VARIABLE`), the + * variable name is everything before the suffix and the class name comes from resolving + * the export's `classIndex`. + * @param pkg Parsed package. + * @returns Component templates in export-order. + */ +function extractComponents(pkg: UAssetPackage): UAssetBlueprintComponent[] { + const result: UAssetBlueprintComponent[] = [] + + for (const entry of pkg.exports) { + if (!entry.objectName.endsWith(CONSTANTS.uasset.blueprint.componentSuffix)) continue + + result.push({ + name: entry.objectName.slice(0, -CONSTANTS.uasset.blueprint.componentSuffix.length), + className: resolveClassName(pkg, entry.classIndex), + }) + } + + return result +} + +/** + * Extract every user-defined function graph (exports whose class resolves to `"Function"`). + * @param pkg Parsed package. + * @returns Function names in export-order. + */ +function extractFunctions(pkg: UAssetPackage): string[] { + const result: string[] = [] + + for (const entry of pkg.exports) { + if (resolveClassName(pkg, entry.classIndex) !== "Function") continue + result.push(entry.objectName) + } + + return result +} + +/** + * Collect the distinct `/Game/`-rooted package paths referenced by this Blueprint's imports, + * deduplicated and sorted alphabetically. + * @param pkg Parsed package. + * @returns Sorted unique referenced asset package paths. + */ +function extractReferencedAssets(pkg: UAssetPackage): string[] { + const seen = new Set() + + for (const imp of pkg.imports) { + const outerPackage = walkOuterToPackage(pkg, imp.outerIndex) + if (outerPackage.startsWith(CONSTANTS.uasset.gamePackagePrefix)) seen.add(outerPackage) + } + + return Array.from(seen).sort() +} + +/** + * Format the shaper output into a compact single-line summary suitable for an AI prompt. + * @param shape Everything the shaper collected, minus `renderedSummary` and `kind`. + * @returns One line like `"Blueprint(BP_Enemy, parent=Actor, components=2, functions=1, refs=3)"`. + */ +function renderSummary(shape: Omit): string { + const parent = shape.parentClass || "?" + return `Blueprint(${shape.name}, parent=${parent}, components=${shape.components.length}, functions=${shape.functions.length}, refs=${shape.referencedAssets.length})` +} + +/** + * Shape a Blueprint main asset into the Layer 2 view a commit-message tool feeds to the AI, + * pulls parent class, SCS components, user functions, and referenced `/Game/` assets out of + * the parsed package. + * @param pkg Parsed package returned by `parseUAsset`. + * @param main Main-asset descriptor returned by `resolveMainAsset` (expected `className === "Blueprint"`). + * @returns The Blueprint shape with a pre-rendered one-line summary. + */ +export function shapeBlueprint(pkg: UAssetPackage, main: UAssetMainAsset): UAssetBlueprintShape { + const name = main.export.objectName + const parentClass = resolveParentClass(pkg, main) + const components = extractComponents(pkg) + const functions = extractFunctions(pkg) + const referencedAssets = extractReferencedAssets(pkg) + + const renderedSummary = renderSummary({ name, parentClass, components, functions, referencedAssets }) + + return { + kind: "blueprint", + name, + parentClass, + components, + functions, + referencedAssets, + renderedSummary, + } +} diff --git a/src/main/lib/utils/fs.ts b/src/main/lib/utils/fs.ts new file mode 100644 index 0000000..55a2b46 --- /dev/null +++ b/src/main/lib/utils/fs.ts @@ -0,0 +1,15 @@ +import { access } from "node:fs/promises" + +/** + * Checks whether a path currently exists on disk. + * @param target The absolute path to check. + * @returns True if the path exists, `false` otherwise. + */ +export async function pathExists(target: string): Promise { + try { + await access(target) + return true + } catch { + return false + } +} diff --git a/src/main/lib/window/index.ts b/src/main/lib/windows/index.ts similarity index 85% rename from src/main/lib/window/index.ts rename to src/main/lib/windows/index.ts index 78aff93..8184651 100644 --- a/src/main/lib/window/index.ts +++ b/src/main/lib/windows/index.ts @@ -28,7 +28,7 @@ export function attachWindowStateBroadcaster(window: BrowserWindow) { */ const broadcast = () => { if (window.isDestroyed()) return - window.webContents.send(CONSTANTS.ipc.windowStateChanged, getWindowState(window)) + window.webContents.send(CONSTANTS.ipc.windowsStateChanged, getWindowState(window)) } // Attach the broadcast function to all relevant window events @@ -49,18 +49,18 @@ export function attachWindowStateBroadcaster(window: BrowserWindow) { * to control its owning `BrowserWindow` (minimize, maximize/restore, close), * and to query its current state. */ -export function registerWindowControlHandlers() { - ipcMain.handle(CONSTANTS.ipc.windowGetState, event => { +export function registerWindowsControlHandlers() { + ipcMain.handle(CONSTANTS.ipc.windowsGetState, event => { const window = BrowserWindow.fromWebContents(event.sender) if (!window) return null return getWindowState(window) }) - ipcMain.on(CONSTANTS.ipc.windowMinimize, event => { + ipcMain.on(CONSTANTS.ipc.windowsMinimize, event => { BrowserWindow.fromWebContents(event.sender)?.minimize() }) - ipcMain.on(CONSTANTS.ipc.windowMaximizeToggle, event => { + ipcMain.on(CONSTANTS.ipc.windowsMaximizeToggle, event => { const window = BrowserWindow.fromWebContents(event.sender) if (!window) return @@ -68,7 +68,7 @@ export function registerWindowControlHandlers() { else window.maximize() }) - ipcMain.on(CONSTANTS.ipc.windowClose, event => { + ipcMain.on(CONSTANTS.ipc.windowsClose, event => { BrowserWindow.fromWebContents(event.sender)?.close() }) } diff --git a/src/main/types/dialog.d.ts b/src/main/types/dialogs.d.ts similarity index 100% rename from src/main/types/dialog.d.ts rename to src/main/types/dialogs.d.ts diff --git a/src/main/types/tree.d.ts b/src/main/types/fileTree.d.ts similarity index 100% rename from src/main/types/tree.d.ts rename to src/main/types/fileTree.d.ts diff --git a/src/main/types/git.d.ts b/src/main/types/gitCommands.d.ts similarity index 100% rename from src/main/types/git.d.ts rename to src/main/types/gitCommands.d.ts diff --git a/src/main/types/ipc.d.ts b/src/main/types/ipc.d.ts new file mode 100644 index 0000000..647c5f4 --- /dev/null +++ b/src/main/types/ipc.d.ts @@ -0,0 +1,5 @@ +/** + * The envelope every fallible IPC handler wraps its return value in, so the + * handler never rejects and the renderer side can decide how to react. + */ +export type IpcResult = { ok: true; value: T } | { ok: false; error: string } diff --git a/src/main/types/lfs.d.ts b/src/main/types/lfs.d.ts deleted file mode 100644 index c284831..0000000 --- a/src/main/types/lfs.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * A single Git LFS lock. - */ -export type LfsLock = { - id: string - path: string - owner: string - lockedAt: string - isMine: boolean -} - -/** - * The outcome of a lock or unlock operation on a single file. - */ -export type LfsLockResult = { - path: string - ok: boolean - error?: string -} diff --git a/src/main/types/lfsCommands.d.ts b/src/main/types/lfsCommands.d.ts new file mode 100644 index 0000000..2df9fb2 --- /dev/null +++ b/src/main/types/lfsCommands.d.ts @@ -0,0 +1,61 @@ +/** + * A single Git LFS lock. + */ +export type LfsLock = { + id: string + path: string + owner: string + lockedAt: string + isMine: boolean +} + +/** + * The outcome of a lock or unlock operation on a single file. + */ +export type LfsLockResult = { + path: string + ok: boolean + error?: string +} + +/** + * The payload streamed over the LFS lock progress channel, letting the renderer + * track a live `done/total` count while a lock or unlock batch is in flight. + * The `requestId` scopes each event to the originating invocation so multiple + * concurrent batches do not cross-contaminate. + */ +export type LfsLockProgress = { + requestId: string + done: number + total: number +} + +/** + * The outcome of a lock migration for a single staged rename, describing how + * the old path's lock was carried over to the new path. + */ +export type LfsLockMigration = { + from: string + to: string + /** + * The migration status, one of: + * - `migrated`: The old lock belonged to the current user, the new path was + * locked and the old lock released + * - `skipped-not-locked`: The old path had no active lock, nothing to do + * - `skipped-not-mine`: The old path is locked by another user and cannot + * be transferred without a force-unlock + * - `skipped-not-lockable`: The new path is not covered by the `lockable` + * attribute, so it cannot receive the lock + * - `failed-lock`: Locking the new path failed, the old lock was preserved + * - `failed-unlock`: The new path was locked but the old lock could not be + * released, both locks currently exist + */ + status: + | "migrated" + | "skipped-not-locked" + | "skipped-not-mine" + | "skipped-not-lockable" + | "failed-lock" + | "failed-unlock" + error?: string +} diff --git a/src/main/types/project.d.ts b/src/main/types/projects.d.ts similarity index 82% rename from src/main/types/project.d.ts rename to src/main/types/projects.d.ts index 9adc6bf..6480ac7 100644 --- a/src/main/types/project.d.ts +++ b/src/main/types/projects.d.ts @@ -1,4 +1,11 @@ -import type { Project } from "@/main/types/store" +/** + * A project definition. + */ +export type Project = { + path: string + name: string + lastOpenedAt: string +} /** * Why an attempt to open a project did not result in an open repository: diff --git a/src/main/types/store.d.ts b/src/main/types/store.d.ts index b300c94..6932304 100644 --- a/src/main/types/store.d.ts +++ b/src/main/types/store.d.ts @@ -1,19 +1,11 @@ -import type { LfsLock } from "@/main/types/lfs" +import type { LfsLock } from "@/main/types/lfsCommands" +import type { Project } from "@/main/types/projects" /** * What GitGame should do with the previously opened project when it launches. */ export type StartupBehavior = "reopen-last" | "start-clean" -/** - * A project definition. - */ -export type Project = { - path: string - name: string - lastOpenedAt: string -} - /** * The user-facing application preferences. */ diff --git a/src/main/types/uassets.d.ts b/src/main/types/uassets.d.ts new file mode 100644 index 0000000..e0fd1ff --- /dev/null +++ b/src/main/types/uassets.d.ts @@ -0,0 +1,658 @@ +/** + * Numeric value + engine's inline doc comment for a single enum entry. + * */ +export type UAssetEnumEntry = { + value: number + comment: string +} + +/** + * One entry from the summary's custom version container: subsystem GUID + version index. + */ +export interface UAssetCustomVersion { + /** + * 32-char upper-hex GUID identifying the subsystem (Blueprints, Framework, Niagara, ...). + */ + key: string + + /** + * Subsystem-specific version index the package was saved with. + */ + version: number +} + +/** + * Decoded `FEngineVersion` (semantic version + branch string). + */ +export interface UAssetEngineVersion { + /** + * Major version number, e.g. `5` for UE 5.x. + */ + major: number + + /** + * Minor version number, e.g. `8` for UE 5.8. + */ + minor: number + + /** + * Patch/hotfix version number. + */ + patch: number + + /** + * Perforce/git changelist number the engine was built at. + */ + changelist: number + + /** + * Release branch name, e.g. `++UE5+Release-5.8`. + */ + branch: string +} + +/** + * One entry in the summary's generation list: counts snapshot at a prior save. + */ +export interface UAssetGenerationInfo { + /** + * Number of exports the package had at this generation. + */ + exportCount: number + + /** + * Number of names the package had at this generation. + */ + nameCount: number +} + +/** + * Decoded `FPackageFileSummary`, fields not useful to Layer 1 extraction are still advanced past + * (so the reader lands at the correct offset) but exposed as-is here for downstream inspection, + * offsets serialized as `int64` are surfaced as `bigint`, the rest are `int32`, sections that + * don't exist in the file typically use `count = 0` and `offset = -1`. + * @see /Engine/Source/Runtime/CoreUObject/Public/UObject/PackageFileSummary.h + */ +export interface UAssetPackageFileSummary { + /** + * Magic number identifying the file as a UE package, matches `PACKAGE_FILE_TAG` after + * any endian swap has been applied by the reader. + */ + tag: number + + /** + * Summary format revision, always negative for modern packages, `-9` at the time of writing. + */ + legacyFileVersion: number + + /** + * Vestigial UE3 engine version field, kept in the wire format for backward compat, unused today. + */ + legacyUE3Version: number + + /** + * UE4 object format version the package was saved with, compare against `UAssetObjectVersionUE4`. + */ + fileVersionUE4: number + + /** + * UE5 object format version (0 for packages saved before UE5), compare against `UAssetObjectVersionUE5`. + */ + fileVersionUE5: number + + /** + * Custom licensee-specific format version, typically 0 for stock Epic builds. + */ + fileVersionLicenseeUE: number + + /** + * Per-subsystem version table (Blueprint, Niagara, Framework, ...) the package was saved against. + */ + customVersions: UAssetCustomVersion[] + + /** + * Hex-encoded `FIoHash` (post-`PACKAGE_SAVED_HASH`) or legacy `FGuid` identifying the saved bytes. + */ + savedHash: string + + /** + * Total on-disk size of the summary plus name table plus import & export maps combined. + */ + totalHeaderSize: number + + /** + * Original package path at last save, e.g. `/Game/Characters/BP_Enemy`. + */ + packageName: string + + /** + * `EPackageFlags` bitmask, test against entries in `UAssetPackageFlags`. + */ + packageFlags: number + + /** + * Number of entries in the name table. + */ + nameCount: number + + /** + * Byte offset from the start of the file to the name table. + */ + nameOffset: number + + /** + * Number of soft object path entries (present when `ADD_SOFTOBJECTPATH_LIST` or newer). + */ + softObjectPathsCount: number + + /** + * Byte offset to the soft object path list, `-1` when the section is absent. + */ + softObjectPathsOffset: number + + /** + * Editor-only localization identifier for this package. + */ + localizationId: string + + /** + * Number of gatherable localizable text fragments in this package. + */ + gatherableTextDataCount: number + + /** + * Byte offset to the gatherable text data section, `-1` when absent. + */ + gatherableTextDataOffset: number + + /** + * Number of objects defined inside this package. + */ + exportCount: number + + /** + * Byte offset to the export map (list of `FObjectExport` entries). + */ + exportOffset: number + + /** + * Number of external objects this package references. + */ + importCount: number + + /** + * Byte offset to the import map (list of `FObjectImport` entries). + */ + importOffset: number + + /** + * Number of Verse cell exports (present when `VERSE_CELLS` or newer). + */ + cellExportCount: number + + /** + * Byte offset to the cell export map, `-1` when absent. + */ + cellExportOffset: number + + /** + * Number of Verse cell imports. + */ + cellImportCount: number + + /** + * Byte offset to the cell import map, `-1` when absent. + */ + cellImportOffset: number + + /** + * Byte offset to the metadata section (present when `METADATA_SERIALIZATION_OFFSET` or newer). + */ + metaDataOffset: number + + /** + * Byte offset to the export dependency table (per-export import indices). + */ + dependsOffset: number + + /** + * Number of soft package references stored in this package. + */ + softPackageReferencesCount: number + + /** + * Byte offset to the soft package references section, `-1` when absent. + */ + softPackageReferencesOffset: number + + /** + * Byte offset to the searchable names map, `-1` when absent. + */ + searchableNamesOffset: number + + /** + * Byte offset to the editor thumbnail table, `-1` in cooked packages. + */ + thumbnailTableOffset: number + + /** + * Number of import type hierarchy entries (present when `IMPORT_TYPE_HIERARCHIES` or newer). + */ + importTypeHierarchiesCount: number + + /** + * Byte offset to the import type hierarchy map, `-1` when absent. + */ + importTypeHierarchiesOffset: number + + /** + * Editor-only stable identity for this package, empty when the file was saved without editor data. + */ + persistentGuid: string + + /** + * Snapshot counts for each prior save of this package, latest last. + */ + generations: UAssetGenerationInfo[] + + /** + * Engine build that wrote this file to disk. + */ + savedByEngineVersion: UAssetEngineVersion + + /** + * Oldest engine build that can still load this file (matters for hotfix releases). + */ + compatibleWithEngineVersion: UAssetEngineVersion + + /** + * Legacy package-level compression flags, should be `0` for source (editor-side) `.uasset` files. + */ + compressionFlags: number + + /** + * Value UE uses to distinguish official saves from modder-built packages. + */ + packageSource: number + + /** + * Byte offset to the asset registry tag data (thumbnails, class info for the editor browser). + */ + assetRegistryDataOffset: number + + /** + * Byte offset where bulk data (mip levels, sound waves, geometry buffers) begins, `int64`. + */ + bulkDataStartOffset: bigint + + /** + * Byte offset to `FWorldTileInfo`, `-1` for non-map assets. + */ + worldTileInfoDataOffset: number + + /** + * Streaming-install chunk IDs this package is assigned to. + */ + chunkIds: number[] + + /** + * Number of preload dependency graph entries (cooked-only, `-1` on editor-side files). + */ + preloadDependencyCount: number + + /** + * Byte offset to the preload dependency graph. + */ + preloadDependencyOffset: number + + /** + * Number of leading name entries actually referenced by serialized export data + * (present when `NAMES_REFERENCED_FROM_EXPORT_DATA` or newer, defaults to `nameCount`). + */ + namesReferencedFromExportDataCount: number + + /** + * Byte offset to the payload table of contents (present when `PAYLOAD_TOC` or newer), `-1` when absent, `int64`. + */ + payloadTocOffset: bigint + + /** + * Byte offset to the bulk/data resource table (present when `DATA_RESOURCES` or newer), `-1` when absent. + */ + dataResourceOffset: number +} + +/** + * Decoded `FObjectImport`, one entry in the import map, represents an external object this + * package references, FName fields are pre-resolved to their display strings using the name + * table, `FPackageIndex` fields (outerIndex) are kept as raw signed integers: `0` means null, + * positive `N` points at export `N - 1`, negative `-N` points at import `N - 1`. + * @see /Engine/Source/Runtime/CoreUObject/Public/UObject/ObjectResource.h `FObjectImport` + */ +export interface UAssetImport { + /** + * Class's outer package, e.g. `"/Script/CoreUObject"` or `"/Script/Engine"`. + */ + classPackage: string + + /** + * Class type of the imported object, e.g. `"Class"`, `"Function"`, `"BlueprintGeneratedClass"`, `"Texture2D"`. + */ + className: string + + /** + * `FPackageIndex` of this import's outer (containing) object, or `0` for a top-level import. + */ + outerIndex: number + + /** + * Name of the imported object itself, e.g. `"BP_Enemy_C"` or `"T_Rock_Diffuse"`. + */ + objectName: string + + /** + * Containing package path of this import (post-`VER_UE4_NON_OUTER_PACKAGE_IMPORT`), empty + * string on older packages that inferred the package from the outer chain instead. + */ + packageName: string + + /** + * True when this import is optional and may not be present at load time + * (post-`OPTIONAL_RESOURCES`). + */ + bImportOptional: boolean +} + +/** + * Decoded `FObjectExport`, one entry in the export map, represents an object defined inside + * this package, FName fields are pre-resolved to strings, `FPackageIndex` fields are raw + * signed integers with the same convention as `UAssetImport.outerIndex`. + * @see /Engine/Source/Runtime/CoreUObject/Public/UObject/ObjectResource.h `FObjectExport` + */ +export interface UAssetExport { + /** + * Package index of the object's class (typically a negative value pointing at an import). + */ + classIndex: number + + /** + * Package index of the object's parent (super) class, or `0` when none. + */ + superIndex: number + + /** + * Package index of the object's archetype template, `0` when pre-`VER_UE4_TemplateIndex_IN_COOKED_EXPORTS`. + */ + templateIndex: number + + /** + * Package index of the object's outer (containing) object, `0` when this is a top-level export. + */ + outerIndex: number + + /** + * Name of the exported object itself. + */ + objectName: string + + /** + * `EObjectFlags` bitmask (masked to `RF_Load` on save). + */ + objectFlags: number + + /** + * Serialized byte size of this export's property data, `int64` on modern packages, + * `int32` pre-`VER_UE4_64BIT_EXPORTMAP_SERIALSIZES`. + */ + serialSize: bigint + + /** + * Byte offset to this export's serialized property data, absolute from file start. + */ + serialOffset: bigint + + /** + * True if this export was force-included by the cooker. + */ + bForcedExport: boolean + + /** + * True if this export is stripped in client-only builds. + */ + bNotForClient: boolean + + /** + * True if this export is stripped in server-only builds. + */ + bNotForServer: boolean + + /** + * True when the export was inherited from a parent generation (post-`TRACK_OBJECT_EXPORT_IS_INHERITED`). + */ + bIsInheritedInstance: boolean + + /** + * `EPackageFlags` bitmask associated with this specific export (usually mirrors summary flags). + */ + packageFlags: number + + /** + * True when the export should not always be loaded for editor games (post-`VER_UE4_LOAD_FOR_EDITOR_GAME`). + */ + bNotAlwaysLoadedForEditorGame: boolean + + /** + * True when this export represents a top-level asset (post-`VER_UE4_COOKED_ASSETS_IN_EDITOR_SUPPORT`). + */ + bIsAsset: boolean + + /** + * True when a public hash should be generated for this export (post-`OPTIONAL_RESOURCES`). + */ + bGeneratePublicHash: boolean + + /** + * Starting index into the preload dependency array + * (post-`VER_UE4_PRELOAD_DEPENDENCIES_IN_COOKED_EXPORTS`). + */ + firstExportDependency: number + + /** + * Number of "serialize before serialize" preload dependencies. + */ + serializationBeforeSerializationDependencies: number + + /** + * Number of "create before serialize" preload dependencies. + */ + createBeforeSerializationDependencies: number + + /** + * Number of "serialize before create" preload dependencies. + */ + serializationBeforeCreateDependencies: number + + /** + * Number of "create before create" preload dependencies. + */ + createBeforeCreateDependencies: number + + /** + * Start of the script serialization block (post-`SCRIPT_SERIALIZATION_OFFSET`, + * absent when using unversioned property serialization). + */ + scriptSerializationStartOffset: bigint + + /** + * End of the script serialization block. + */ + scriptSerializationEndOffset: bigint +} + +/** + * Header data for one tagged property inside an export's property stream, the value payload + * itself is not decoded here, use `valueOffset` + `size` to locate the raw bytes when a + * downstream module needs them (a type-specific extractor, a diff tool, etc.). + * @see /Engine/Source/Runtime/CoreUObject/Private/UObject/PropertyTag.cpp `operator<<(FStructuredArchive::FSlot, FPropertyTag&)` + */ +export interface UAssetPropertyTag { + /** + * Property name, e.g. `"Damage"` or `"MeshComponent"`. + */ + name: string + + /** + * Rendered property type, simple types read as the class name (`"IntProperty"`, + * `"NameProperty"`), containers include their inner type (`"StructProperty(Vector)"`, + * `"ArrayProperty(StructProperty(Vector))"`, `"MapProperty(NameProperty,ObjectProperty)"`). + */ + typeName: string + + /** + * Serialized byte size of the value payload that follows the header, `0` for skipped tags + * and `BoolProperty` tags whose value is baked into the flag bits. + */ + size: number + + /** + * Position within a static array, `0` for scalar (non-array) properties. + */ + arrayIndex: number + + /** + * True when the tag represents a `BoolProperty` set to `true`, false encodes both `false` + * booleans and non-boolean tags. + */ + boolValue: boolean + + /** + * True when the tag was marked as skipped during save, the value payload is empty. + */ + skipped: boolean + + /** + * True when the value payload uses the property's binary/native `Serialize` method instead + * of the property system's default serialization. + */ + binaryOrNativeSerialize: boolean + + /** + * Optional per-property GUID (used by properties that migrate their name across engine + * versions), empty string when not present. + */ + propertyGuid: string + + /** + * Byte offset (absolute from file start) to the start of the value payload. + */ + valueOffset: bigint +} + +/** + * Full Layer 1 decode of a `.uasset` file: header summary, resolved name table, import map, + * and export map. + */ +export interface UAssetPackage { + /** + * Decoded `FPackageFileSummary` at the top of the file. + */ + summary: UAssetPackageFileSummary + + /** + * Name table indexed by FName index. + */ + names: string[] + + /** + * External object references (`FObjectImport` entries) with FNames pre-resolved. + */ + imports: UAssetImport[] + + /** + * Objects defined inside this package (`FObjectExport` entries) with FNames pre-resolved. + */ + exports: UAssetExport[] +} + +/** + * The "main" asset of a package: the top-level export a `.uasset` was created to hold + * (a Blueprint, a Material, a Texture2D, etc.). + */ +export interface UAssetMainAsset { + /** + * Zero-based index of the main export in `pkg.exports`. + */ + index: number + + /** + * The main export entry itself. + */ + export: UAssetExport + + /** + * Resolved class name, e.g. `"Blueprint"`, `"Material"`, `"Texture2D"`, `"AnimSequence"`. + */ + className: string + + /** + * UE package path of the class, e.g. `"/Script/Engine"` or `"/Script/CoreUObject"`. + */ + classPackage: string +} + +/** + * One Blueprint SCS (Simple Construction Script) component template, extracted from an export + * whose name ends in `_GEN_VARIABLE`. + */ +export interface UAssetBlueprintComponent { + /** + * Variable name assigned in the editor, e.g. `"MeshComponent"` or `"Arrow"`. + */ + name: string + + /** + * Component class type, e.g. `"StaticMeshComponent"` or `"ArrowComponent"`. + */ + className: string +} + +/** + * Shaper output for a `UBlueprint` asset, the Layer 2 view a commit-message generator or diff + * tool consumes for Blueprint packages. + */ +export interface UAssetBlueprintShape { + /** + * Discriminator tag, always `"blueprint"` for this shape. + */ + kind: "blueprint" + + /** + * Blueprint asset name, e.g. `"BP_Enemy"`. + */ + name: string + + /** + * Class the Blueprint inherits from, e.g. `"Actor"` or `"Character"`, empty when unresolved. + */ + parentClass: string + + /** + * SCS component templates declared on the Blueprint. + */ + components: UAssetBlueprintComponent[] + + /** + * Names of user-defined function graphs (exports whose class resolves to `"Function"`). + */ + functions: string[] + + /** + * `/Game/`-rooted asset paths referenced by this Blueprint's import table. + */ + referencedAssets: string[] + + /** + * Compact one-line summary suitable for feeding into an AI prompt. + */ + renderedSummary: string +} diff --git a/src/preload/index.ts b/src/preload/index.ts index d2e50ed..3e54dc0 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,18 +1,18 @@ import appApiRoutes from "@preload/routes/app" -import dialogApiRoutes from "@preload/routes/dialog" -import gitApiRoutes from "@preload/routes/git" -import lfsApiRoutes from "@preload/routes/lfs" -import projectApiRoutes from "@preload/routes/project" -import shellApiRoutes from "@preload/routes/shell" -import treeApiRoutes from "@preload/routes/tree" -import windowApiRoutes from "@preload/routes/window" +import dialogsApiRoutes from "@preload/routes/dialogs" +import fileTreeApiRoutes from "@preload/routes/fileTree" +import gitCommandsApiRoutes from "@preload/routes/gitCommands" +import lfsCommandsApiRoutes from "@preload/routes/lfsCommands" +import projectsApiRoutes from "@preload/routes/projects" +import shellsApiRoutes from "@preload/routes/shells" +import windowsApiRoutes from "@preload/routes/windows" import { contextBridge } from "electron" -import type { ConfirmDialogOptions, DialogOptions } from "@/main/types/dialog" -import type { GitBranch, GitCommit, GitStatus } from "@/main/types/git" -import type { LfsLock, LfsLockResult } from "@/main/types/lfs" -import type { OpenProjectResult } from "@/main/types/project" -import type { AppPreferences, Project } from "@/main/types/store" -import type { FileTreeNode } from "@/main/types/tree" +import type { ConfirmDialogOptions, DialogOptions } from "@/main/types/dialogs" +import type { FileTreeNode } from "@/main/types/fileTree" +import type { GitBranch, GitCommit, GitStatus } from "@/main/types/gitCommands" +import type { LfsLock, LfsLockMigration, LfsLockResult } from "@/main/types/lfsCommands" +import type { OpenProjectResult, Project } from "@/main/types/projects" +import type { AppPreferences } from "@/main/types/store" /** * A snapshot of the current state of the application window. @@ -29,23 +29,25 @@ export type WindowState = { * The type for the API surface exposed to the renderer process via `window.api`. */ export type GitgameApi = { - app: { - version: string - } platform: { value: NodeJS.Platform isWindows: boolean isLinux: boolean isMacOS: boolean } - window: { - getState: () => Promise - onStateChange: (callback: (state: WindowState) => void) => () => void - minimize: () => void - toggleMaximize: () => void - close: () => void + app: { + version: string + } + dialogs: { + confirm: (options: ConfirmDialogOptions) => Promise + error: (title: string, message: string, detail?: string) => void + getOptions: () => Promise + respond: (result: boolean) => void + } + fileTree: { + get: (dir: string) => Promise } - git: { + gitCommands: { isRepository: (dir: string) => Promise getRepositoryRoot: (dir: string) => Promise getStatus: (dir: string) => Promise @@ -53,17 +55,24 @@ export type GitgameApi = { getLog: (dir: string, limit?: number) => Promise getRemoteUrl: (dir: string) => Promise } - lfs: { + lfsCommands: { listLocks: (dir: string) => Promise getCachedLocks: (dir: string) => Promise getLockableFiles: (dir: string) => Promise - lockPaths: (dir: string, paths: string[]) => Promise - unlockPaths: (dir: string, paths: string[], force?: boolean) => Promise + lockPaths: ( + dir: string, + paths: string[], + onProgress?: (done: number, total: number) => void, + ) => Promise + unlockPaths: ( + dir: string, + paths: string[], + force?: boolean, + onProgress?: (done: number, total: number) => void, + ) => Promise + migrateLocks: (dir: string) => Promise } - tree: { - getFileTree: (dir: string) => Promise - } - project: { + projects: { addLocal: () => Promise open: (dir: string) => Promise getRecent: () => Promise @@ -72,14 +81,15 @@ export type GitgameApi = { getPreferences: () => Promise setPreferences: (preferences: Partial) => Promise } - shell: { + shells: { openExternal: (url: string) => void } - dialog: { - confirm: (options: ConfirmDialogOptions) => Promise - error: (title: string, content: string) => void - getOptions: () => Promise - respond: (result: boolean) => void + windows: { + getState: () => Promise + onStateChange: (callback: (state: WindowState) => void) => () => void + minimize: () => void + toggleMaximize: () => void + close: () => void } } @@ -87,20 +97,20 @@ export type GitgameApi = { * The API surface exposed to the renderer process via `window.api`. */ const api: GitgameApi = { - app: appApiRoutes, platform: { value: process.platform, isWindows: process.platform === "win32", isLinux: process.platform === "linux", isMacOS: process.platform === "darwin", }, - window: windowApiRoutes, - git: gitApiRoutes, - lfs: lfsApiRoutes, - tree: treeApiRoutes, - project: projectApiRoutes, - shell: shellApiRoutes, - dialog: dialogApiRoutes, + app: appApiRoutes, + dialogs: dialogsApiRoutes, + fileTree: fileTreeApiRoutes, + gitCommands: gitCommandsApiRoutes, + lfsCommands: lfsCommandsApiRoutes, + projects: projectsApiRoutes, + shells: shellsApiRoutes, + windows: windowsApiRoutes, } as const // Exposes the API surface to the renderer process diff --git a/src/preload/lib/audio.ts b/src/preload/lib/audio.ts new file mode 100644 index 0000000..de062f4 --- /dev/null +++ b/src/preload/lib/audio.ts @@ -0,0 +1,19 @@ +import errorSound from "@main/assets/sounds/error.mp3" + +/** + * The cached error-chime audio element. + */ +let errorAudio: HTMLAudioElement | null = null + +/** + * Returns the preloaded Win95 error chime. + * @returns The cached error-chime audio element. + */ +export function getErrorAudio(): HTMLAudioElement { + if (!errorAudio) { + errorAudio = new Audio(errorSound) + errorAudio.preload = "auto" + } + + return errorAudio +} diff --git a/src/preload/lib/ipc.ts b/src/preload/lib/ipc.ts new file mode 100644 index 0000000..0248c56 --- /dev/null +++ b/src/preload/lib/ipc.ts @@ -0,0 +1,15 @@ +import { ipcRenderer } from "electron" +import type { IpcResult } from "@/main/types/ipc" + +/** + * Invokes a `safeHandle`-registered channel and unwraps its `IpcResult`, + * throwing when the handler reported a failure. + * @param channel The IPC channel to invoke. + * @param args The arguments forwarded to the handler. + * @returns The value returned by the handler. + */ +export async function safeInvoke(channel: string, ...args: unknown[]): Promise { + const result = (await ipcRenderer.invoke(channel, ...args)) as IpcResult + if (!result.ok) throw new Error(result.error) + return result.value +} diff --git a/src/preload/lib/lockProgress.ts b/src/preload/lib/lockProgress.ts new file mode 100644 index 0000000..a52fa68 --- /dev/null +++ b/src/preload/lib/lockProgress.ts @@ -0,0 +1,31 @@ +import { randomUUID } from "node:crypto" +import CONSTANTS from "@main/lib/constants" +import { ipcRenderer } from "electron" +import type { LfsLockProgress } from "@/main/types/lfsCommands" + +/** + * Runs a lock/unlock invocation while forwarding per-file progress to the + * caller's `onProgress` callback, scoped to a fresh request id. + * @param onProgress The renderer-side progress callback, or `undefined`. + * @param invoke Thunk that runs the underlying `safeInvoke` call. + * @returns The value resolved by `invoke`. + */ +export async function withLockProgress( + onProgress: ((done: number, total: number) => void) | undefined, + invoke: (requestId: string | null) => Promise, +): Promise { + if (!onProgress) return invoke(null) + + const requestId = randomUUID() + const listener = (_: unknown, payload: LfsLockProgress) => { + if (payload.requestId === requestId) onProgress(payload.done, payload.total) + } + + ipcRenderer.on(CONSTANTS.ipc.lfsCommandsLockProgress, listener) + + try { + return await invoke(requestId) + } finally { + ipcRenderer.off(CONSTANTS.ipc.lfsCommandsLockProgress, listener) + } +} diff --git a/src/preload/routes/app.ts b/src/preload/routes/app.ts index 62fc2c8..2cf57fc 100644 --- a/src/preload/routes/app.ts +++ b/src/preload/routes/app.ts @@ -2,10 +2,6 @@ import CONSTANTS from "@main/lib/constants" import type { GitgameApi } from "@preload/index" import { ipcRenderer } from "electron" -/** - * The application version, resolved once at preload load time so the renderer - * can read it as a plain string without an async IPC round-trip. - */ const appApiRoutes: GitgameApi["app"] = { version: ipcRenderer.sendSync(CONSTANTS.ipc.appGetVersion) as string, } diff --git a/src/preload/routes/dialog.ts b/src/preload/routes/dialog.ts deleted file mode 100644 index a00fc01..0000000 --- a/src/preload/routes/dialog.ts +++ /dev/null @@ -1,16 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import type { GitgameApi } from "@preload/index" -import { ipcRenderer } from "electron" - -/** - * The API surface for the Win95-styled dialog windows, used both by callers - * (`confirm`, `error`) and by the dialog window itself (`getOptions`, `respond`). - */ -const dialogApiRoutes: GitgameApi["dialog"] = { - confirm: options => ipcRenderer.invoke(CONSTANTS.ipc.dialogConfirm, options), - error: (title, content) => ipcRenderer.send(CONSTANTS.ipc.dialogError, title, content), - getOptions: () => ipcRenderer.invoke(CONSTANTS.ipc.dialogGetOptions), - respond: result => ipcRenderer.send(CONSTANTS.ipc.dialogRespond, result), -} - -export default dialogApiRoutes diff --git a/src/preload/routes/dialogs.ts b/src/preload/routes/dialogs.ts new file mode 100644 index 0000000..b0432a5 --- /dev/null +++ b/src/preload/routes/dialogs.ts @@ -0,0 +1,19 @@ +import CONSTANTS from "@main/lib/constants" +import type { GitgameApi } from "@preload/index" +import { getErrorAudio } from "@preload/lib/audio" +import { ipcRenderer } from "electron" + +const dialogsApiRoutes: GitgameApi["dialogs"] = { + confirm: options => ipcRenderer.invoke(CONSTANTS.ipc.dialogsConfirm, options), + error: (title, message, detail) => { + const audio = getErrorAudio() + audio.currentTime = 0 + audio.play().catch(() => null) + + ipcRenderer.send(CONSTANTS.ipc.dialogsError, title, message, detail) + }, + getOptions: () => ipcRenderer.invoke(CONSTANTS.ipc.dialogsGetOptions), + respond: result => ipcRenderer.send(CONSTANTS.ipc.dialogsRespond, result), +} + +export default dialogsApiRoutes diff --git a/src/preload/routes/fileTree.ts b/src/preload/routes/fileTree.ts new file mode 100644 index 0000000..dfe98ad --- /dev/null +++ b/src/preload/routes/fileTree.ts @@ -0,0 +1,10 @@ +import CONSTANTS from "@main/lib/constants" +import type { GitgameApi } from "@preload/index" +import { safeInvoke } from "@preload/lib/ipc" +import type { FileTreeNode } from "@/main/types/fileTree" + +const fileTreeApiRoutes: GitgameApi["fileTree"] = { + get: dir => safeInvoke(CONSTANTS.ipc.fileTreeGet, dir), +} + +export default fileTreeApiRoutes diff --git a/src/preload/routes/git.ts b/src/preload/routes/git.ts deleted file mode 100644 index 0f48c3d..0000000 --- a/src/preload/routes/git.ts +++ /dev/null @@ -1,20 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import type { GitgameApi } from "@preload/index" -import { ipcRenderer } from "electron" - -/** - * The API surface for read-only Git operations. - * - * Every call targets a repository by path, so the renderer is not required to - * hold a notion of a "current" working directory in the main process. - */ -const gitApiRoutes: GitgameApi["git"] = { - isRepository: dir => ipcRenderer.invoke(CONSTANTS.ipc.gitIsRepository, dir), - getRepositoryRoot: dir => ipcRenderer.invoke(CONSTANTS.ipc.gitGetRepositoryRoot, dir), - getStatus: dir => ipcRenderer.invoke(CONSTANTS.ipc.gitGetStatus, dir), - listBranches: dir => ipcRenderer.invoke(CONSTANTS.ipc.gitListBranches, dir), - getLog: (dir, limit) => ipcRenderer.invoke(CONSTANTS.ipc.gitGetLog, dir, limit), - getRemoteUrl: dir => ipcRenderer.invoke(CONSTANTS.ipc.gitGetRemoteUrl, dir), -} - -export default gitApiRoutes diff --git a/src/preload/routes/gitCommands.ts b/src/preload/routes/gitCommands.ts new file mode 100644 index 0000000..34702be --- /dev/null +++ b/src/preload/routes/gitCommands.ts @@ -0,0 +1,15 @@ +import CONSTANTS from "@main/lib/constants" +import type { GitgameApi } from "@preload/index" +import { safeInvoke } from "@preload/lib/ipc" +import type { GitBranch, GitCommit, GitStatus } from "@/main/types/gitCommands" + +const gitCommandsApiRoutes: GitgameApi["gitCommands"] = { + isRepository: dir => safeInvoke(CONSTANTS.ipc.gitCommandsIsRepository, dir), + getRepositoryRoot: dir => safeInvoke(CONSTANTS.ipc.gitCommandsGetRepositoryRoot, dir), + getStatus: dir => safeInvoke(CONSTANTS.ipc.gitCommandsGetStatus, dir), + listBranches: dir => safeInvoke(CONSTANTS.ipc.gitCommandsListBranches, dir), + getLog: (dir, limit) => safeInvoke(CONSTANTS.ipc.gitCommandsGetLog, dir, limit), + getRemoteUrl: dir => safeInvoke(CONSTANTS.ipc.gitCommandsGetRemoteUrl, dir), +} + +export default gitCommandsApiRoutes diff --git a/src/preload/routes/lfs.ts b/src/preload/routes/lfs.ts deleted file mode 100644 index c9af25c..0000000 --- a/src/preload/routes/lfs.ts +++ /dev/null @@ -1,16 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import type { GitgameApi } from "@preload/index" -import { ipcRenderer } from "electron" - -/** - * The API surface for Git LFS locking operations. - */ -const lfsApiRoutes: GitgameApi["lfs"] = { - listLocks: dir => ipcRenderer.invoke(CONSTANTS.ipc.lfsListLocks, dir), - getCachedLocks: dir => ipcRenderer.invoke(CONSTANTS.ipc.lfsGetCachedLocks, dir), - getLockableFiles: dir => ipcRenderer.invoke(CONSTANTS.ipc.lfsGetLockableFiles, dir), - lockPaths: (dir, paths) => ipcRenderer.invoke(CONSTANTS.ipc.lfsLockPaths, dir, paths), - unlockPaths: (dir, paths, force) => ipcRenderer.invoke(CONSTANTS.ipc.lfsUnlockPaths, dir, paths, force), -} - -export default lfsApiRoutes diff --git a/src/preload/routes/lfsCommands.ts b/src/preload/routes/lfsCommands.ts new file mode 100644 index 0000000..1c6ed23 --- /dev/null +++ b/src/preload/routes/lfsCommands.ts @@ -0,0 +1,22 @@ +import CONSTANTS from "@main/lib/constants" +import type { GitgameApi } from "@preload/index" +import { safeInvoke } from "@preload/lib/ipc" +import { withLockProgress } from "@preload/lib/lockProgress" +import type { LfsLock, LfsLockMigration, LfsLockResult } from "@/main/types/lfsCommands" + +const lfsCommandsApiRoutes: GitgameApi["lfsCommands"] = { + listLocks: dir => safeInvoke(CONSTANTS.ipc.lfsCommandsListLocks, dir), + getCachedLocks: dir => safeInvoke(CONSTANTS.ipc.lfsCommandsGetCachedLocks, dir), + getLockableFiles: dir => safeInvoke(CONSTANTS.ipc.lfsCommandsGetLockableFiles, dir), + lockPaths: (dir, paths, onProgress) => + withLockProgress(onProgress, requestId => + safeInvoke(CONSTANTS.ipc.lfsCommandsLockPaths, dir, paths, requestId), + ), + unlockPaths: (dir, paths, force, onProgress) => + withLockProgress(onProgress, requestId => + safeInvoke(CONSTANTS.ipc.lfsCommandsUnlockPaths, dir, paths, force, requestId), + ), + migrateLocks: dir => safeInvoke(CONSTANTS.ipc.lfsCommandsMigrateLocks, dir), +} + +export default lfsCommandsApiRoutes diff --git a/src/preload/routes/project.ts b/src/preload/routes/project.ts deleted file mode 100644 index 13d0299..0000000 --- a/src/preload/routes/project.ts +++ /dev/null @@ -1,19 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import type { GitgameApi } from "@preload/index" -import { ipcRenderer } from "electron" - -/** - * The API surface for project management operations (folder picker, recent - * projects, and preferences). - */ -const projectApiRoutes: GitgameApi["project"] = { - addLocal: () => ipcRenderer.invoke(CONSTANTS.ipc.projectAddLocal), - open: dir => ipcRenderer.invoke(CONSTANTS.ipc.projectOpen, dir), - getRecent: () => ipcRenderer.invoke(CONSTANTS.ipc.projectGetRecent), - removeRecent: dir => ipcRenderer.invoke(CONSTANTS.ipc.projectRemoveRecent, dir), - clearRecent: () => ipcRenderer.invoke(CONSTANTS.ipc.projectClearRecent), - getPreferences: () => ipcRenderer.invoke(CONSTANTS.ipc.projectGetPreferences), - setPreferences: preferences => ipcRenderer.invoke(CONSTANTS.ipc.projectSetPreferences, preferences), -} - -export default projectApiRoutes diff --git a/src/preload/routes/projects.ts b/src/preload/routes/projects.ts new file mode 100644 index 0000000..119ac4f --- /dev/null +++ b/src/preload/routes/projects.ts @@ -0,0 +1,17 @@ +import CONSTANTS from "@main/lib/constants" +import type { GitgameApi } from "@preload/index" +import { safeInvoke } from "@preload/lib/ipc" +import type { OpenProjectResult, Project } from "@/main/types/projects" +import type { AppPreferences } from "@/main/types/store" + +const projectsApiRoutes: GitgameApi["projects"] = { + addLocal: () => safeInvoke(CONSTANTS.ipc.projectsAddLocal), + open: dir => safeInvoke(CONSTANTS.ipc.projectsOpen, dir), + getRecent: () => safeInvoke(CONSTANTS.ipc.projectsGetRecent), + removeRecent: dir => safeInvoke(CONSTANTS.ipc.projectsRemoveRecent, dir), + clearRecent: () => safeInvoke(CONSTANTS.ipc.projectsClearRecent), + getPreferences: () => safeInvoke(CONSTANTS.ipc.projectsGetPreferences), + setPreferences: preferences => safeInvoke(CONSTANTS.ipc.projectsSetPreferences, preferences), +} + +export default projectsApiRoutes diff --git a/src/preload/routes/shell.ts b/src/preload/routes/shell.ts deleted file mode 100644 index a497794..0000000 --- a/src/preload/routes/shell.ts +++ /dev/null @@ -1,12 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import type { GitgameApi } from "@preload/index" -import { ipcRenderer } from "electron" - -/** - * The API surface for shell operations (opening external resources). - */ -const shellApiRoutes: GitgameApi["shell"] = { - openExternal: url => ipcRenderer.send(CONSTANTS.ipc.shellOpenExternal, url), -} - -export default shellApiRoutes diff --git a/src/preload/routes/shells.ts b/src/preload/routes/shells.ts new file mode 100644 index 0000000..5c8a58b --- /dev/null +++ b/src/preload/routes/shells.ts @@ -0,0 +1,9 @@ +import CONSTANTS from "@main/lib/constants" +import type { GitgameApi } from "@preload/index" +import { ipcRenderer } from "electron" + +const shellsApiRoutes: GitgameApi["shells"] = { + openExternal: url => ipcRenderer.send(CONSTANTS.ipc.shellsOpenExternal, url), +} + +export default shellsApiRoutes diff --git a/src/preload/routes/tree.ts b/src/preload/routes/tree.ts deleted file mode 100644 index 01bb810..0000000 --- a/src/preload/routes/tree.ts +++ /dev/null @@ -1,12 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import type { GitgameApi } from "@preload/index" -import { ipcRenderer } from "electron" - -/** - * The API surface for reading the repository file tree. - */ -const treeApiRoutes: GitgameApi["tree"] = { - getFileTree: dir => ipcRenderer.invoke(CONSTANTS.ipc.treeGetFileTree, dir), -} - -export default treeApiRoutes diff --git a/src/preload/routes/window.ts b/src/preload/routes/window.ts deleted file mode 100644 index 7ffe362..0000000 --- a/src/preload/routes/window.ts +++ /dev/null @@ -1,22 +0,0 @@ -import CONSTANTS from "@main/lib/constants" -import type { GitgameApi, WindowState } from "@preload/index" -import { ipcRenderer } from "electron" - -/** - * The API surface for window-related operations. - */ -const windowApiRoutes: GitgameApi["window"] = { - getState: () => ipcRenderer.invoke(CONSTANTS.ipc.windowGetState), - onStateChange: callback => { - const listener = (_: unknown, state: WindowState) => callback(state) - - ipcRenderer.on(CONSTANTS.ipc.windowStateChanged, listener) - - return () => ipcRenderer.off(CONSTANTS.ipc.windowStateChanged, listener) - }, - minimize: () => ipcRenderer.send(CONSTANTS.ipc.windowMinimize), - toggleMaximize: () => ipcRenderer.send(CONSTANTS.ipc.windowMaximizeToggle), - close: () => ipcRenderer.send(CONSTANTS.ipc.windowClose), -} - -export default windowApiRoutes diff --git a/src/preload/routes/windows.ts b/src/preload/routes/windows.ts new file mode 100644 index 0000000..3c60f79 --- /dev/null +++ b/src/preload/routes/windows.ts @@ -0,0 +1,19 @@ +import CONSTANTS from "@main/lib/constants" +import type { GitgameApi, WindowState } from "@preload/index" +import { ipcRenderer } from "electron" + +const windowsApiRoutes: GitgameApi["windows"] = { + getState: () => ipcRenderer.invoke(CONSTANTS.ipc.windowsGetState), + onStateChange: callback => { + const listener = (_: unknown, state: WindowState) => callback(state) + + ipcRenderer.on(CONSTANTS.ipc.windowsStateChanged, listener) + + return () => ipcRenderer.off(CONSTANTS.ipc.windowsStateChanged, listener) + }, + minimize: () => ipcRenderer.send(CONSTANTS.ipc.windowsMinimize), + toggleMaximize: () => ipcRenderer.send(CONSTANTS.ipc.windowsMaximizeToggle), + close: () => ipcRenderer.send(CONSTANTS.ipc.windowsClose), +} + +export default windowsApiRoutes diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 603c5d4..b22654c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,7 +1,7 @@ import computerIcon from "@react95-icons/Computer3_16x16_4.png" +import FileTreeProvider from "@renderer/components/contexts/FileTree" import ProjectProvider, { useProjectContext } from "@renderer/components/contexts/Project" import StatusProvider from "@renderer/components/contexts/Status" -import TreeProvider from "@renderer/components/contexts/Tree" import TreeViewProvider from "@renderer/components/contexts/TreeView" import useMenuShortcuts from "@renderer/hooks/useMenuShortcuts" import { useCallback, useEffect, useMemo } from "react" @@ -57,7 +57,7 @@ function AppShell() { openProject(action.path) break case "project:clear-recent": { - const confirmed = await window.api.dialog.confirm({ + const confirmed = await window.api.dialogs.confirm({ title: "Clear recent projects", message: "Clear the entire recent projects list?", detail: "This only forgets the entries here, your project folders on disk are left untouched.", @@ -69,13 +69,38 @@ function AppShell() { break } case "window:close": - window.api.window.close() + window.api.windows.close() break case "view:reload": window.location.reload() break case "shell:open-external": - window.api.shell.openExternal(action.url) + window.api.shells.openExternal(action.url) + break + case "devtools:test-confirm": + window.api.dialogs.confirm({ + title: "Test confirm", + message: "This is a test confirm dialog.", + detail: "Use it to preview the Win95 confirm styling from the Dev Tools menu.", + confirmLabel: "Sure", + cancelLabel: "Nope", + }) + break + case "devtools:test-error": + window.api.dialogs.error("Test error", "This is a test error message.") + break + case "devtools:test-error-with-detail": + window.api.dialogs.error( + "Test error with detail", + "5 files could not be updated.", + [ + "Content/Characters/Hero/BP_Hero.uasset: locked by john", + "Content/Characters/Hero/SK_Hero.uasset: locked by jane", + "Content/Maps/MainMenu.umap: locked by bob", + "Content/UI/HUD/WBP_HUD.uasset: locked by alice", + "Content/VFX/P_Explosion.uasset", + ].join("\n"), + ) break } }, @@ -111,11 +136,11 @@ export default function App() { - + - + diff --git a/src/renderer/DialogApp.tsx b/src/renderer/DialogApp.tsx index 8e718e8..105d7b6 100644 --- a/src/renderer/DialogApp.tsx +++ b/src/renderer/DialogApp.tsx @@ -1,8 +1,9 @@ +import errorIcon from "@react95-icons/Hand_32x32_4.png" import AppRoot from "@renderer/components/layouts/AppRoot" import MainLayout from "@renderer/components/layouts/main" import { useCallback, useEffect, useState } from "react" -import { Button } from "react95" -import type { DialogOptions } from "@/main/types/dialog" +import { Button, Frame } from "react95" +import type { DialogOptions } from "@/main/types/dialogs" import TitleBar from "@/renderer/components/bars/Title" export default function DialogApp() { @@ -12,19 +13,19 @@ export default function DialogApp() { * Responds to the dialog with a confirmation, closing the window. */ const handleConfirm = useCallback(() => { - window.api.dialog.respond(true) + window.api.dialogs.respond(true) }, []) /** * Responds to the dialog with a cancellation, closing the window. */ const handleCancel = useCallback(() => { - window.api.dialog.respond(false) + window.api.dialogs.respond(false) }, []) - // Fetch the options for this dialog window once, on mount + // Fetch the options for this dialog window once, on mount. useEffect(() => { - window.api.dialog.getOptions().then(setOptions) + window.api.dialogs.getOptions().then(setOptions) }, []) // Confirm on Enter, cancel on Escape @@ -47,11 +48,27 @@ export default function DialogApp() { -
-
-

{options?.message}

+
+
+ {options?.variant === "error" && ( + + )} + +
+

{options?.message}

- {options?.detail &&

{options.detail}

} + {options?.detail && ( + +

{options.detail}

+ + )} +
diff --git a/src/renderer/components/bars/Title.tsx b/src/renderer/components/bars/Title.tsx index 2c70049..493a885 100644 --- a/src/renderer/components/bars/Title.tsx +++ b/src/renderer/components/bars/Title.tsx @@ -28,7 +28,7 @@ export default function TitleBar({ title, icon, mode = "full", onClose }: TitleB WebkitAppRegion: "drag", } as CSSProperties } - onDoubleClick={() => window.api.window.toggleMaximize()} + onDoubleClick={() => window.api.windows.toggleMaximize()} >
{icon && !window.api.platform.isMacOS && ( @@ -48,7 +48,7 @@ export default function TitleBar({ title, icon, mode = "full", onClose }: TitleB size="sm" square aria-label="Minimize" - onClick={() => window.api.window.minimize()} + onClick={() => window.api.windows.minimize()} className="w-6! h-5.5! min-w-0!" > minimize @@ -57,7 +57,7 @@ export default function TitleBar({ title, icon, mode = "full", onClose }: TitleB size="sm" square aria-label="Maximize" - onClick={() => window.api.window.toggleMaximize()} + onClick={() => window.api.windows.toggleMaximize()} className="w-6! h-5.5! min-w-0!" > maximize @@ -69,7 +69,7 @@ export default function TitleBar({ title, icon, mode = "full", onClose }: TitleB size="sm" square aria-label="Close" - onClick={() => (onClose ? onClose() : window.api.window.close())} + onClick={() => (onClose ? onClose() : window.api.windows.close())} className="w-6! h-5.5! min-w-0! ml-1" > close diff --git a/src/renderer/components/contexts/Tree.tsx b/src/renderer/components/contexts/FileTree.tsx similarity index 53% rename from src/renderer/components/contexts/Tree.tsx rename to src/renderer/components/contexts/FileTree.tsx index 0127b0a..52e6d0a 100644 --- a/src/renderer/components/contexts/Tree.tsx +++ b/src/renderer/components/contexts/FileTree.tsx @@ -1,18 +1,18 @@ import { useProjectContext } from "@renderer/components/contexts/Project" import { useStatusContext } from "@renderer/components/contexts/Status" import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react" -import type { LfsLock, LfsLockResult } from "@/main/types/lfs" -import type { FileTreeNode } from "@/main/types/tree" +import type { FileTreeNode } from "@/main/types/fileTree" +import type { LfsLock, LfsLockResult } from "@/main/types/lfsCommands" +import { reportError } from "@/renderer/lib/utils/errors" import { indexLocks } from "@/renderer/lib/utils/lockStates" /** * The type for the tree context. */ -export type TreeContextType = { +export type FileTreeContextType = { fileTree: FileTreeNode[] locksByPath: Map isLoading: boolean - error: Error | null refresh: () => Promise lock: (paths: string[]) => Promise unlock: (paths: string[], force?: boolean) => Promise @@ -22,20 +22,19 @@ export type TreeContextType = { * The `Tree` context, providing the current repository's file tree and its Git * LFS lock state. */ -export const TreeContext = createContext(undefined) +export const FileTreeContext = createContext(undefined) /** - * The props for the `TreeProvider` component. + * The props for the `FileTreeProvider` component. */ -type TreeProviderProps = { +type FileTreeProviderProps = { children: ReactNode } -export default function TreeProvider({ children }: TreeProviderProps) { +export default function FileTreeProvider({ children }: FileTreeProviderProps) { const [fileTree, setFileTree] = useState([]) const [locksByPath, setLocksByPath] = useState>(new Map()) const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) const { currentProject } = useProjectContext() const { runTask } = useStatusContext() @@ -50,18 +49,18 @@ export default function TreeProvider({ children }: TreeProviderProps) { } try { - setLocksByPath(indexLocks(await window.api.lfs.listLocks(currentProject?.path ?? null))) + setLocksByPath(indexLocks(await window.api.lfsCommands.listLocks(currentProject.path))) } catch (err) { - setError(err instanceof Error ? err : new Error(String(err))) + reportError("Failed to load LFS locks", err) } }, [currentProject?.path]) /** - * Reloads the file tree and the LFS locks for the current project. + * Reloads the file tree and the LFS locks for the current project, + * migrating any locks held on staged renames to their new path so a locked + * file keeps its lock after being renamed. */ const refresh = useCallback(async () => { - setError(null) - if (!currentProject?.path) { setFileTree([]) setLocksByPath(new Map()) @@ -72,20 +71,31 @@ export default function TreeProvider({ children }: TreeProviderProps) { // Sets the cached locks immediately if available try { - setLocksByPath(indexLocks(await window.api.lfs.getCachedLocks(currentProject?.path ?? null))) + setLocksByPath(indexLocks(await window.api.lfsCommands.getCachedLocks(currentProject.path))) } catch { // A cache miss is expected on first-ever open and must not block refresh } // The tree and the locks load in parallel so a slow git-lfs call does not // delay the tree render, and each surfaces as soon as it is ready - const treePromise = window.api.tree - .getFileTree(currentProject?.path ?? null) + const treePromise = window.api.fileTree + .get(currentProject.path) .then(setFileTree) - .catch(err => setError(err instanceof Error ? err : new Error(String(err)))) + .catch(err => reportError("Failed to load file tree", err)) await Promise.all([treePromise, refreshLocks()]) + // Carries locks across staged renames, then re-reads the locks if any + // migration actually moved a lock so the tree overlays the new paths + try { + const migrations = await window.api.lfsCommands.migrateLocks(currentProject.path) + if (migrations.some(m => m.status === "migrated" || m.status === "failed-unlock")) { + await refreshLocks() + } + } catch (err) { + reportError("Failed to migrate LFS locks", err) + } + setIsLoading(false) }, [refreshLocks, currentProject?.path]) @@ -97,16 +107,27 @@ export default function TreeProvider({ children }: TreeProviderProps) { async (paths: string[]): Promise => { if (!currentProject?.path) return [] - const label = `Locking ${paths.length} ${paths.length === 1 ? "file" : "files"}...` - try { - return await runTask(label, async () => { - const results = await window.api.lfs.lockPaths(currentProject?.path ?? null, paths) + return await runTask("Locking...", async handle => { + const results = await window.api.lfsCommands.lockPaths( + currentProject?.path ?? null, + paths, + (done, total) => { + if (total <= 1) { + handle.setLabel(`Locking ${total} ${total === 1 ? "file" : "files"}...`) + return + } + + handle.setLabel(`Locking ${done}/${total} files...`) + handle.setProgress((done / total) * 100) + }, + ) + await refreshLocks() return results }) } catch (err) { - setError(err instanceof Error ? err : new Error(String(err))) + reportError("Failed to lock files", err) return [] } }, @@ -123,16 +144,28 @@ export default function TreeProvider({ children }: TreeProviderProps) { if (!currentProject?.path) return [] const verb = force ? "Force-unlocking" : "Unlocking" - const label = `${verb} ${paths.length} ${paths.length === 1 ? "file" : "files"}...` try { - return await runTask(label, async () => { - const results = await window.api.lfs.unlockPaths(currentProject?.path ?? null, paths, force) + return await runTask(`${verb}...`, async handle => { + const results = await window.api.lfsCommands.unlockPaths( + currentProject?.path ?? null, + paths, + force, + (done, total) => { + if (total <= 1) { + handle.setLabel(`${verb} ${total} ${total === 1 ? "file" : "files"}...`) + return + } + + handle.setLabel(`${verb} ${done}/${total} files...`) + handle.setProgress((done / total) * 100) + }, + ) await refreshLocks() return results }) } catch (err) { - setError(err instanceof Error ? err : new Error(String(err))) + reportError(force ? "Failed to force-unlock files" : "Failed to unlock files", err) return [] } }, @@ -145,28 +178,27 @@ export default function TreeProvider({ children }: TreeProviderProps) { }, [refresh]) return ( - {children} - + ) } /** - * A custom hook to access the repository file tree and lock state from the `TreeContext`. + * A custom hook to access the repository file tree and lock state from the `FileTreeContext`. * @returns The tree context value. */ -export function useTreeContext() { - const ctx = useContext(TreeContext) - if (ctx === undefined) throw new Error("'useTreeContext' must be used within a 'TreeProvider'") +export function useFileTreeContext() { + const ctx = useContext(FileTreeContext) + if (ctx === undefined) throw new Error("'useFileTreeContext' must be used within a 'FileTreeProvider'") return ctx } diff --git a/src/renderer/components/contexts/Project.tsx b/src/renderer/components/contexts/Project.tsx index a579c48..ae4cdef 100644 --- a/src/renderer/components/contexts/Project.tsx +++ b/src/renderer/components/contexts/Project.tsx @@ -1,7 +1,6 @@ import type { ReactNode } from "react" import { createContext, useCallback, useContext, useEffect, useState } from "react" -import type { OpenProjectResult } from "@/main/types/project" -import type { Project } from "@/main/types/store" +import type { OpenProjectResult, Project } from "@/main/types/projects" /** * The type for the project context. @@ -34,7 +33,7 @@ type ProjectProviderProps = { /** * Provides the current project and recent projects to the component tree, backed - * by the `window.api.project` bridge. + * by the `window.api.projects` bridge. */ export default function ProjectProvider({ children }: ProjectProviderProps) { const [currentProject, setCurrentProject] = useState(null) @@ -47,7 +46,7 @@ export default function ProjectProvider({ children }: ProjectProviderProps) { * Refreshes the recent projects list from the store. */ const refreshRecentProjects = useCallback(async () => { - setRecentProjects(await window.api.project.getRecent()) + setRecentProjects(await window.api.projects.getRecent()) }, []) /** @@ -78,13 +77,13 @@ export default function ProjectProvider({ children }: ProjectProviderProps) { /** * Prompts the user to pick a local folder and opens it as the current project. */ - const addLocalProject = useCallback(() => runOpen(() => window.api.project.addLocal()), [runOpen]) + const addLocalProject = useCallback(() => runOpen(() => window.api.projects.addLocal()), [runOpen]) /** * Opens an existing project by path (e.g. from the recent projects list). * @param dir The absolute repository path to open. */ - const openProject = useCallback((dir: string) => runOpen(() => window.api.project.open(dir)), [runOpen]) + const openProject = useCallback((dir: string) => runOpen(() => window.api.projects.open(dir)), [runOpen]) /** * Removes a project from the recent projects list, closing it if it is the @@ -92,7 +91,7 @@ export default function ProjectProvider({ children }: ProjectProviderProps) { * @param dir The absolute repository path to forget. */ const removeRecentProject = useCallback(async (dir: string) => { - setRecentProjects(await window.api.project.removeRecent(dir)) + setRecentProjects(await window.api.projects.removeRecent(dir)) setCurrentProject(current => (current?.path === dir ? null : current)) }, []) @@ -100,7 +99,7 @@ export default function ProjectProvider({ children }: ProjectProviderProps) { * Clears every entry from the recent projects list. */ const clearRecentProjects = useCallback(async () => { - setRecentProjects(await window.api.project.clearRecent()) + setRecentProjects(await window.api.projects.clearRecent()) }, []) /** @@ -119,7 +118,7 @@ export default function ProjectProvider({ children }: ProjectProviderProps) { let cancelled = false - window.api.git + window.api.gitCommands .getRemoteUrl(currentProject.path) .then(url => { if (cancelled) return @@ -143,8 +142,8 @@ export default function ProjectProvider({ children }: ProjectProviderProps) { try { const [projects, preferences] = await Promise.all([ - window.api.project.getRecent(), - window.api.project.getPreferences(), + window.api.projects.getRecent(), + window.api.projects.getPreferences(), ]) setRecentProjects(projects) diff --git a/src/renderer/components/contexts/TreeView.tsx b/src/renderer/components/contexts/TreeView.tsx index a4e5d11..0dec225 100644 --- a/src/renderer/components/contexts/TreeView.tsx +++ b/src/renderer/components/contexts/TreeView.tsx @@ -1,5 +1,5 @@ +import { useFileTreeContext } from "@renderer/components/contexts/FileTree" import { useProjectContext } from "@renderer/components/contexts/Project" -import { useTreeContext } from "@renderer/components/contexts/Tree" import WORKSPACE_CONFIG from "@renderer/config/workspace" import useDebouncedValue from "@renderer/hooks/useDebouncedValue" import { @@ -32,7 +32,7 @@ import { useMemo, useState, } from "react" -import type { FileTreeNode } from "@/main/types/tree" +import type { FileTreeNode } from "@/main/types/fileTree" /** * The state of an open context menu, anchored to a right-clicked node and @@ -111,7 +111,7 @@ type TreeViewProviderProps = { export default function TreeViewProvider({ children }: TreeViewProviderProps) { const { currentProject } = useProjectContext() - const { fileTree, locksByPath, lock, unlock } = useTreeContext() + const { fileTree, locksByPath, lock, unlock } = useFileTreeContext() const [expandedPaths, setExpandedPaths] = useState([]) const [selectedPath, setSelectedPath] = useState(undefined) @@ -137,7 +137,7 @@ export default function TreeViewProvider({ children }: TreeViewProviderProps) { useEffect(() => { let cancelled = false - window.api.project + window.api.projects .getPreferences() .then(preferences => { if (cancelled) return @@ -361,7 +361,7 @@ export default function TreeViewProvider({ children }: TreeViewProviderProps) { dismissMenu() - const confirmed = await window.api.dialog.confirm({ + const confirmed = await window.api.dialogs.confirm({ title: "Force unlock", message: `Force unlock ${menu.othersPaths.length} file${menu.othersPaths.length === 1 ? "" : "s"} locked by other users?`, detail: "Forcing may discard work the lock owner has not pushed yet.", diff --git a/src/renderer/components/menus/TreeViewContext.tsx b/src/renderer/components/menus/TreeViewContextMenu.tsx similarity index 100% rename from src/renderer/components/menus/TreeViewContext.tsx rename to src/renderer/components/menus/TreeViewContextMenu.tsx diff --git a/src/renderer/components/panes/Details.tsx b/src/renderer/components/panes/Details.tsx index 3588cbd..4b1d8a2 100644 --- a/src/renderer/components/panes/Details.tsx +++ b/src/renderer/components/panes/Details.tsx @@ -1,8 +1,8 @@ import { cn } from "@cybearl/cypack/frontend" import lockIcon from "@react95-icons/Lock_16x16_4.png" import refreshIcon from "@react95-icons/Refresh_16x16_4.png" +import { useFileTreeContext } from "@renderer/components/contexts/FileTree" import { useProjectContext } from "@renderer/components/contexts/Project" -import { useTreeContext } from "@renderer/components/contexts/Tree" import { useTreeViewContext } from "@renderer/components/contexts/TreeView" import Tooltip from "@renderer/components/ui/Tooltip" import { useCallback, useMemo } from "react" @@ -16,7 +16,7 @@ type DetailsPaneProps = { export default function DetailsPane({ className }: DetailsPaneProps) { const { currentProject } = useProjectContext() - const { locksByPath, refresh, lock, unlock } = useTreeContext() + const { locksByPath, refresh, lock, unlock } = useFileTreeContext() const { selectedNode } = useTreeViewContext() /** @@ -100,7 +100,7 @@ export default function DetailsPane({ className }: DetailsPaneProps) { * users, after native confirmation. */ const handleForceUnlock = useCallback(async () => { - const confirmed = await window.api.dialog.confirm({ + const confirmed = await window.api.dialogs.confirm({ title: "Force unlock", message: `Force unlock ${othersPaths.length} file${othersPaths.length === 1 ? "" : "s"} locked by other users?`, detail: "Forcing may discard work the lock owner has not pushed yet.", diff --git a/src/renderer/components/panes/Files.tsx b/src/renderer/components/panes/Files.tsx index 247cb0a..047b82c 100644 --- a/src/renderer/components/panes/Files.tsx +++ b/src/renderer/components/panes/Files.tsx @@ -3,7 +3,7 @@ import lockIcon from "@react95-icons/Lock_16x16_4.png" import { useTreeViewContext } from "@renderer/components/contexts/TreeView" import { type ChangeEvent, type CSSProperties, useCallback } from "react" import { Button, TextInput } from "react95" -import TreeView from "@/renderer/components/views/Tree" +import TreeView from "@/renderer/components/views/TreeView" type FilesPaneProps = { className?: string diff --git a/src/renderer/components/rows/FlatResults.tsx b/src/renderer/components/rows/FlatResults.tsx index e689b81..3e4a47a 100644 --- a/src/renderer/components/rows/FlatResults.tsx +++ b/src/renderer/components/rows/FlatResults.tsx @@ -1,10 +1,10 @@ import { cn } from "@cybearl/cypack/frontend" -import { useTreeContext } from "@renderer/components/contexts/Tree" +import { useFileTreeContext } from "@renderer/components/contexts/FileTree" import { useTreeViewContext } from "@renderer/components/contexts/TreeView" import { renderLockIcon, renderTypeIcon } from "@renderer/lib/utils/treeView" import { useMemo } from "react" import type { RowComponentProps } from "react-window" -import type { FileTreeNode } from "@/main/types/tree" +import type { FileTreeNode } from "@/main/types/fileTree" /** * The shared per-row data forwarded to every virtualized row by react-window's @@ -16,7 +16,7 @@ export type FlatResultsRowData = { } export default function FlatResultsRow({ index, style, matches }: RowComponentProps) { - const { locksByPath } = useTreeContext() + const { locksByPath } = useFileTreeContext() const { selectedPath, lockStates, lockOwners, select, openMenu } = useTreeViewContext() /** diff --git a/src/renderer/components/spaces/Workspace.tsx b/src/renderer/components/spaces/Workspace.tsx index cfabb78..e2ce2ab 100644 --- a/src/renderer/components/spaces/Workspace.tsx +++ b/src/renderer/components/spaces/Workspace.tsx @@ -11,7 +11,7 @@ export default function Workspace() { * @param finalWidth The width to persist, in pixels. */ const persistWidth = useCallback((finalWidth: number) => { - window.api.project.setPreferences({ filesPaneWidth: finalWidth }) + window.api.projects.setPreferences({ filesPaneWidth: finalWidth }) }, []) const { width, setWidth, handleDragStart } = useResizablePaneWidth({ @@ -25,7 +25,7 @@ export default function Workspace() { useEffect(() => { let cancelled = false - window.api.project.getPreferences().then(preferences => { + window.api.projects.getPreferences().then(preferences => { if (cancelled) return setWidth(preferences.filesPaneWidth) }) diff --git a/src/renderer/components/views/Tree.tsx b/src/renderer/components/views/TreeView.tsx similarity index 95% rename from src/renderer/components/views/Tree.tsx rename to src/renderer/components/views/TreeView.tsx index fdf69b7..bc5889f 100644 --- a/src/renderer/components/views/Tree.tsx +++ b/src/renderer/components/views/TreeView.tsx @@ -1,15 +1,15 @@ -import { useTreeContext } from "@renderer/components/contexts/Tree" +import { useFileTreeContext } from "@renderer/components/contexts/FileTree" import { useTreeViewContext } from "@renderer/components/contexts/TreeView" import { type MouseEvent, useCallback, useEffect, useMemo, useRef } from "react" import { TreeView as React95TreeView, ScrollView } from "react95" import FlatResultsList from "@/renderer/components/lists/FlatResults" -import TreeViewContextMenu from "@/renderer/components/menus/TreeViewContext" +import TreeViewContextMenu from "@/renderer/components/menus/TreeViewContextMenu" import { buildTree, findNodeByPath, resolveNode } from "@/renderer/lib/utils/treeView" export default function TreeView() { const containerRef = useRef(null) - const { fileTree, locksByPath } = useTreeContext() + const { fileTree, locksByPath } = useFileTreeContext() const { expandedPaths, selectedPath, diff --git a/src/renderer/config/menus.ts b/src/renderer/config/menus.ts index ecdba6c..0694682 100644 --- a/src/renderer/config/menus.ts +++ b/src/renderer/config/menus.ts @@ -1,4 +1,4 @@ -import type { Project } from "@/main/types/store" +import type { Project } from "@/main/types/projects" /** * A dispatchable menu action. @@ -10,6 +10,9 @@ export type MenuAction = | { type: "window:close" } | { type: "view:reload" } | { type: "shell:open-external"; url: string } + | { type: "devtools:test-confirm" } + | { type: "devtools:test-error" } + | { type: "devtools:test-error-with-detail" } /** * External resources opened from the `Help` menu. @@ -99,6 +102,27 @@ export function buildTopLevelMenus( currentProject: Project | null, remoteBrowsableUrl: string | null, ): TopLevelMenu[] { + const devToolsMenu: TopLevelMenu = { + label: "Dev Tools", + items: [ + { + type: "item", + label: "Test Confirm Dialog", + action: { type: "devtools:test-confirm" }, + }, + { + type: "item", + label: "Test Error Dialog", + action: { type: "devtools:test-error" }, + }, + { + type: "item", + label: "Test Error Dialog with Detail", + action: { type: "devtools:test-error-with-detail" }, + }, + ], + } + return [ { label: "File", @@ -342,6 +366,7 @@ export function buildTopLevelMenus( }, ], }, + ...(import.meta.env.DEV ? [devToolsMenu] : []), { label: "Help", items: [ diff --git a/src/renderer/hooks/useWindowStates.tsx b/src/renderer/hooks/useWindowStates.tsx index 259a619..8a5ca36 100644 --- a/src/renderer/hooks/useWindowStates.tsx +++ b/src/renderer/hooks/useWindowStates.tsx @@ -9,11 +9,11 @@ export default function useWindowStates() { useEffect(() => { let cancelled = false - window.api.window.getState().then(state => { + window.api.windows.getState().then(state => { if (!cancelled) setStates(state) }) - const unsubscribe = window.api.window.onStateChange(setStates) + const unsubscribe = window.api.windows.onStateChange(setStates) return () => { cancelled = true diff --git a/src/renderer/index.html b/src/renderer/index.html index 18e7b78..2234be8 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -3,7 +3,7 @@ - + GitGame diff --git a/src/renderer/lib/utils/errors.ts b/src/renderer/lib/utils/errors.ts new file mode 100644 index 0000000..7a2f5b5 --- /dev/null +++ b/src/renderer/lib/utils/errors.ts @@ -0,0 +1,11 @@ +/** + * Surfaces an unexpected operation failure in a native error dialog, extracting + * a plain message from an unknown thrown value so callers can pass the raw + * `catch` binding without narrowing it first. + * @param title The dialog title, describing what the app was trying to do. + * @param err The thrown value, typically the argument of a `catch` clause. + */ +export function reportError(title: string, err: unknown) { + const message = err instanceof Error ? err.message : String(err) + window.api.dialogs.error(title, message) +} diff --git a/src/renderer/lib/utils/lockStates.ts b/src/renderer/lib/utils/lockStates.ts index 6796212..f259f17 100644 --- a/src/renderer/lib/utils/lockStates.ts +++ b/src/renderer/lib/utils/lockStates.ts @@ -1,5 +1,5 @@ -import type { LfsLock } from "@/main/types/lfs" -import type { FileTreeNode } from "@/main/types/tree" +import type { FileTreeNode } from "@/main/types/fileTree" +import type { LfsLock } from "@/main/types/lfsCommands" /** * The lock state of a tree node: diff --git a/src/renderer/lib/utils/search.ts b/src/renderer/lib/utils/search.ts index a5f8731..bd6a8d9 100644 --- a/src/renderer/lib/utils/search.ts +++ b/src/renderer/lib/utils/search.ts @@ -1,6 +1,6 @@ import picomatch from "picomatch" +import type { FileTreeNode } from "@/main/types/fileTree" import type { AppPreferences } from "@/main/types/store" -import type { FileTreeNode } from "@/main/types/tree" /** * The user-facing search filters applied on top of the file tree. @@ -108,7 +108,7 @@ export function collectMatchingFiles(nodes: FileTreeNode[], filters: SearchFilte * @param patch The preference fields to persist. */ export function persistSearchPreferences(patch: Partial) { - window.api.project.setPreferences(patch).catch(error => { + window.api.projects.setPreferences(patch).catch(error => { console.error("Failed to persist search preferences:", error) }) } diff --git a/src/renderer/lib/utils/treeView.tsx b/src/renderer/lib/utils/treeView.tsx index 09fc70a..16c2c01 100644 --- a/src/renderer/lib/utils/treeView.tsx +++ b/src/renderer/lib/utils/treeView.tsx @@ -4,8 +4,8 @@ import folderIcon from "@react95-icons/Folder_16x16_4.png" import lockIcon from "@react95-icons/Lock_16x16_4.png" import Tooltip from "@renderer/components/ui/Tooltip" import type { TreeLeaf } from "react95" -import type { LfsLock, LfsLockResult } from "@/main/types/lfs" -import type { FileTreeNode } from "@/main/types/tree" +import type { FileTreeNode } from "@/main/types/fileTree" +import type { LfsLock, LfsLockResult } from "@/main/types/lfsCommands" import type { LockOwnerCount, NodeLockState } from "@/renderer/lib/utils/lockStates" /** @@ -16,9 +16,10 @@ export function reportLockFailures(results: LfsLockResult[]) { const failed = results.filter(result => !result.ok) if (failed.length === 0) return + const message = `${failed.length} file${failed.length === 1 ? "" : "s"} could not be updated.` const detail = failed.map(result => (result.error ? `${result.path}: ${result.error}` : result.path)).join("\n") - window.api.dialog.error("Some files could not be updated", detail) + window.api.dialogs.error("Some files could not be updated", message, detail) } /** diff --git a/src/renderer/styles/react95.css b/src/renderer/styles/react95.css index d9daf37..4bd5246 100644 --- a/src/renderer/styles/react95.css +++ b/src/renderer/styles/react95.css @@ -1,5 +1,4 @@ @layer base { - /* * Inlined from `react95`'s `styleReset` so the renderer no longer needs a * styled-components `createGlobalStyle` wrapper just to inject these rules. @@ -195,7 +194,6 @@ details > summary::before { transform: translateX(-10%); } - /** * Emphasizes the currently selected tree item without cascading into descendants * of a selected folder. Uses `text-shadow` to simulate bold so the pixel-font @@ -216,4 +214,4 @@ details > summary::before { background-position: bottom right; background-size: 16px 16px; background-repeat: no-repeat; -} \ No newline at end of file +} diff --git a/src/renderer/types/assets.d.ts b/src/renderer/types/assets.d.ts index aa27514..13fe9e8 100644 --- a/src/renderer/types/assets.d.ts +++ b/src/renderer/types/assets.d.ts @@ -14,6 +14,14 @@ declare module "*.woff2" { export default src } +/** + * Ambient declaration for static MP3 asset imports resolved by Vite at build time. + */ +declare module "*.mp3" { + const src: string + export default src +} + /** * Ambient declaration for direct PNG imports through the `@react95-icons/*` alias (for React95). */ diff --git a/src/renderer/types/env.d.ts b/src/renderer/types/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/renderer/types/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tests/unit/uassets/parser.test.ts b/tests/unit/uassets/parser.test.ts new file mode 100644 index 0000000..864ceb1 --- /dev/null +++ b/tests/unit/uassets/parser.test.ts @@ -0,0 +1,62 @@ +import { existsSync, readFileSync } from "node:fs" +import { resolve } from "node:path" +import { parseUAsset } from "@main/lib/uassets/parseUAsset" +import { resolveMainAsset } from "@main/lib/uassets/resolveMainAsset" +import { describe, test } from "vitest" + +/** + * Path to the fixture UAsset file. + */ +const FIXTURE_PATH = resolve(process.cwd(), ".ue5/UAssets/Blueprints/BP_Paticle_Wind_Settings.uasset") + +describe.skipIf(!existsSync(FIXTURE_PATH))("parseUAsset: BP_Paticle_Wind_Settings.uasset (UE 5.8)", () => { + const buffer = readFileSync(FIXTURE_PATH) + const pkg = parseUAsset(buffer) + + test("summary tag and version fields match a modern UE 5.8 file", ({ expect }) => { + expect(pkg.summary.tag).toBe(0x9e2a83c1) + expect(pkg.summary.legacyFileVersion).toBe(-9) + expect(pkg.summary.fileVersionUE4).toBeGreaterThan(0) + expect(pkg.summary.fileVersionUE5).toBeGreaterThanOrEqual(1000) + }) + + test("summary section counts and total header size are positive", ({ expect }) => { + expect(pkg.summary.nameCount).toBeGreaterThan(0) + expect(pkg.summary.importCount).toBeGreaterThan(0) + expect(pkg.summary.exportCount).toBeGreaterThan(0) + expect(pkg.summary.totalHeaderSize).toBeGreaterThan(0) + }) + + test("summary packageName looks like a UE package path", ({ expect }) => { + expect(pkg.summary.packageName).toMatch(/^\/(Game|Engine|Script|Temp)/) + }) + + test("name table decodes to summary.nameCount entries and includes None", ({ expect }) => { + expect(pkg.names).toHaveLength(pkg.summary.nameCount) + expect(pkg.names).toContain("None") + }) + + test("import map exposes at least one /Script/CoreUObject import", ({ expect }) => { + expect(pkg.imports).toHaveLength(pkg.summary.importCount) + const coreImport = pkg.imports.find(i => i.classPackage === "/Script/CoreUObject") + expect(coreImport).toBeDefined() + }) + + test("every export has a non-empty name plus positive serial size and offset", ({ expect }) => { + expect(pkg.exports).toHaveLength(pkg.summary.exportCount) + + for (const entry of pkg.exports) { + expect(entry.objectName.length).toBeGreaterThan(0) + expect(entry.serialSize).toBeGreaterThan(0n) + expect(entry.serialOffset).toBeGreaterThan(0n) + } + }) + + test("resolveMainAsset identifies the Blueprint export and its class", ({ expect }) => { + const main = resolveMainAsset(pkg) + expect(main.export.objectName).toBe("BP_Paticle_Wind_Settings") + expect(main.className).toBe("Blueprint") + expect(main.classPackage).toBe("/Script/Engine") + expect(main.index).toBeGreaterThanOrEqual(0) + }) +}) diff --git a/tests/unit/uassets/propertyTag.test.ts b/tests/unit/uassets/propertyTag.test.ts new file mode 100644 index 0000000..31e9fa1 --- /dev/null +++ b/tests/unit/uassets/propertyTag.test.ts @@ -0,0 +1,106 @@ +import { UAssetBufferReader } from "@main/lib/uassets/bufferReader" +import { readUAssetPropertyTags } from "@main/lib/uassets/parser/propertyTag" +import { describe, test } from "vitest" +import type { UAssetPackageFileSummary } from "@/main/types/uassets" + +/** + * Bare-minimum `UAssetPackageFileSummary` shape used by `readUAssetPropertyTags`, the reader + * only touches `packageFlags` (unversioned check) and `fileVersionUE5` (format gate), so we + * cast a partial object rather than filling every field. + */ +const SUMMARY = { + packageFlags: 0, + fileVersionUE5: 1012, // PROPERTY_TAG_COMPLETE_TYPE_NAME +} as unknown as UAssetPackageFileSummary + +/** + * Synthetic name table used across the buffers constructed below. Index 0 is `"None"` so the + * reader's stream terminator is a simple zero-filled FName pair. + */ +const NAMES = ["None", "MyIntProp", "IntProperty", "MyBoolProp", "BoolProperty"] + +describe("readUAssetPropertyTags: synthetic streams", () => { + test("reads one IntProperty tag and stops at the None terminator", ({ expect }) => { + const buffer = Buffer.from([ + // "name": FName(index=1 -> "MyIntProp", number=0) + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // "typeName": 1 node, FName(index=2 -> "IntProperty", number=0), innerCount=0 + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // "size": int32(4) + 0x04, 0x00, 0x00, 0x00, + // "flags": uint8(0), no optional fields + 0x00, + // "value": int32(42) + 0x2a, 0x00, 0x00, 0x00, + // Stream terminator: FName(index=0 -> "None", number=0) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]) + const reader = new UAssetBufferReader(buffer) + + const tags = readUAssetPropertyTags(reader, SUMMARY, NAMES, 0) + + expect(tags).toHaveLength(1) + expect(tags[0].name).toBe("MyIntProp") + expect(tags[0].typeName).toBe("IntProperty") + expect(tags[0].size).toBe(4) + expect(tags[0].arrayIndex).toBe(0) + expect(tags[0].boolValue).toBe(false) + expect(tags[0].skipped).toBe(false) + expect(tags[0].binaryOrNativeSerialize).toBe(false) + expect(tags[0].propertyGuid).toBe("") + }) + + test("BoolTrue flag surfaces via boolValue with a zero-size payload", ({ expect }) => { + const buffer = Buffer.from([ + // "name": FName(index=3 -> "MyBoolProp", number=0) + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // "typeName": 1 node, FName(index=4 -> "BoolProperty", number=0), innerCount=0 + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // "size": int32(0), value lives in the flag bits + 0x00, 0x00, 0x00, 0x00, + // "flags": uint8(0x10 = BoolTrue) + 0x10, + // Stream terminator: FName(index=0 -> "None", number=0) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]) + const reader = new UAssetBufferReader(buffer) + + const tags = readUAssetPropertyTags(reader, SUMMARY, NAMES, 0) + + expect(tags).toHaveLength(1) + expect(tags[0].name).toBe("MyBoolProp") + expect(tags[0].typeName).toBe("BoolProperty") + expect(tags[0].size).toBe(0) + expect(tags[0].boolValue).toBe(true) + }) + + test("empty stream (immediate None terminator) returns no tags", ({ expect }) => { + // Stream terminator only: FName(index=0 -> "None", number=0) + const buffer = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + const reader = new UAssetBufferReader(buffer) + + const tags = readUAssetPropertyTags(reader, SUMMARY, NAMES, 0) + + expect(tags).toHaveLength(0) + }) + + test("throws on unversioned property serialization", ({ expect }) => { + const unversioned = { + packageFlags: 0x2000, // PKG_UnversionedProperties + fileVersionUE5: 1012, + } as unknown as UAssetPackageFileSummary + const reader = new UAssetBufferReader(Buffer.alloc(8)) + + expect(() => readUAssetPropertyTags(reader, unversioned, NAMES, 0)).toThrow(/unversioned/) + }) + + test("throws on legacy pre-PROPERTY_TAG_COMPLETE_TYPE_NAME files", ({ expect }) => { + const legacy = { + packageFlags: 0, + fileVersionUE5: 1011, + } as unknown as UAssetPackageFileSummary + const reader = new UAssetBufferReader(Buffer.alloc(8)) + + expect(() => readUAssetPropertyTags(reader, legacy, NAMES, 0)).toThrow(/legacy/i) + }) +}) diff --git a/tests/unit/uassets/shapers/blueprint.test.ts b/tests/unit/uassets/shapers/blueprint.test.ts new file mode 100644 index 0000000..f7f5445 --- /dev/null +++ b/tests/unit/uassets/shapers/blueprint.test.ts @@ -0,0 +1,47 @@ +import { existsSync, readFileSync } from "node:fs" +import { resolve } from "node:path" +import { parseUAsset } from "@main/lib/uassets/parseUAsset" +import { resolveMainAsset } from "@main/lib/uassets/resolveMainAsset" +import { shapeBlueprint } from "@main/lib/uassets/shapers/blueprint" +import { describe, test } from "vitest" + +/** + * Path to the fixture UAsset file. + */ +const FIXTURE_PATH = resolve(process.cwd(), ".ue5/UAssets/Blueprints/BP_Paticle_Wind_Settings.uasset") + +describe.skipIf(!existsSync(FIXTURE_PATH))("shapeBlueprint: BP_Paticle_Wind_Settings.uasset (UE 5.8)", () => { + const buffer = readFileSync(FIXTURE_PATH) + const pkg = parseUAsset(buffer) + const main = resolveMainAsset(pkg) + const shape = shapeBlueprint(pkg, main) + + test("shape is tagged as a Blueprint and mirrors the fixture asset name", ({ expect }) => { + expect(shape.kind).toBe("blueprint") + expect(shape.name).toBe("BP_Paticle_Wind_Settings") + }) + + test("parentClass resolves to a non-empty class name", ({ expect }) => { + expect(shape.parentClass.length).toBeGreaterThan(0) + }) + + test("components include Arrow (ArrowComponent) and Billboard (BillboardComponent)", ({ expect }) => { + const arrow = shape.components.find(c => c.name === "Arrow") + const billboard = shape.components.find(c => c.name === "Billboard") + expect(arrow?.className).toBe("ArrowComponent") + expect(billboard?.className).toBe("BillboardComponent") + }) + + test("functions and referencedAssets are arrays (possibly empty)", ({ expect }) => { + expect(Array.isArray(shape.functions)).toBe(true) + expect(Array.isArray(shape.referencedAssets)).toBe(true) + }) + + test("renderedSummary embeds the asset name, parent class, and section counts", ({ expect }) => { + expect(shape.renderedSummary).toContain("BP_Paticle_Wind_Settings") + expect(shape.renderedSummary).toContain(`parent=${shape.parentClass}`) + expect(shape.renderedSummary).toContain(`components=${shape.components.length}`) + expect(shape.renderedSummary).toContain(`functions=${shape.functions.length}`) + expect(shape.renderedSummary).toContain(`refs=${shape.referencedAssets.length}`) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..5414010 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import tsconfigPaths from "vite-tsconfig-paths" +import { defineConfig } from "vitest/config" + +/** + * The main configuration for the Vitest tests. + */ +const vitestConfig = defineConfig({ + plugins: [tsconfigPaths()], + test: { + include: ["./tests/unit/**/*.test.ts"], + }, +}) + +export default vitestConfig diff --git a/yarn.lock b/yarn.lock index 4cd0ef6..244733a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -173,58 +173,58 @@ "@babel/helper-validator-identifier" "^7.29.7" "@biomejs/biome@^2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.5.2.tgz#02e284f63fbbef1feca5297688e4f0459949c5c0" - integrity sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA== + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.5.3.tgz#492a6b3f7b899cc060af1fb8210cf0cbf536df8d" + integrity sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A== optionalDependencies: - "@biomejs/cli-darwin-arm64" "2.5.2" - "@biomejs/cli-darwin-x64" "2.5.2" - "@biomejs/cli-linux-arm64" "2.5.2" - "@biomejs/cli-linux-arm64-musl" "2.5.2" - "@biomejs/cli-linux-x64" "2.5.2" - "@biomejs/cli-linux-x64-musl" "2.5.2" - "@biomejs/cli-win32-arm64" "2.5.2" - "@biomejs/cli-win32-x64" "2.5.2" - -"@biomejs/cli-darwin-arm64@2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.2.tgz#97e2eae09112b9d5ee357a5c2998da8c7e990df2" - integrity sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg== - -"@biomejs/cli-darwin-x64@2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.2.tgz#1cffb19f327a2ad732fbfe8969836fd28c4e211b" - integrity sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g== - -"@biomejs/cli-linux-arm64-musl@2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.2.tgz#e8d85b60996e272ca80a8bdb5c6a6d61b7cccc0a" - integrity sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw== - -"@biomejs/cli-linux-arm64@2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.2.tgz#95721e4bba88739f41c55f5f48c7aab12ab79127" - integrity sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ== - -"@biomejs/cli-linux-x64-musl@2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.2.tgz#ae5f210a0905469e964f97ad5128db2e0c17d6fd" - integrity sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw== - -"@biomejs/cli-linux-x64@2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.2.tgz#c5e45474ae82d48800c51f213fb21c13021276c4" - integrity sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA== - -"@biomejs/cli-win32-arm64@2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.2.tgz#471bb6d54f5218c20efaa519705e0e2b3bd025ca" - integrity sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww== - -"@biomejs/cli-win32-x64@2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.2.tgz#637657694440b95556870ed5464ff5546e2fb3f0" - integrity sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A== + "@biomejs/cli-darwin-arm64" "2.5.3" + "@biomejs/cli-darwin-x64" "2.5.3" + "@biomejs/cli-linux-arm64" "2.5.3" + "@biomejs/cli-linux-arm64-musl" "2.5.3" + "@biomejs/cli-linux-x64" "2.5.3" + "@biomejs/cli-linux-x64-musl" "2.5.3" + "@biomejs/cli-win32-arm64" "2.5.3" + "@biomejs/cli-win32-x64" "2.5.3" + +"@biomejs/cli-darwin-arm64@2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.3.tgz#a0a21f00cdee8cc8dfc4e527902ae963155b2458" + integrity sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g== + +"@biomejs/cli-darwin-x64@2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.3.tgz#657807a7e41399764ef977fea393f4d4aa5e9e03" + integrity sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ== + +"@biomejs/cli-linux-arm64-musl@2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.3.tgz#4750c8b16bf621c1cc1f0eb75be9b84da7d0fa5f" + integrity sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg== + +"@biomejs/cli-linux-arm64@2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.3.tgz#e2dbd0a2131bf6e8f229da98bcfff860cab981e7" + integrity sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA== + +"@biomejs/cli-linux-x64-musl@2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.3.tgz#498d54b571babe46e509df65065dc9531b1f0555" + integrity sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ== + +"@biomejs/cli-linux-x64@2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.3.tgz#b7ba31747be7ad4ffaba357b38bc5f5c46afb629" + integrity sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA== + +"@biomejs/cli-win32-arm64@2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.3.tgz#8a8549f61c022815a3ceb98b219668124c3ad1f1" + integrity sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw== + +"@biomejs/cli-win32-x64@2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.3.tgz#fdcaef0909b166f32d28c0a52f4f84e4e4dab63f" + integrity sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q== "@cybearl/cypack@^1.11.10": version "1.11.10" @@ -333,22 +333,22 @@ minimatch "^9.0.3" plist "^3.1.0" -"@emnapi/core@^1.10.0": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6" - integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ== +"@emnapi/core@^1.11.1": + version "1.11.2" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.2.tgz#fab0a0f3c492d11f5a9ac9065d0d73955ee1c1c9" + integrity sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA== dependencies: "@emnapi/wasi-threads" "1.2.2" tslib "^2.4.0" -"@emnapi/runtime@^1.10.0": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24" - integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw== +"@emnapi/runtime@^1.11.1": + version "1.11.2" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.2.tgz#eb22f04d76febfdf4f87fdaff54c8a53f6bf0dbd" + integrity sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.2.2", "@emnapi/wasi-threads@^1.2.1": +"@emnapi/wasi-threads@1.2.2", "@emnapi/wasi-threads@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz#4c93becf5bfa3b13d1bbdcc06aee38321ad8139a" integrity sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA== @@ -729,9 +729,9 @@ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@react95/icons@^2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@react95/icons/-/icons-2.5.0.tgz#3d3bc5ec0bab800691f8efa1769b058c85637da7" - integrity sha512-EtqvLwDDush/szePx7Jp2HndovImDWXJ1BGBKpwQJ5BHJqCEq7UDjwkwaSkK904VNmprP/hzjHwhUfPyk1yG5Q== + version "2.5.3" + resolved "https://registry.yarnpkg.com/@react95/icons/-/icons-2.5.3.tgz#49463ae4c3df98d97e935c319ac78da73d25640b" + integrity sha512-vbo/0bdHKSg1ilCVrh47IhqywSBb7TqENalYaR+u5LRjimTcxVSqrEyLFksDVPfE/EFCmcJOv17MEUPhod4SQA== "@rolldown/pluginutils@1.0.0-rc.3": version "1.0.0-rc.3" @@ -875,10 +875,10 @@ dependencies: defer-to-connect "^2.0.0" -"@tailwindcss/node@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.3.1.tgz#77402afcfa29c4b48b8494d0edfc4428d0a504ba" - integrity sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A== +"@tailwindcss/node@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.3.2.tgz#2ba563b05ff662b9172c9645d78a5edd59f96eb3" + integrity sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg== dependencies: "@jridgewell/remapping" "^2.3.5" enhanced-resolve "5.21.6" @@ -886,101 +886,101 @@ lightningcss "1.32.0" magic-string "^0.30.21" source-map-js "^1.2.1" - tailwindcss "4.3.1" - -"@tailwindcss/oxide-android-arm64@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz#83c6762cd383a2ebc6e01897b0f35f19225e6653" - integrity sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ== - -"@tailwindcss/oxide-darwin-arm64@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz#2558b7e835889ad721823e4dcb50dd5071d747d8" - integrity sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA== - -"@tailwindcss/oxide-darwin-x64@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz#d987957b87a26668b6d0117ccd4a8a4d1a318a2b" - integrity sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg== - -"@tailwindcss/oxide-freebsd-x64@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz#75b342c81a07b1afa437976ec82f86d372431da7" - integrity sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g== - -"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz#6730adc6d17187eeeff2f14f6a914d009749cb97" - integrity sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg== - -"@tailwindcss/oxide-linux-arm64-gnu@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz#869d16b3d9bd8097b797a3dd876db0368c07eae3" - integrity sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ== - -"@tailwindcss/oxide-linux-arm64-musl@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz#ab110680ce3c7a2a135656db4402dffc1fb9c1d7" - integrity sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA== - -"@tailwindcss/oxide-linux-x64-gnu@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz#422a4175a76ae60dd9d17946eec3584cb636352f" - integrity sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg== - -"@tailwindcss/oxide-linux-x64-musl@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz#f4c714a653a0e742955d2af2c53d0064b4c500d1" - integrity sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ== - -"@tailwindcss/oxide-wasm32-wasi@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz#32172ca8b2427b9c2bb09c97756960185b7d4fc0" - integrity sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA== - dependencies: - "@emnapi/core" "^1.10.0" - "@emnapi/runtime" "^1.10.0" - "@emnapi/wasi-threads" "^1.2.1" + tailwindcss "4.3.2" + +"@tailwindcss/oxide-android-arm64@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz#add4f8eba265b20b14634a22717621a3350cd6d4" + integrity sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA== + +"@tailwindcss/oxide-darwin-arm64@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz#2439b7c991679c006d16ed00264c60d540567554" + integrity sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w== + +"@tailwindcss/oxide-darwin-x64@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz#016ffbb375bb5feabab0fd42c744b3cf21647968" + integrity sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ== + +"@tailwindcss/oxide-freebsd-x64@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz#6fe112b103ff659671483293c61c853651635ee0" + integrity sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA== + +"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz#30fffb495ac59ea01046ee48c2bfa247ca6eda5e" + integrity sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w== + +"@tailwindcss/oxide-linux-arm64-gnu@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz#910f4754e9aa8f8b04c018bebab627a52bd568b9" + integrity sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw== + +"@tailwindcss/oxide-linux-arm64-musl@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz#bdb9861bb5064209ec5ff101afff6d046fb15ecd" + integrity sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA== + +"@tailwindcss/oxide-linux-x64-gnu@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz#424276287610607303a35f52f2a172b95af4898a" + integrity sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w== + +"@tailwindcss/oxide-linux-x64-musl@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz#606d5dc2fd7d2e8cdd7d1f1c4a1f4d5639056e05" + integrity sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw== + +"@tailwindcss/oxide-wasm32-wasi@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz#f5d4ed2bcd12507217cc1a92e4fc507fdac6dd8c" + integrity sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw== + dependencies: + "@emnapi/core" "^1.11.1" + "@emnapi/runtime" "^1.11.1" + "@emnapi/wasi-threads" "^1.2.2" "@napi-rs/wasm-runtime" "^1.1.4" "@tybys/wasm-util" "^0.10.2" tslib "^2.8.1" -"@tailwindcss/oxide-win32-arm64-msvc@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz#07a11b6eb1f578d012460e6ad6f2352a28d32514" - integrity sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg== +"@tailwindcss/oxide-win32-arm64-msvc@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz#a4ec761083fef55c530e204fad43edba629ea1bf" + integrity sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ== -"@tailwindcss/oxide-win32-x64-msvc@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz#60c6095d97b141c02de36bb52a16c358d9bdaa98" - integrity sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA== +"@tailwindcss/oxide-win32-x64-msvc@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz#38bf0e3878d17afedb0260758b661973b2d9de70" + integrity sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ== -"@tailwindcss/oxide@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.3.1.tgz#6fdd28b3abf785e2c2cac31f52c4755875826828" - integrity sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA== +"@tailwindcss/oxide@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.3.2.tgz#82157fb0d5bebf1188234855c5b7dc754da54065" + integrity sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag== optionalDependencies: - "@tailwindcss/oxide-android-arm64" "4.3.1" - "@tailwindcss/oxide-darwin-arm64" "4.3.1" - "@tailwindcss/oxide-darwin-x64" "4.3.1" - "@tailwindcss/oxide-freebsd-x64" "4.3.1" - "@tailwindcss/oxide-linux-arm-gnueabihf" "4.3.1" - "@tailwindcss/oxide-linux-arm64-gnu" "4.3.1" - "@tailwindcss/oxide-linux-arm64-musl" "4.3.1" - "@tailwindcss/oxide-linux-x64-gnu" "4.3.1" - "@tailwindcss/oxide-linux-x64-musl" "4.3.1" - "@tailwindcss/oxide-wasm32-wasi" "4.3.1" - "@tailwindcss/oxide-win32-arm64-msvc" "4.3.1" - "@tailwindcss/oxide-win32-x64-msvc" "4.3.1" + "@tailwindcss/oxide-android-arm64" "4.3.2" + "@tailwindcss/oxide-darwin-arm64" "4.3.2" + "@tailwindcss/oxide-darwin-x64" "4.3.2" + "@tailwindcss/oxide-freebsd-x64" "4.3.2" + "@tailwindcss/oxide-linux-arm-gnueabihf" "4.3.2" + "@tailwindcss/oxide-linux-arm64-gnu" "4.3.2" + "@tailwindcss/oxide-linux-arm64-musl" "4.3.2" + "@tailwindcss/oxide-linux-x64-gnu" "4.3.2" + "@tailwindcss/oxide-linux-x64-musl" "4.3.2" + "@tailwindcss/oxide-wasm32-wasi" "4.3.2" + "@tailwindcss/oxide-win32-arm64-msvc" "4.3.2" + "@tailwindcss/oxide-win32-x64-msvc" "4.3.2" "@tailwindcss/vite@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@tailwindcss/vite/-/vite-4.3.1.tgz#636f812c870061bd9255ca24d756216f4df031d9" - integrity sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ== + version "4.3.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/vite/-/vite-4.3.2.tgz#35787e4d450a6b2430693245483441cfd78bf612" + integrity sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA== dependencies: - "@tailwindcss/node" "4.3.1" - "@tailwindcss/oxide" "4.3.1" - tailwindcss "4.3.1" + "@tailwindcss/node" "4.3.2" + "@tailwindcss/oxide" "4.3.2" + tailwindcss "4.3.2" "@tootallnate/once@2": version "2.0.1" @@ -1037,6 +1037,14 @@ "@types/node" "*" "@types/responselike" "^1.0.0" +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + "@types/debug@^4.1.6": version "4.1.13" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.13.tgz#22d1cc9d542d3593caea764f974306ab36286ee7" @@ -1044,7 +1052,12 @@ dependencies: "@types/ms" "*" -"@types/estree@1.0.9": +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + +"@types/estree@1.0.9", "@types/estree@^1.0.0": version "1.0.9" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== @@ -1074,9 +1087,9 @@ integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node@*", "@types/node@^24.12.2", "@types/node@^24.9.0": - version "24.13.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.2.tgz#3b9b280a7055128359f125eb1067d9a190f39854" - integrity sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA== + version "24.13.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.3.tgz#49f18bd3c647866dcda51a0756c145e14590ce16" + integrity sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q== dependencies: undici-types "~7.18.0" @@ -1129,6 +1142,67 @@ "@types/babel__core" "^7.20.5" react-refresh "^0.18.0" +"@vitest/expect@3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.7.tgz#70a34158383d008c3bf5d802e2643317f09df6d8" + integrity sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/spy" "3.2.7" + "@vitest/utils" "3.2.7" + chai "^5.2.0" + tinyrainbow "^2.0.0" + +"@vitest/mocker@3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.7.tgz#331be944cb783c642dd42bd743411aca24ea0466" + integrity sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA== + dependencies: + "@vitest/spy" "3.2.7" + estree-walker "^3.0.3" + magic-string "^0.30.17" + +"@vitest/pretty-format@3.2.7", "@vitest/pretty-format@^3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.7.tgz#2a7b593f8e007e9d8ef7e7343aa30ec73fdeaf29" + integrity sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA== + dependencies: + tinyrainbow "^2.0.0" + +"@vitest/runner@3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.7.tgz#c0c080228189f1fa6cda40f59be09d746b0aca51" + integrity sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA== + dependencies: + "@vitest/utils" "3.2.7" + pathe "^2.0.3" + strip-literal "^3.0.0" + +"@vitest/snapshot@3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.7.tgz#a3a7e1950ce99ec4cf02395e20ddca403b6c818e" + integrity sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g== + dependencies: + "@vitest/pretty-format" "3.2.7" + magic-string "^0.30.17" + pathe "^2.0.3" + +"@vitest/spy@3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.7.tgz#ca7fbee44019523ca450395d9a2284ce9ece1f31" + integrity sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ== + dependencies: + tinyspy "^4.0.3" + +"@vitest/utils@3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.7.tgz#302c8126211ac4dfea87b3b5085c098d6d22e89e" + integrity sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw== + dependencies: + "@vitest/pretty-format" "3.2.7" + loupe "^3.1.4" + tinyrainbow "^2.0.0" + "@xmldom/xmldom@^0.9.10": version "0.9.10" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz#a0ad5a26fe8aa996310870726e1704977f769dee" @@ -1269,6 +1343,11 @@ assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -1314,10 +1393,10 @@ base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -baseline-browser-mapping@^2.10.38: - version "2.10.40" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz#f372c8eb36ff4ad0b5e7ae467014abef124554ba" - integrity sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw== +baseline-browser-mapping@^2.10.42: + version "2.10.42" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz#195dcc76baa269a497f0b07decace169fee9ac58" + integrity sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q== bl@^4.1.0: version "4.1.0" @@ -1341,17 +1420,17 @@ bluebird@^3.5.5: integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== brace-expansion@^1.1.7: - version "1.1.15" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.15.tgz#a6d90d54067236e5f42570a3b7378d594d9b7738" - integrity sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg== + version "1.1.16" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.16.tgz#723d3a30c0558c225abc9fc479a73e14e26c3c2f" + integrity sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^2.0.1, brace-expansion@^2.0.2: - version "2.1.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.1.tgz#c68b1c4111c76aae3a6fba55d496cee10c39dad8" - integrity sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA== + version "2.1.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.2.tgz#0bba2271feb7d458b0d31ad13625aaa4754431e2" + integrity sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA== dependencies: balanced-match "^1.0.0" @@ -1363,14 +1442,14 @@ brace-expansion@^5.0.5: balanced-match "^4.0.2" browserslist@^4.24.0: - version "4.28.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.4.tgz#dd8b8167a32845ff5f8cd6ce13f5abba16cd04c9" - integrity sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw== - dependencies: - baseline-browser-mapping "^2.10.38" - caniuse-lite "^1.0.30001799" - electron-to-chromium "^1.5.376" - node-releases "^2.0.48" + version "4.28.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.5.tgz#438b7d38c0d4b47740bbb36778d5bdca01b37838" + integrity sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ== + dependencies: + baseline-browser-mapping "^2.10.42" + caniuse-lite "^1.0.30001800" + electron-to-chromium "^1.5.387" + node-releases "^2.0.50" update-browserslist-db "^1.2.3" buffer-from@^1.0.0: @@ -1484,10 +1563,21 @@ camelize@^1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== -caniuse-lite@^1.0.30001799: - version "1.0.30001799" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz#5c909138c27f1a61219d3e092071c1cc7d32dc55" - integrity sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw== +caniuse-lite@^1.0.30001800: + version "1.0.30001803" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz#b2a5d696e042bc8304dcd4942c39fe330fbbcb24" + integrity sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg== + +chai@^5.2.0: + version "5.3.3" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06" + integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" @@ -1497,6 +1587,11 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +check-error@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5" + integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA== + chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" @@ -1675,7 +1770,7 @@ dateformat@^5.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-5.0.3.tgz#fe2223eff3cc70ce716931cb3038b59a9280696e" integrity sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA== -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4, debug@^4.4.1: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -1694,6 +1789,16 @@ dedent-js@^1.0.1: resolved "https://registry.yarnpkg.com/dedent-js/-/dedent-js-1.0.1.tgz#bee5fb7c9e727d85dffa24590d10ec1ab1255305" integrity sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ== +dedent@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.2.tgz#34e2264ab538301e27cf7b07bf2369c19baa8dd9" + integrity sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA== + +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + defaults@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" @@ -1819,10 +1924,10 @@ electron-publish@25.1.7: lazy-val "^1.0.5" mime "^2.5.2" -electron-to-chromium@^1.5.376: - version "1.5.379" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz#ccece5fdc4a19880b570a3903881701df848b763" - integrity sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA== +electron-to-chromium@^1.5.387: + version "1.5.389" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz#538be9ebec78026d4daba6be321ab854dfac2a8f" + integrity sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg== electron-updater@^6.3.9: version "6.8.9" @@ -1851,9 +1956,9 @@ electron-vite@^5.0.0: picocolors "^1.1.1" electron@^42.5.0: - version "42.5.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-42.5.0.tgz#09de6f3f10146ed03c401405da695b5743ecdec7" - integrity sha512-cYEKS9XFz+c9fAB4jI0x49yz1FFzB55r3q96wu9YkwwJMv7t9202IE/ltlgy6yitl/J4M7C8JQcmUqdzDvPl/w== + version "42.6.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-42.6.1.tgz#00650f2c47d5e50f580f420c974c75d2179819e7" + integrity sha512-HR9yiOyl+kZpSAugjOpBNvKpoh1wNpTK4wl6geD9W9Xmo6L6IgEzVB/JKlbEUDdXYeqRaaEhTUSSfyQ/g9iXvQ== dependencies: "@electron-internal/extract-zip" "^1.0.1" "@electron/get" "^5.0.0" @@ -1916,6 +2021,11 @@ es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-module-lexer@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" @@ -2002,6 +2112,18 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +expect-type@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.4.0.tgz#24edf7f0cc69a44d008567ba4594ab96f3c3a3d6" + integrity sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA== + exponential-backoff@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz#51cf92c1c0493c766053f9d3abee4434c244d2f6" @@ -2013,9 +2135,9 @@ extsprintf@^1.2.0: integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== fast-copy@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-4.0.3.tgz#935adef81c26276dcbe8892347af307b5090206a" - integrity sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw== + version "4.0.4" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-4.0.4.tgz#e569e2331ee6a9d67826c31aeb0e84b50bfb63e6" + integrity sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA== fast-deep-equal@^3.1.1: version "3.1.3" @@ -2203,6 +2325,11 @@ glob@^8.0.1: minimatch "^5.0.1" once "^1.3.0" +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -2452,6 +2579,11 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-tokens@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.1.tgz#2ec43964658435296f6761b34e10671c2d9527f4" + integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== + js-yaml@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.0.tgz#d1900572a7f7cf0b5f540c83673e60bad3436592" @@ -2597,6 +2729,11 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +loupe@^3.1.0, loupe@^3.1.4: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" + integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== + lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" @@ -2626,7 +2763,7 @@ lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -magic-string@^0.30.19, magic-string@^0.30.21: +magic-string@^0.30.17, magic-string@^0.30.19, magic-string@^0.30.21: version "0.30.21" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== @@ -2845,10 +2982,10 @@ node-gyp@^9.0.0: tar "^6.1.2" which "^2.0.2" -node-releases@^2.0.48: - version "2.0.50" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.50.tgz#597197a852071ce42fc2550e58e223242bcba969" - integrity sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg== +node-releases@^2.0.50: + version "2.0.51" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.51.tgz#cdc08433577f5b32ad01694481726e22eeb54aef" + integrity sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ== nopt@^6.0.0: version "6.0.0" @@ -2948,6 +3085,16 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +pathval@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d" + integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ== + pe-library@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/pe-library/-/pe-library-0.4.1.tgz#e269be0340dcb13aa6949d743da7d658c3e2fbea" @@ -2958,12 +3105,7 @@ picocolors@^1.1.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^4.0.3, picomatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" - integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== - -picomatch@^4.0.5: +picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4, picomatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab" integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A== @@ -3031,9 +3173,9 @@ postcss-value-parser@^4.0.2: integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@^8.5.6: - version "8.5.15" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c" - integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A== + version "8.5.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28" + integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg== dependencies: nanoid "^3.3.12" picocolors "^1.1.1" @@ -3285,6 +3427,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + signal-exit@^3.0.2, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -3375,11 +3522,21 @@ ssri@^9.0.0: dependencies: minipass "^3.1.1" +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + stat-mode@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== +std-env@^3.9.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" + integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -3440,6 +3597,13 @@ strip-json-comments@^5.0.2: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-5.0.3.tgz#b7304249dd402ee67fd518ada993ab3593458bcf" integrity sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw== +strip-literal@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-3.1.0.tgz#222b243dd2d49c0bcd0de8906adbd84177196032" + integrity sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg== + dependencies: + js-tokens "^9.0.1" + styled-components@^6.4.3: version "6.4.3" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.4.3.tgz#9544423c537b76ba091cb5a8b6b4f36833df4cfc" @@ -3474,10 +3638,10 @@ tailwind-merge@^3.3.1: resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.6.0.tgz#88d83242d1dd7bc847223f73dcf210dd1f2ee11c" integrity sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w== -tailwindcss@4.3.1, tailwindcss@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.3.1.tgz#78ee06f6186bc8fb9603f8083eb703dc7dd96a10" - integrity sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q== +tailwindcss@4.3.2, tailwindcss@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.3.2.tgz#408ee67d767a0fef7b174674bb9c5ce136a5ace1" + integrity sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA== tapable@^2.3.3: version "2.3.3" @@ -3516,7 +3680,17 @@ tiny-typed-emitter@^2.1.0: resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5" integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA== -tinyglobby@^0.2.15: +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +tinyglobby@^0.2.14, tinyglobby@^0.2.15: version "0.2.17" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== @@ -3524,6 +3698,21 @@ tinyglobby@^0.2.15: fdir "^6.5.0" picomatch "^4.0.4" +tinypool@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" + integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== + +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + +tinyspy@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78" + integrity sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q== + tmp-promise@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" @@ -3543,6 +3732,11 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" +tsconfck@^3.0.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" + integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== + tslib@^2.4.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" @@ -3616,7 +3810,27 @@ verror@^1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vite@^7.0.0: +vite-node@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" + integrity sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg== + dependencies: + cac "^6.7.14" + debug "^4.4.1" + es-module-lexer "^1.7.0" + pathe "^2.0.3" + vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" + +vite-tsconfig-paths@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz#d9a71106a7ff2c1c840c6f1708042f76a9212ed4" + integrity sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + +"vite@^5.0.0 || ^6.0.0 || ^7.0.0-0", vite@^7.0.0: version "7.3.6" resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.6.tgz#0547a395e68d3746e9a505f1fd4469fe09b49cc4" integrity sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg== @@ -3630,6 +3844,35 @@ vite@^7.0.0: optionalDependencies: fsevents "~2.3.3" +vitest@^3.2.4: + version "3.2.7" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.7.tgz#1944b6ed013a25fd26a73d18e1af92c10a57af6c" + integrity sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/expect" "3.2.7" + "@vitest/mocker" "3.2.7" + "@vitest/pretty-format" "^3.2.7" + "@vitest/runner" "3.2.7" + "@vitest/snapshot" "3.2.7" + "@vitest/spy" "3.2.7" + "@vitest/utils" "3.2.7" + chai "^5.2.0" + debug "^4.4.1" + expect-type "^1.2.1" + magic-string "^0.30.17" + pathe "^2.0.3" + picomatch "^4.0.2" + std-env "^3.9.0" + tinybench "^2.9.0" + tinyexec "^0.3.2" + tinyglobby "^0.2.14" + tinypool "^1.1.1" + tinyrainbow "^2.0.0" + vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" + vite-node "3.2.4" + why-is-node-running "^2.3.0" + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" @@ -3644,6 +3887,14 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + wide-align@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"