Status: Phase 2 implemented 2026-08-03
Unity Asset Workbench is a desktop-first inspection and extraction tool for Unity game modding. User points it at an installed game's data directory. App discovers Unity containers, creates a disk-backed searchable SQLite catalog, exposes exact asset locations and metadata through bounded pages, and copies selected raw object payloads to user-owned output folders.
Primary promise: quick visibility into packed game data without changing source installation.
- Browse folders through native desktop dialogs.
- List useful identity and location data for every readable asset.
- Remain responsive during large scans.
- Extract one or many assets safely.
- Keep parsing independent from desktop/UI libraries.
- Leave clear extension points for format-aware exporters and future mod utilities.
- Reconstruct a Unity Editor project.
- Modify or repack source data.
- Decode every Unity version and proprietary codec.
- Resolve every MonoBehaviour schema.
- Circumvent encryption or DRM.
- App restores most recent data folder.
- User selects game root or
GameName_Datathrough PhotinoEx dialog. - App discovers candidate files recursively.
- Background scan identifies serialized files and UnityFS-family bundles.
- Scanner computes game build identity and resumes completed containers from the matching SQLite catalog.
- Parsed asset addresses are committed in bounded batches; UI reports progress and server-pages live rows during parsing.
- A completion marker makes each container a durable resume boundary. Incomplete rows stay viewable but are cleared before that container is reparsed.
- SQL queries exclude locally owned components from the top-level browser and apply text/type filters plus paging without materializing the catalog.
- Inspecting an object opens a detail pane, queries that GameObject's components on demand, and reopens only the selected asset for a bounded format-aware preview.
- User selects row or checkbox set.
- App asks for output folder through native dialog.
- Extractor reopens each source container read-only and locates asset by bundle entry plus path ID.
- Exact serialized byte range is copied to a unique output name.
- Failures are isolated per asset during bulk extraction.
- Inspector dispatches by Unity class ID after user selects one asset.
- TextAsset becomes bounded UTF-8/UTF-16 text or hex; Texture2D/Sprite becomes PNG; AudioClip exposes metadata and supported encoded media.
- Decoded export reopens source and writes one unique format-appropriate artifact. Raw export stays available.
- CSV/JSON catalog export streams all rows directly from SQLite, including component ownership, without building an in-memory catalog.
flowchart LR
UI["MudBlazor UI"] --> Contracts["Core contracts"]
Host["PhotinoEx desktop host"] --> UI
UI --> Dialogs["PhotinoEx native dialogs"]
Contracts --> Catalog["AssetsTools catalog"]
Catalog --> Discovery["Unity file discovery"]
Catalog --> ATN["AssetsTools.NET"]
Catalog --> Decoder["Format-aware decoders"]
Catalog --> SQLite["SQLite address catalog"]
Discovery --> Game["Game data folder (read only)"]
ATN --> Game
Catalog --> Output["User extraction folder"]
UI --> Settings["Local JSON settings"]
UnityAssetWorkbench.Core
- Dependency-free domain records.
IUnityAssetCatalogscan/extract boundary.IWorkbenchSettingsStorepersistence boundary.- Safe output-name utility.
UnityAssetWorkbench.Infrastructure
- Candidate-file discovery.
- Bundle signature detection.
- AssetsTools.NET parsing and raw byte extraction.
- AssetsTools.NET.Texture image decoding, text/hex inspection, audio resource resolution, and decoded export.
- Game build fingerprinting, streaming SQLite writes, paging, filtering, and lazy component queries.
- Streaming CSV/JSON catalog export.
- Local JSON settings under
%LOCALAPPDATA%/UnityAssetWorkbench/settings.json.
UnityAssetWorkbench.Desktop
- PhotinoEx application lifetime and native window configuration.
- MudBlazor component tree and theme.
- UI orchestration, progress, cancellation, filtering, selection, notifications.
- Explicit build copy for MudBlazor static assets because PhotinoEx uses a physical
wwwrootprovider rather than ASP.NET static-web-asset middleware.
UnityAssetWorkbench.Tests
- Fast unit tests around discovery, signatures, safe names, and empty-catalog behavior.
UnityAssetRecord holds:
- Source file absolute path
- Optional bundle entry path
- Container kind
- Unity path ID
- Numeric type ID and readable type name
- Best-effort
m_Name - Serialized byte size
- Unity version
- Optional
m_GameObjectobject reference for component ownership
Asset key combines source file, bundle entry, path ID, and type ID. This identity is stable for one game build but not promised across game updates.
An asset is nested only when its owner reference uses local file ID zero and resolves to a GameObject path ID in the same source file and bundle entry. This prevents identical path IDs in different serialized containers from being joined. External or unresolved references remain visible as top-level objects rather than disappearing from the browser.
AssetCatalogSnapshot is a small immutable scan summary: root, asset count, warnings, timing, display version, build fingerprint, and resumed-container count. It never owns asset rows.
AssetCatalogQuery and AssetCatalogPage define bounded database reads. AssetInspection returns one selected record, locally owned components, and an optional AssetPreview. Preview kind identifies hex, text, image, audio, or metadata; byte media and decoded-export extension exist only when supported. UnityAssetRecord remains the page/selection transport type, not catalog storage authority.
Build identity is deliberately faster than hashing every game byte. SHA-256 covers a schema marker plus every discovered container's relative path, byte length, and UTC modification ticks. Top-level game executable names, metadata, and product/file versions are also included. Display text uses executable version when available plus a short fingerprint; otherwise it uses the short fingerprint alone.
Any identity change replaces that game root's active database and reparses all containers. This conservative whole-build boundary prevents a catalog from mixing versions and prevents rollback copies from accumulating. A mutation that preserves path, length, and timestamp is the known false-negative boundary. Moving the installation changes its root cache namespace and causes a reparse.
Cache root is %LOCALAPPDATA%/UnityAssetWorkbench/catalog-cache. A SHA-256 hash of normalized game root selects one directory containing catalog.db. Metadata records schema, root, active build fingerprint, display version, and candidate count. A build or schema mismatch deletes and recreates only that root's database.
Schema normalizes Containers, serialized Entries, and Types. Assets stores entry ID, path ID, type ID, eager GameObject name when applicable, byte size, owner file/path IDs, and a derived component flag. Names for other types are resolved directly from the Unity source on inspection. Full source paths, Unity versions, entry names, and type strings are stored once at their natural scope. UI-only computed properties and raw payloads are never persisted.
Workers write 16,384-address transactions under a process-local writer gate while WAL permits concurrent UI reads. One private, non-pooled writer connection remains open for the scan, preserves its prepared/cache state, and closes in a guaranteed completion path. It uses synchronous=NORMAL, a 16,384-page WAL checkpoint interval, and bounded cache/memory-map settings. Prepared commands, per-container type-ID deduplication, single-statement entry upserts, partial owner/top-level indexes, and removal of redundant indexes reduce write amplification. Container rows start incomplete; committed batches remain inspectable after cancellation, but only a final completion marker is eligible for resume. Restarting an incomplete container deletes its previous entry rows before parsing. Warnings and the final asset count share the completion transaction.
Candidate discovery intentionally excludes obvious assemblies, logs, metadata text, and resource sidecars. Known Unity container extensions, extensionless files, globalgamemanagers, and level* remain candidates.
Detection then uses content, not extension alone:
- UnityFS-family ASCII signature means bundle.
AssetsFile.IsAssetsFilemeans loose serialized file.- Unknown candidates are skipped.
Bundle directory entries are tested with IsAssetsFile; non-serialized resource entries remain available for later streamed-resource work but are not indexed as objects in MVP.
Each readable object gets type from Unity class ID. Before field decoding, its isolated worker selects the class database matching Metadata.UnityVersion from an embedded AssetsTools.NET-compatible classdata.tpk. Embedded type trees remain usable, while stripped player files gain the templates required to read GameObject.m_Name and component m_GameObject. Other asset names are intentionally deferred until inspection. Unsupported/custom engine versions fall back without rejecting asset metadata; decode failure only restores the Type #PathId label and prevents automatic component nesting.
- Discovery: O(number of filesystem entries).
- Metadata scan: O(number of containers + number of assets).
- UI text filter: O(number of assets) per debounced change.
- Extraction: O(serialized payload size) per asset.
Name/path substring search currently uses parameterized SQLite LIKE; FTS5 is a future optimization for extremely large catalogs.
Raw extractor copies serialized object bytes. Format-aware handlers reopen source at inspection/export time, avoiding long-lived handles and stale shared parser state.
Implemented class-ID dispatch:
- TextAsset: UTF-8/UTF-16 validation, bounded text/hex preview,
.txtor.binexport. - Texture2D: metadata plus AssetsTools.NET.Texture decode to PNG.
- Sprite: local texture PPtr resolution, decoded texture-rectangle crop, PNG output.
- AudioClip: metadata, embedded/sidecar/bundle resource lookup, detected WAV/Ogg/FLAC/MP3/FSB pass-through, browser playback for supported containers.
- Other types: bounded raw hex preview; raw export remains fallback.
Safety rules:
- Source streams opened explicitly read-only with shared-read access and deterministically disposed.
- Output directory created only after explicit user selection.
- Existing files never overwritten;
_2,_3, and later suffixes are added. - Invalid filename characters are replaced.
- Names capped at 120 characters.
- Payloads above 2 GB rejected until streaming output replaces byte-array buffering.
- Cancellation checked during file loops and payload reads.
- Raw serialized objects use
.rawso output never masquerades as decoded media. - Decoded artifacts are capped at 256 MiB; image export is capped at 67,108,864 pixels.
- UI previews cap generic hex at 64 KiB, text at 512 KiB, playable audio at 8 MiB, PNG payload at 24 MiB, and decoded images at 16,777,216 pixels.
- Catalog exports use temporary files and atomic final moves; cancellation deletes the incomplete temporary file.
Decoded audio currently preserves detected encoded containers rather than transcoding. FSB metadata/export works, but FSB decode does not. Sprite export handles direct texture rectangles; rotated atlas packing is a documented boundary.
Container files are processed by a fixed task pool with bounded concurrency. Maximum concurrency is min(32, max(1, Environment.ProcessorCount - 1)), reserving one logical processor whenever the machine has more than one. Auto additionally caps this value at eight. The UI generates only hardware-valid worker choices, and old persisted values are clamped when loaded.
Each long-lived worker owns one AssetsTools.NET manager for its scan lifetime. Its embedded class package and current Unity-version database remain loaded across container jobs; database selection changes only when the next serialized file reports a different Unity version. Bundles, serialized files, and streams are unloaded between jobs. No parser state crosses worker boundaries. Entries inside a bundle remain sequential because they share decompressed bundle state.
Metadata cataloging deliberately avoids AssetsManager.GetBaseField, which copies the complete serialized object to a temporary stream and materializes a full value tree. A per-file, per-type read plan locates m_Name only for GameObjects and m_GameObject.m_FileID/m_GameObject.m_PathID only for components. A reusable AssetTypeValueIterator walks each relevant object in the original reader and materializes only those target values. Other types skip object-field reads entirely. Metadata results and pending database rows use value types; full UnityAssetRecord objects are allocated only for the bounded live preview and queried UI pages.
Workers stream records into per-container SQLite writers instead of a global collection. Only one 16,384-record compact value batch exists per active worker, and SQLite serializes write transactions through one reused connection. Component candidates are flagged during insertion; container completion only demotes unresolved local owners instead of rewriting every valid component row. Completed worker results contain counts and warnings only. Progress mutation and callbacks are serialized so counts never regress.
MudTable ServerData requests one page of top-level objects with SQL search and type filters. During scanning, the active page reloads at most once every three seconds, uses the monotonic scan count as a temporary unfiltered total, and therefore avoids repeated COUNT(*) passes over a growing multi-million-row catalog. Exact counts and type summaries are calculated after completion or cancellation. Normal pages contain 25–250 UnityAssetRecord instances; changing page releases old records unless explicitly selected. Component rows are queried only after GameObject inspection.
The 250 ms refresh also renders elapsed monotonic time even while one large container produces no progress event. Estimated remaining time uses mean elapsed time per completed container and stays in a calculating state until the first container completes. This is intentionally labeled approximate because container sizes and cache hits vary.
Completed cache markers and warnings are read into a small path-keyed manifest with two SQL queries before workers start. Workers perform in-memory file metadata checks, avoiding one connection, metadata query, and thread-pool hop per candidate. A hit returns only asset count and warnings; it never reconstructs cached assets in memory. Progress counts distinguish resumed containers, while table rows continue coming directly from SQLite.
CancellationToken stops scheduling new containers and is checked between bundle entries and assets. Higher concurrency improves throughput for independent files on fast storage, but compressed bundles can raise peak memory roughly with the number of active workers. One processor is always reserved when possible, Auto is capped at eight, and buffered live table refreshes are limited to four per second. Extraction remains sequential in MVP to preserve predictable output naming and disk behavior.
Single-screen workbench minimizes navigation:
- Hero block establishes purpose and read-only expectation.
- Folder command bar owns browse, scan, cancel, and progress.
- Result table and metrics update during scanning; a live-state banner distinguishes transient ordering from the final catalog.
- Metric cards summarize scan.
- Filter toolbar keeps query/type/export controls close to results.
- Master/detail browser keeps top-level assets and GameObjects in the dense table.
- Sticky object inspector shows complete metadata and, for GameObjects, locally owned component children.
- Server-side hierarchy/type/filter queries return only requested page; no full-catalog projection exists in Blazor.
- Per-row utilities copy exact location or export one raw asset; inspector adds decoded export when supported.
- Preview pane renders metadata, bounded text/hex, PNG, or native HTML audio controls.
- Catalog export menu writes CSV or JSON without blocking renderer thread.
- Warning panel stays collapsed until needed.
Dark theme is default for long inspection sessions. Light theme remains one click away. Layout compresses below desktop width, though minimum native window width is 980 px.
Expected failures are scoped narrowly:
- Unreadable container becomes scan warning; other files continue.
- Unreadable bundle entry becomes entry warning; other entries continue.
- Unreadable asset name becomes blank name; record remains.
- Missing/incomplete container marker becomes a miss and is reparsed.
- Database initialization/write failure stops the scan because SQLite is catalog authority; no unbounded in-memory fallback exists.
- Build/schema mismatch replaces only selected root's catalog database.
- Bulk extraction catches per-asset failure and returns summary.
- Top-level fatal exceptions surface through native PhotinoEx message dialog.
Raw error text is user-visible today. Later versions should add structured error codes, logs, and copy-diagnostics action.
Required by product brief. Source dependency is pinned because requested upstream publishes no documented package and currently requires .NET 10. Pin makes builds reproducible. Host-specific calls remain inside Desktop.
Risk: upstream README says not to use it in production and points to PhotinoXDX. Mitigation: no Core or Infrastructure reference to PhotinoEx; migration stays localized.
Pinned revision also leaves custom-scheme response MemoryStream.Position at EOF after copying page content. Repository carries and bootstraps a minimal cursor-reset patch. Regression test asserts response begins at position zero and retains HTML bytes. WebView2 also begins its own about:blank navigation after controller creation; PhotinoEx's immediate app navigation loses that race with ConnectionAborted. Second local patch defers app navigation until initial navigation completion fires on WebView UI thread.
Windows host also passed a temporary WndProcDelegate to native code. GC could collect it while window remained active, terminating process on next callback. Local patch stores delegate for WinPhotinoEx lifetime.
Provides mature interactive Blazor components, theme system, tables, progress, selection, and notifications without separate JavaScript UI framework.
Small focused C# library capable of reading Unity serialized files and bundles. Chosen for indexing, raw extraction, and on-demand object field access. AssetsTools.NET.Texture 3.0.2 plus its decoder dependency handles Texture2D/Sprite PNG output; unsupported texture formats degrade to metadata instead of failing inspection. Its assembly references StbImageSharp 2.27.13 and StbImageWriteSharp 1.16.7 without declaring them in its NuGet dependency graph, so Infrastructure pins both directly to guarantee desktop output copying.
AssetsTools.NET's documented stripped-file flow requires loading classdata.tpk and selecting a database by the serialized file's Unity version. Infrastructure embeds the 289,605-byte package distributed with the official UABEA v8 Windows release (SHA-256 129e1f80f930415db6779fe6089afa75280cb51462bcee812beab6cd81a764c6). Every worker manager loads its own package/database state and reuses it across containers, preserving the no-shared-parser-state concurrency rule without repeated package decompression. Package/database selection failure is a metadata miss, not a container failure.
Microsoft.Data.Sqlite 10.0.10 provides the ADO.NET boundary. Its native bundle is explicitly raised to SQLitePCLRaw.bundle_e_sqlite3 3.0.5 so restore does not select the vulnerable 2.1.11 transitive dependency. Package audit is part of verification.
SQLite ADO.NET async methods execute synchronously, so initialization, manifest reads, and UI queries move complete database operations to worker-pool tasks. Scan workers already run off the renderer and call the synchronous writer directly, avoiding nested thread-pool scheduling for every container. WAL enables a scan-lifetime private writer connection and short-lived read-only UI connections to overlap without shared-cache conflicts. Connection-local durability and cache pragmas are applied by the connection factory rather than only during schema setup.
Game data is untrusted binary input. Parser exceptions are caught at file/entry and preview boundaries. No game-provided scripts execute. App does not load managed assemblies from target game. Output paths derive from user-selected directory and sanitized filenames.
Preview dimensions, encoded payloads, audio, text, hex, and decoded artifacts have explicit allocation caps. MonoBehaviour support must inspect assemblies as metadata only, never load them into default runtime context.
Original project-owned source, documentation, and assets use Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC-BY-NC-ND-4.0). Unmodified noncommercial redistribution requires attribution. Adapted material may be produced privately for noncommercial use but may not be shared under this license.
LICENSE contains canonical legal code. NOTICE identifies project attribution and scope. Third-party dependencies, vendored PhotinoEx source, embedded Unity class data, trademarks, and extracted game assets are excluded and retain their respective rights.
.github/workflows/ci-release.yml runs Windows restore, Release build, all tests, and a transitive NuGet vulnerability audit for pull requests and pushes to main. It uses the pinned SDK from global.json and runs the same PhotinoEx bootstrap used by developers.
Tags matching v* enter a gated release job after CI succeeds. The job validates strict semantic-version syntax and verifies that the tagged commit is reachable from origin/main. It then performs a RID-specific restore, publishes a self-contained win-x64 application, includes project notices, creates a ZIP and SHA-256 sidecar, retains both as workflow artifacts, and creates a GitHub release with generated notes. Prerelease suffixes mark the GitHub release as a prerelease. A rerun replaces assets on an existing release.
The release job alone receives contents: write; all other jobs remain read-only. GitHub's scoped workflow token is used by the preinstalled GitHub CLI, so no long-lived repository secret is required.
- Desktop shell
- Native folder selection
- Loose assets and bundle indexing
- Search/type filters
- Single/bulk raw extraction
- Cancellation, warnings, settings, tests, docs
- TextAsset decoded preview/export
- Texture2D and Sprite preview/PNG
- Audio metadata and supported decode
- Hex/text inspector
- CSV/JSON catalog export
- PPtr reference traversal
- Inbound/outbound dependency graph
- MonoBehaviour schema resolution
- Addressables names and groups
- Search by field value
- Workspace copies and backups
- Asset replacement
- Bundle repack
- Binary diff and patch packages
- Explicit validation and restore flow
- 2026-08-03: Started empty workspace with three-layer architecture plus tests.
- 2026-08-03: Pinned requested PhotinoEx source at
b946e2a5c0a0aa734901056f39a96908c045440f. - 2026-08-03: Selected AssetsTools.NET for first reader/extractor.
- 2026-08-03: Kept MVP source handling strictly read-only.
- 2026-08-03: Defined extraction as raw serialized payload with unique output names.
- 2026-08-03: Initially used immutable in-memory snapshot; later replaced after real catalog scale exposed its limits.
- 2026-08-03: Fixed blank PhotinoEx window by rewinding copied custom-scheme response streams before WebView2 consumes them.
- 2026-08-03: Added bounded per-container parallel scanning with isolated parser state, deterministic aggregation, and persisted worker control.
- 2026-08-03: Added batched live-result delivery so parsed assets appear before the scan completes.
- 2026-08-03: Added version-keyed atomic container caches for unchanged-build reuse and cancelled-scan resume.
- 2026-08-03: Removed per-batch renderer-context callbacks; added worker-side progress coalescing and a bounded 2,000-asset live preview drained every 250 ms.
- 2026-08-03: Added
m_GameObjectrelationship indexing, nested component inspection, a responsive master/detail browser, and cache schema v2 to prevent reuse of flat v1 records. - 2026-08-03: Embedded a versioned Unity class package, selected per-file databases in every scan worker, and advanced cache schema to v3 so real GameObject names replace cached path-ID placeholders.
- 2026-08-03: Replaced per-object full value-tree decoding with reusable streaming metadata iterators, reused one parser/class package per fixed worker, removed concurrent per-asset aggregation, and stopped live-batch allocation after preview saturation to reduce GC pressure.
- 2026-08-03: Added elapsed and approximate remaining-time indicators to scan progress, refreshed by the existing bounded UI timer.
- 2026-08-03: Replaced verbose per-container JSON and full in-memory snapshots with normalized SQLite, 512-row streaming commits, current-build replacement, server-paged SQL filtering, and lazy GameObject component queries.
- 2026-08-03: Accelerated SQLite ingestion with 4,096-row commits, correctly scoped normal synchronization, prepared statements, type deduplication, reduced index/update amplification, and tuned WAL checkpoints.
- 2026-08-03: Added multi-million-row scan mode: 16,384-row compact value batches, one reused writer connection, partial indexes, bulk resume-manifest loading, lazy non-GameObject names, fast live paging hints, and three-second database refresh throttling.
- 2026-08-03: Added bounded on-demand TextAsset/hex inspection, Texture2D and Sprite PNG previews/exports, AudioClip metadata/playback/pass-through export, and streaming CSV/JSON catalog export.
- 2026-08-03: Added explicit StbImageSharp/StbImageWriteSharp runtime dependencies and a real PNG-encoder regression test after first image inspection exposed the incomplete AssetsTools.NET.Texture package dependency graph.
- 2026-08-03: Licensed original project materials under CC BY-NC-ND 4.0, with canonical legal code and explicit third-party exclusions.
- 2026-08-03: Added Windows CI and gated
v*release automation with main-ancestry validation, self-contained ZIP packaging, checksums, generated release notes, and least-privilege token permissions.