Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,7 @@ vite.config.ts.timestamp-*
.vite/

# AI
.claude
.claude

# UE5 source code
.ue5
14 changes: 13 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,65 @@
<h2 align="center">@cybearl/gitgame</h2>
<p align="center">A small tool with a web interface to simplify UE5-based game development with Git LFS.</p>
</p>

## 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
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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"
Expand Down
Binary file added src/main/assets/sounds/error.mp3
Binary file not shown.
4 changes: 2 additions & 2 deletions src/main/config/dialogs.ts
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/main/config/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const WEB_PREFERENCES: WebPreferences = {
sandbox: false,
contextIsolation: true,
nodeIntegration: false,
autoplayPolicy: "no-user-gesture-required",
}

/**
Expand Down
28 changes: 14 additions & 14 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -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"

/**
Expand Down Expand Up @@ -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()

Expand Down
101 changes: 65 additions & 36 deletions src/main/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 10 additions & 10 deletions src/main/lib/dialog/index.ts → src/main/lib/dialogs/index.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -40,7 +40,7 @@ function loadDialog(window: BrowserWindow) {
*/
export function openDialog(parent: BrowserWindow | null, options: DialogOptions): Promise<boolean> {
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,
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
10 changes: 10 additions & 0 deletions src/main/lib/fileTree/handlers.ts
Original file line number Diff line number Diff line change
@@ -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))
}
Loading
Loading