Files
dhkatzandClaude Opus 5.5 34330046e4
CI / test (push) Successful in 1m6s
Choose the hub server in Settings; nightlies use staging
The T5DE Hub section gets a Server choice (T5DE, Staging, Custom), used from the next start. Without one,
the install's channel decides: nightlies and dev builds use hub.staging.t5de.dev, releases hub.t5de.dev.
The client passes the channel to the proxy, which keeps each hub's sign-in and product IDs apart; that
needs proxy 0.2.0, now pinned.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 09:17:34 -08:00

23 KiB

t5de

Patch workflow for the IMVU Classic client (Windows, py2exe, Python 2.7). Rewrite of the original t5de project, now hosted at git.t5de.dev. Later goals: a proxy backend for IMVU's servers, and possibly a client rewrite on a modern stack.

Rules

  • Never commit IMVU's code: no decompiled source, extracted UI files, or client binaries. work/ and .cache/ are gitignored for this reason. Patches in patches/ are diffs and are fine to commit.
  • Releases are bring-your-own-client: users run the patcher on their own copy; never redistribute the full client.
  • Never commit secrets (VPN configs, keys, tokens).
  • Test with a throwaway IMVU account, never a real one.

How it works

  • t5de setup downloads the installer pinned in t5de.toml (sha256-checked), extracts it with 7-Zip, decompiles the first-party modules in library.zip (imvu, main, im, Ext) with uncompyle6, and extracts ui/chrome/imvuContent.jar. The result becomes the base commit of the git repo work/ (library/, imvuContent/), and patches/*.patch are applied on top with git am --3way, in filename order, with a fixed committer (so it works without a git identity, and applied commits hash identically everywhere).
  • t5de save exports every commit after base with git format-patch, using flags that make the output deterministic, so unchanged commits re-export byte-identically.
  • The base must be identical on every machine: uncompyle6/xdis are pinned, output is normalized (base.py), and the base commit has a fixed author and date. t5de.toml's base pins the expected commit (554.8: 5b8137f), and setup fails if the decompiled base differs; CI then prints the base's per-file blob hashes to compare. Normalizing drops uncompyle6's # Compiled at: header, which is in the decompiling machine's local time zone (bump NORMALIZE_VERSION when normalization changes). Windows paths over 260 characters break setup (deep jar paths), so keep the checkout in a short path.
  • Tests: pytest (unit tests; tests/py27/ scripts run under Python 2.7 for splicing and for checks on a real build). T5DE_REQUIRE_ALL=1 turns skips for missing Python 2.7 or build into failures. CI (.gitea/workflows/ci.yml, Gitea 1.25) builds Python 2.7 from source (cached), runs setup, build and all tests; never upload the build (it's IMVU's client). imvu-update.yml runs t5de check-update daily and opens an issue. tools/drive.py drives the built client without a person (start, screenshot, click, type, imvu: commands such as showMode?mode=shop sent as WM_COPYDATA, stop); the test account's saved login signs in by itself.
  • Patches carry metadata as commit trailers: T5de-Id (lowercase-words), T5de-Group (the installer's section: general, shop, rooms, avatars, chat, renderer for optional patches; core and privacy, which need T5de-Required: true, are listed together as "Always Included"), optional T5de-Depends: id, id, optional T5de-Name (a few words in Title Case, shown in the installer; the user wants short UI text, grouped by area, not one long list). Subject = title, body = description (shown on hover). bundle.py validates them.
  • t5de bundle writes build/bundle/manifest.json (format 1): per patch, its metadata, its compiled overrides per library module, and its UI changes (byte-offset edits with the exact old bytes, or added/removed files); core operations applied with every selection (remove devicefingerprint.exe and npfpbase_vc10.dll, create ui/profile (empty in a real install, so 7-Zip skips it; the client crashes on startup without it), and rewrite the QualityAgent's crash report host to 127.0.0.1); and sha256s of the client files modified. It holds no IMVU files. Each patch is compiled on its own (splice27.py, Python 2.7, comparing the module before and after the patch's commit), so users can choose patches: a patch that changes a definition or UI file an earlier patch changed, or uses a definition one added (same module), must depend on it.
  • splice27.py compiles only changed/added functions and methods (no -O: the decompiler shows explicit raise AssertionError as assert); methods are compiled in a same-named class body (private names mangle right) and copied onto the original class. Module- and class-level changes, removals, and decorators that use class-body names are errors; import-time references to changed functions (aliases, from M import f, star imports) are warnings. Replaced definitions stay reachable as _t5de_original_<name> (class attribute for methods, module global for functions), always IMVU's original (later patches don't overwrite it), so patches can wrap them. It warns when a replaced definition's decompiled base doesn't recompile to IMVU's bytecode (e.g. Network.urlopen), unless the patch calls its original. Overrides are marshalled with version 0 (no interning).
  • patcher/ (Go; t5de-patcher apply|list) applies a bundle to a client folder: verifies it (every file against IMVU's checksum.txt, and the bundle's sha256s), copies it, splices library.zip (each patched module's code: a small code object, written by the patcher in marshal version 0, that execs the original module code, embedded verbatim and first in the stream so its interned-string refs stay valid, then each selected patch's overrides in order), applies UI changes to the jar, applies core, recomputes checksum.txt, and writes t5de-install.json (it only replaces folders with that record). Archives keep entry order and metadata; both are uncompressed.
  • t5de build = t5de bundle, then the patcher (go run, or $T5DE_PATCHER) applying every patch to the cached install into build/IMVUClient/. Builds are byte-reproducible.
  • Patches can add t5de's own files under client/ in work/ (never IMVU's); the bundle carries them per patch (files) and the patcher writes them into the install. Two patches writing the same file need a dependency. Programs built by T5DE's other repositories are pinned in t5de.toml ([[programs]]: a release's download folder and each file's sha256); t5de bundle downloads them (cached in .cache/programs/<sha256>/) into their patch's files. The renderer (t5de-renderer patch) comes this way: to update it, tag a vX.Y.Z release in t5de-renderer (its CI publishes it) and copy the url and sha256s from the release notes.
  • Window icons: Python dialogs load imvu.ico from the program folder (win32/win32window.py, which isn't first-party, so not in work/); the native main window (SceneWindow.dll) loads imvu.ico from the working directory. imvu.ico isn't in checksum.txt (the installer creates it). The t5de-branding patch writes T5DE's icon as client/imvu.ico, sets the main window's icon explicitly, retitles windows "T5DE", and adds a login badge. TopLevelWindow.title is a property bound at class-creation time, so it's changed by wrapping setActiveMode, not set_title. Windows started from a shortcut get the shortcut's icon on the taskbar, and IMVUClient.exe's icon can't be extracted (blank), so shortcuts and the uninstall entry use imvu.ico.
  • Reporting endpoints share hosts with the API (client-dynamic, secure, www.imvu.com), so they're blocked by URL in Network.urlopen/post_multipart (patch 16), which logs each refusal as t5de: blocked request to. Only bugs.imvu.com is reporting-only, so OS-level blocking (hosts/firewall) can't do much more.
  • The proxy (t5de-proxy patch; program from git.t5de.dev/t5de/t5de-proxy, checked out beside this one): every HTTP request the client makes goes through Network.urlopen, the embedded browser's too (IMVU's PythonProtocolHandler in imvugecko.dll replaces Gecko's http/https and calls Network.performGeckoRequest). The patch starts t5de-proxy.exe on first use (main/client.py t5deProxy; ready file %TEMP%\t5de-proxy-<pid>.json, log %TEMP%\t5de-proxy.log, T5DE_PROXY_TRACE=1 for full URLs and IMQ frames), adds a urllib2 handler sending absolute-form requests with its token, and puts a CONNECT strategy (plaintext to the proxy, TLS from it) first for IMQ. A refused connection to the proxy, or no exe, means direct connections as before. Installs wait for t5de-proxy.exe and t5de-renderer.exe to exit too. The proxy also serves community products from local packs (%APPDATA%\T5DE\packs, T5DE_PACKS; t5de-proxy's README, t5de-api's docs/pack.md); the client needs no patch for it. To test one: drive.py url "imvu:PutOn?productId=<local ID>" (local IDs from 2,000,000,000, in %APPDATA%\T5DE\proxy\products.json), and Dress Up for outfit saves. The proxy also gets T5 products from the hub's catalogs into the person's library and the client's inventory, and answers http://t5de-proxy.local/ (its own control address, needing its token): the t5de-hub patch's Settings > T5DE > T5DE Hub section signs in and out through it (SettingsGeckoListener.handle_t5deHub). Its Server choice (T5DE, Staging, Custom) is the proxy's, from the next start: without one, the install's channel decides (t5deChannel, from t5de-install.json, passed as -channel): nightlies and dev builds (including build/IMVUClient) use hub.staging.t5de.dev, releases hub.t5de.dev; T5DE_HUB overrides both. The proxy keeps each hub's sign-in and product IDs apart (t5de-proxy's README). The client passes -channel since proxy 0.2.0; older proxies refuse it and the client connects directly. The t5de-shop patch adds a T5 choice to the shop's rating filter (for everyone; T5 filter images in shop/img/filter*t5_*.png, the T5 badge in widgets/product/img/t5.png), answered by the proxy from the hub; T5 products are local IDs from 2,000,000,000 (Product.isT5), free, and buying saves to the library. The t5de-inventory patch does the same in the clothing inventory (tool/inventory); the T5 choice isn't saved: IMVU's inventoryMaturity preference only takes ga/all/ap/applus, and any other value raises an error that ends the client. t5de-outfits marks outfits with T5 products (outfits tool, outfit card); t5de-people-badges shows the T5 badge on people who opted in on the hub (avatar cards, friends, Find People, Get Matched) via IMVU.t5deBadge (js/imvu.js), which batches lookups into one async t5deBadges call (StandardGeckoListener.handle_t5deBadges, a threadtask asking the proxy's /badges). t5de-rooms badges rooms hosted by T5DE's hub in the room list (owner 2100000000; the proxy serves them, its internal/hosted). t5de-instances: T5DE_INSTANCE=<name> runs a second client beside the first with its own data (%APPDATA%\T5DE-<name>), settings and sign-in; drive.py --instance 2 start|... drives it. Screenshots and typed input need the VM's screen shown (Remote Desktop not minimized). t5de build installs the pinned proxy; to test an unreleased one, copy t5de-proxy's dist/t5de-proxy.exe into build/IMVUClient/ after building.

Verified facts (client 554.8)

  • The NSIS installer extracts with 7-Zip byte-identically to a real install (all files in checksum.txt match), so CI can run on Linux without running the installer.
  • checksum.txt holds decimal CRC32s and paths; the old patcher removed the library, imvuContent and devicefingerprint lines. The build should recompute it.
  • Bytecode is Python 2.7 (magic 62211); 357 first-party modules. imvu/tool/RadioTool.py fails to decompile and is stubbed.
  • The decompiler is deterministic but not faithful: recompiling decompiled code gives identical bytecode for only ~60% of modules, and 6 don't compile at all. So never ship whole recompiled files. The 6 (mostly mangled string escapes): imvu/bugreport/__init__.py, imvu/geography.py, imvu/translation.py, imvu/task/util.py, imvu/platform/windows.py (null bytes), main/login.py. Patches avoid them.
  • IMVUQualityAgent.exe launches and restarts the client, and on a crash it sends GET /catalog/imvu_monitor_submit.php?cid= to www.imvu.com itself (raw WSOCK32), out of reach of Python patches.
  • npfpbase_vc10.dll is a BlueCava device-fingerprinting browser plugin that the embedded Gecko can load.
  • Patch 1 also filters Creator Mode facts; patch 14 drops all facts. The filter is kept so patch 1 stands alone.
  • imvu.expectModules (called from the exe's boot script) rejects unexpected modules loaded at startup; --no-expected-module-check bypasses it.
  • The UI is XULRunner/Gecko; IMVU's own JS in the jar isn't minified.
  • The 3D scene is locked to 30 FPS: SceneWindow.dll's main loop (runApp) waits out zero.dll's millisecondsPerFrame (33), then stepFrame advances the scene by the 33 ms ticks since the last frame (FrameIntervalMaker::getFrameInterval, at most 5; the tick is also a float 33.0 in zero.dll) and draws once. Animations are 30-per-second frame sequences advanced one whole frame per tick (ActionInstance::step calls incrementActionFrame per tick); camera smoothing (FixedRateInterpolatedValue) and particles step per tick too. So shorter ticks speed animations up (tried and dropped: a patch shrinking the tick made them run 2x at 60 FPS), and drawing more often without shorter ticks shows the same scene twice. T5DE's renderer does high frame rates (the Frame Rate setting) by drawing between ticks, with animations, crossfades, the camera and particles at the time between them, through checked hooks in its adapter (t5de-renderer's docs/architecture.md; no added delay, nothing predicted). The user wanted no added delay, so blending finished frames was ruled out. Native changes are made in memory from Python (ctypes, checking the instructions first) or as our own source code, never as opaque binary edits: the user wants them readable.
  • Renderers (Settings > Advanced): "Direct3D" is Direct3D 9 inside SceneWindow.dll (RenderEngineDx); "OpenGL" is the plugin glRenderEngine.dll (exports only createRenderEngine, isRenderEngineSupported; interface PiRenderEngine, with readable C++ symbol names in SceneWindow.dll); "Standard" is the Direct3D 9 engine on SwiftShader (swiftshader.dll, CPU). D3D11 and OpenGLSM2 exist only as Python enum values. The user wants a modern renderer (the next big phase; likely its own repo, C++), taking the plugin's place or as another choice, with higher frame rates (interpolated animation) and newer texture and lighting features like the newer WASM client's (written by us, not copied; texture features depend on what product data the servers send Classic). The renderer is its own repository, t5de-renderer (git.t5de.dev/t5de/t5de-renderer, checked out beside this one; its docs/interface.md has the findings). Ghidra 12.2 (needs Java 25) is at %LOCALAPPDATA%\Programs\ghidra_12.2_PUBLIC; its projects live in %LOCALAPPDATA%\t5de-dev\ghidra (Ghidra rejects paths with a dot-folder like .cache, and they hold IMVU's code, so never in a repository).

Next

  • The build runs on a real client (launched directly, without IMVUQualityAgent.exe), sends no reporting requests, and the user has checked the patches' features.
  • End-user release: T5DE-Setup.exe, the Go patcher with a Wails wizard and the bundle embedded (no IMVU files; bring-your-own-client), per-user (no admin). Wizard: welcome; find IMVU Classic (HKCU\Software\IMVU default value = install folder) and verify it; choose patches (core/privacy shown locked); options (install folder, default %LOCALAPPDATA%\T5DEClient, shortcuts); install (apply, shortcuts, "Apps & features" uninstall entry under HKCU); finish. Re-running offers modify/repair/update/uninstall via t5de-install.json. Shortcuts launch IMVUClient.exe directly (the agent is untested). Done: t5de-patcher install|uninstall (internal/install; Windows parts behind a System interface, faked in tests, real one tested on Windows only). Install keeps t5de-setup.exe and t5de-bundle.json in the install, records shortcuts in t5de-install.json, registers HKCU\...\Uninstall\T5DE (NoModify/NoRepair; the setup does those). Uninstall run from inside the install relaunches from a %TEMP%\t5de-uninstall-<pid>.exe copy, which deletes itself afterwards; -remove-data also removes HKCU\Software\T5DE and %APPDATA%\T5DE. Reinstalling replaces the folder, including the browser profile (cookies). Verified end to end on a real install. Done: cmd/t5de-setup, the wizard (Wails v2 with a plain HTML/CSS/JS frontend/, no npm; build tags desktop, production, -H=windowsgui; Linux CI vets and cross-compiles it for Windows only). Modes: install; manage (when an install exists, or run from the copy in one: change patches, repair/update, uninstall); uninstall (what "Apps & features" runs). Run from inside an install, it relaunches from a temporary copy. Bundle: embedded from cmd/t5de-setup/bundle/manifest.json (gitignored; copied in by release builds), else -bundle, else the install's t5de-bundle.json. T5DE_SETUP_SELFTEST=<file> makes the page report what it rendered and close. Look: IMVU Classic's dialog palette (imvuContent/dialogs/css/imvu_jquery_dialog.css: dark gray #383838, gold #ffe795->#ffd200 buttons with black text, gray #616161->#3e3e3e secondary), always dark. Copy is terse (the user dislikes wordy titles and descriptions). Wails loads the window icon from icon resource #3, so the setup's resources come from winres/winres.json (go-winres make), not simply (which uses #1). Logo and icon: patcher/assets (the user's); both exes link rsrc_windows_amd64.syso (icon, version info, manifest), generated by go-winres v0.3.3 via go generate ./cmd/... (reproducible). Done: release CI. ci.yml builds dist/T5DE-Setup.exe (bundle embedded, version via -X .../internal/buildinfo. Version and go-winres --product-version/--file-version), uploads it as a run artifact, and .gitea/scripts/release.py publishes it with the job token: vX.Y.Z tags make release X.Y.Z; pushes to main replace the nightly pre-release (version X.Y.Z-nightly.N+commit, N commits after the last release tag). The version is shown in the setup and "Apps & features", and recorded in t5de-install.json (version). Code signing: postponed by the user.
  • Diagnostics (internal/diagnostics): anonymous usage statistics to the user's self-hosted Aptabase (its /api/v0/event API) and crash reports to their Bugsink (Sentry envelopes, hand-built), only from builds with the CI secrets APTABASE_HOST, APTABASE_APP_KEY, BUGSINK_DSN (embedded via -X .../buildinfo.*), and only with consent: HKCU\Software\T5DE\Diagnostics (UsageStatistics, CrashReports DWORDs; unset is no). The user wants both on by default: the setup's "Help improve T5DE" page (install and change flows) starts ticked unless a choice is recorded, and records the choice. The setup sends setup_<action>, setup_failed (plus a report) and setup_uninstall events with patch.<id> properties. The client sends through the install's t5de-setup.exe track|report (headless, helper.go, which adds the install's IMVU version and patches), since its Python 2.7 can't do modern HTTPS: the t5de-diagnostics patch sends launch (wrapping clientapp.runMain) and reports crashes, unrecoverable errors and startup failures (wrapping clientapp.reportCrash, reportUnrecoverableError and client._main): type, message and stack only, Windows user name scrubbed. Its helpers are in main/client.py (no module-level imports, so they work when startup fails). Outside an install (e.g. build/IMVUClient) there's no t5de-setup.exe, so nothing is sent. The t5de-settings patch adds a T5DE page to the client's Settings (imvuContent/settings/t5de.js, handlers handle_t5deGetDiagnostic/handle_t5deSetDiagnostic on SettingsGeckoListener, via _winreg). The page is grouped into sections like IMVU's pages (a subsection with an <h1> each, Title Case): Diagnostics; the t5de-renderer patch adds Graphics above it (Frame Rate, t5de-graphics.js, HKCU\Software\T5DE\Graphics\FrameRate: 30, the default, 60, 120, 240, 360, or 0 for uncapped; read live by the renderer's adapter). IMVU's PersistentPrefStore.deleteAllSettings deletes every subkey of Software\T5DE, including Diagnostics, which then means no (fail-safe). Still to do: native crashes (minidumps, imvu/bugreport/windows.py).
  • Self-updates (internal/update, the t5de-updates patch): an install follows its channel (the user's choice): release builds are offered the latest release (/releases/latest), nightlies the nightly release (its version is in the name, "Nightly "), dev builds nothing. runClientApp (after the splash: anything imported before it trips imvu.expectModules, which kills startup) starts t5de-setup.exe check-update -out %TEMP%\t5de-update.json without waiting; IMVU's updateClientIfNeeded (called before and after login) then offers it once per run in IMVU's own update dialog (titled "T5DE Update Available"). "Update Now" runs t5de-setup.exe download-update -dir <install> and exits: it downloads T5DE-Setup.exe (sha256 from the release notes) to a self-deleting temp name and starts it as update -dir <install>, a wizard mode that keeps the install's patches, shortcuts and diagnostics choice, waits for the client to close (install.waitForClient: the exe can't be opened for writing while running), and goes straight to installing unless something needs asking. Tested end to end with a fake release server. Windows won't remove a folder that's any process's working folder: programs started from an install (the update's setup, relaunched copies) get cmd.Dir = os.TempDir(), and replacing an install empties its folder rather than removing it, deleting the record last (apply.prepareOut). A first real update failed this way: every file deleted, the folder kept, then refused as "not a client t5de patched".
  • Forks of the old GitHub repo (surveyed 2026-10-27): only one has new patches, apparently AI-generated and untested. Ideas worth redoing from scratch: a shop "Try on" button (imvu.call('tryOn', pid) in shop/ShopMode.js) and a creator product search (its api.imvu.com endpoints are unverified).
  • Contributor setup: revisit a single bundled t5de executable once the Go patcher exists. Findings so far: msiexec /a extracts python.org's 2.7.18 MSI unelevated into a working portable Python 2.7 (tests pass with it), and contributors who have the pinned client installed could skip the installer download and 7-Zip.
  • Installs keep the exe named IMVUClient.exe (imvu/program/windows.py finds the agent by replacing that name). The patched client's user data is %APPDATA%\T5DE, settings HKCU\Software\T5DE (patch 15).