From 57828cff4425c06dcfc904d0c59690e7736a1353 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 28 Apr 2026 18:05:10 +0100 Subject: [PATCH 01/13] First pass on supporting multiple vf versions --- package-lock.json | 35 ++++-------- package.json | 2 +- src/logic/Decompiler.ts | 2 +- src/logic/{vf.ts => vineflower/vf-1.11.2.ts} | 14 +++-- src/logic/vineflower/vineflower.ts | 57 ++++++++++++++++++++ src/types/vf-runtime.d.ts | 8 ++- src/workers/decompile/client.ts | 2 +- src/workers/decompile/worker.ts | 2 +- 8 files changed, 83 insertions(+), 39 deletions(-) rename src/logic/{vf.ts => vineflower/vf-1.11.2.ts} (65%) create mode 100644 src/logic/vineflower/vineflower.ts diff --git a/package-lock.json b/package-lock.json index c4bf322..66b83a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "@katana-project/zip": "^0.7.1", "@monaco-editor/react": "^4.7.0", - "@run-slicer/vf": "^0.5.0-1.11.2", "@xyflow/react": "^12.10.1", "antd": "^6.3.2", "comlink": "^4.4.2", @@ -19,7 +18,8 @@ "monaco-editor": "^0.55.1", "react": "^19.2.4", "react-dom": "^19.2.4", - "rxjs": "^7.8.2" + "rxjs": "^7.8.2", + "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2" }, "devDependencies": { "@playwright/test": "^1.58.2", @@ -162,7 +162,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3060,12 +3059,6 @@ } } }, - "node_modules/@run-slicer/vf": { - "version": "0.5.0-1.11.2", - "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.5.0-1.11.2.tgz", - "integrity": "sha512-8+otfmuAuuEl1bo6LPp+qeR2TpY5pCccLrONK6/reJpCWdrU+g9++j4Eec1fqqv/DRX7nB79Xp9ZwaETR1mhXA==", - "license": "Apache License 2.0, MIT" - }, "node_modules/@sindresorhus/is": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", @@ -3262,7 +3255,6 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3417,7 +3409,6 @@ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -3428,7 +3419,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3772,7 +3762,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3979,7 +3968,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -4042,8 +4030,7 @@ "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -4153,7 +4140,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4715,7 +4701,6 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", - "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -4827,7 +4812,6 @@ "integrity": "sha512-4RuJK2jP08XwqtUu+5yhCbxEauCm6tv2MFHKEMsjbosK2+vy5us82oI3VLuHwbNyZG7ekZA26U2LLHnGR4frIA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "tsgolint": "bin/tsgolint.js" }, @@ -4982,7 +4966,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4992,7 +4975,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5294,7 +5276,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5326,7 +5307,6 @@ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pathe": "^2.0.3" } @@ -5371,13 +5351,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/vf-1.11.2": { + "name": "@run-slicer/vf", + "version": "0.5.0-1.11.2", + "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.5.0-1.11.2.tgz", + "integrity": "sha512-8+otfmuAuuEl1bo6LPp+qeR2TpY5pCccLrONK6/reJpCWdrU+g9++j4Eec1fqqv/DRX7nB79Xp9ZwaETR1mhXA==", + "license": "Apache License 2.0, MIT" + }, "node_modules/vite": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", @@ -5597,7 +5583,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, diff --git a/package.json b/package.json index b45ecf3..e2b9fd5 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "dependencies": { "@katana-project/zip": "^0.7.1", "@monaco-editor/react": "^4.7.0", - "@run-slicer/vf": "^0.5.0-1.11.2", + "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", "@xyflow/react": "^12.10.1", "antd": "^6.3.2", "comlink": "^4.4.2", diff --git a/src/logic/Decompiler.ts b/src/logic/Decompiler.ts index afd4970..e2fe7fb 100644 --- a/src/logic/Decompiler.ts +++ b/src/logic/Decompiler.ts @@ -6,7 +6,7 @@ import { import { minecraftJar, type MinecraftJar } from "./MinecraftApi"; import { selectedFile } from "./State"; import { bytecode, displayLambdas } from "./Settings"; -import type { Options } from "./vf"; +import type { Options } from "./vineflower/vineflower"; import type { DecompileResult } from "../workers/decompile/types"; import * as worker from "../workers/decompile/client"; import type { Jar } from "../utils/Jar"; diff --git a/src/logic/vf.ts b/src/logic/vineflower/vf-1.11.2.ts similarity index 65% rename from src/logic/vf.ts rename to src/logic/vineflower/vf-1.11.2.ts index adc596e..8a259c2 100644 --- a/src/logic/vf.ts +++ b/src/logic/vineflower/vf-1.11.2.ts @@ -1,8 +1,6 @@ -import wasmPath from "@run-slicer/vf/vf.wasm?url"; -import { load } from "@run-slicer/vf/vf.wasm-runtime.js"; -import type * as vf from "@run-slicer/vf"; - -export type * from "@run-slicer/vf"; +import wasmPath from "vf-1.11.2/vf.wasm?url"; +import { load } from "vf-1.11.2/vf.wasm-runtime.js"; +import type * as vf from "vf-1.11.2"; let runtime: typeof vf | null = null; let runtimePreferWasm = true; @@ -10,7 +8,7 @@ let runtimePreferWasm = true; export async function loadRuntime(preferWasm: boolean) { if (!runtime || runtimePreferWasm !== preferWasm) { runtimePreferWasm = preferWasm; - console.log(`Loading VineFlower ${preferWasm ? "WASM" : "JavaScript"} runtime`); + console.log(`Loading VineFlower 1.11.2 ${preferWasm ? "WASM" : "JavaScript"} runtime`); let loadJs = !preferWasm; if (preferWasm) { @@ -25,12 +23,12 @@ export async function loadRuntime(preferWasm: boolean) { } if (loadJs) { - runtime = await import("@run-slicer/vf/vf.runtime.js"); + runtime = await import("vf-1.11.2/vf.runtime.js"); } } } -export const decompile: typeof vf.decompile = async (name, options) => { +export const decompile: typeof vf.decompile = async (name: string | string[], options?: vf.Config) => { if (!runtime) throw "No runtime loaded"; return await runtime.decompile(name, options); }; diff --git a/src/logic/vineflower/vineflower.ts b/src/logic/vineflower/vineflower.ts new file mode 100644 index 0000000..4e792be --- /dev/null +++ b/src/logic/vineflower/vineflower.ts @@ -0,0 +1,57 @@ +const DEFAULT_VERSION = "1.11.2"; +export type Version = "1.11.2"; + +export async function loadRuntime(preferWasm: boolean, version: Version = DEFAULT_VERSION) { + if (version === "1.11.2") { + const vf1112 = await import("./vf-1.11.2"); + return await vf1112.loadRuntime(preferWasm); + } + + throw new Error(`Unsupported Vineflower version: ${version}`); +} + +export async function decompile(name: string | string[], options?: Config, version: Version = DEFAULT_VERSION) { + if (version === "1.11.2") { + const vf1112 = await import("./vf-1.11.2"); + return await vf1112.decompile(name, options); + } + + throw new Error(`Unsupported Vineflower version: ${version}`); +} + +// Abstracted types from @run-slicer/vf to support multiple versions. +export type Options = Record; + +export interface TokenCollector { + start: (content: string) => void; + visitClass: (start: number, length: number, declaration: boolean, name: string) => void; + visitField: (start: number, length: number, declaration: boolean, className: string, name: string, descriptor: string) => void; + visitMethod: (start: number, length: number, declaration: boolean, className: string, name: string, descriptor: string) => void; + visitParameter: (start: number, length: number, declaration: boolean, className: string, methodName: string, methodDescriptor: string, index: number, name: string) => void; + visitLocal: (start: number, length: number, declaration: boolean, className: string, methodName: string, methodDescriptor: string, index: number, name: string) => void; + end: () => void; +} + +export type LogLevel = "trace" | "info" | "warn" | "error"; + +export interface Logger { + writeMessage: (level: LogLevel, message: string, error?: unknown) => void; + startProcessingClass?: (className: string) => void; + endProcessingClass?: () => void; + startReadingClass?: (className: string) => void; + endReadingClass?: () => void; + startClass?: (className: string) => void; + endClass?: () => void; + startMethod?: (methodName: string) => void; + endMethod?: () => void; + startWriteClass?: (className: string) => void; + endWriteClass?: () => void; +} + +export interface Config { + source?: (name: string) => Promise; + resources?: string[]; + options?: Options; + tokenCollector?: TokenCollector; + logger?: Logger; +} \ No newline at end of file diff --git a/src/types/vf-runtime.d.ts b/src/types/vf-runtime.d.ts index 00f472f..42a54c0 100644 --- a/src/types/vf-runtime.d.ts +++ b/src/types/vf-runtime.d.ts @@ -1,10 +1,14 @@ -declare module "@run-slicer/vf/vf.wasm-runtime.js" { +declare module "vf-1.11.2/vf.wasm-runtime.js" { export function load( wasmPath: string, options?: { noAutoImports?: boolean; } ): Promise<{ exports: typeof import("@run-slicer/vf"); }>; } -declare module "@run-slicer/vf/vf.runtime.js" { +declare module "vf-1.11.2/vf.runtime.js" { export * from "@run-slicer/vf"; } + +declare module "vf-1.11.2" { + export * from "@run-slicer/vf"; +} \ No newline at end of file diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 4c63b74..934f493 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -1,5 +1,5 @@ import * as Comlink from "comlink"; -import type * as vf from "../../logic/vf"; +import type * as vf from "../../logic/vineflower/vineflower"; import { DecompileJar, type DecompileResult } from "./types"; import type { Jar } from "../../utils/Jar"; import type { DecompileWorker } from "./worker"; diff --git a/src/workers/decompile/worker.ts b/src/workers/decompile/worker.ts index cd9c5c3..b55c63c 100644 --- a/src/workers/decompile/worker.ts +++ b/src/workers/decompile/worker.ts @@ -1,4 +1,4 @@ -import * as vf from "../../logic/vf"; +import * as vf from "../../logic/vineflower/vineflower"; import * as Comlink from "comlink"; import Dexie, { type EntityTable, type Table } from "dexie"; import type { Token } from "../../logic/Tokens"; From 02c192334dce1a6fa115655af583e0c5fb7fcd6c Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 28 Apr 2026 19:51:41 +0100 Subject: [PATCH 02/13] Add placeholder 1.12.0 Co-authored-by: Copilot --- package-lock.json | 10 ++++++++- package.json | 1 + src/logic/vineflower/vf-1.12.0.ts | 34 ++++++++++++++++++++++++++++++ src/logic/vineflower/vineflower.ts | 18 ++++++++++------ src/types/vf-runtime.d.ts | 15 +++++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 src/logic/vineflower/vf-1.12.0.ts diff --git a/package-lock.json b/package-lock.json index 66b83a9..46529b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "rxjs": "^7.8.2", - "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2" + "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", + "vf-1.12.0": "npm:@run-slicer/vf@^0.5.0-1.11.2" }, "devDependencies": { "@playwright/test": "^1.58.2", @@ -5358,6 +5359,13 @@ "integrity": "sha512-8+otfmuAuuEl1bo6LPp+qeR2TpY5pCccLrONK6/reJpCWdrU+g9++j4Eec1fqqv/DRX7nB79Xp9ZwaETR1mhXA==", "license": "Apache License 2.0, MIT" }, + "node_modules/vf-1.12.0": { + "name": "@run-slicer/vf", + "version": "0.5.0-1.11.2", + "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.5.0-1.11.2.tgz", + "integrity": "sha512-8+otfmuAuuEl1bo6LPp+qeR2TpY5pCccLrONK6/reJpCWdrU+g9++j4Eec1fqqv/DRX7nB79Xp9ZwaETR1mhXA==", + "license": "Apache License 2.0, MIT" + }, "node_modules/vite": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", diff --git a/package.json b/package.json index e2b9fd5..93f4e1a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@katana-project/zip": "^0.7.1", "@monaco-editor/react": "^4.7.0", "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", + "vf-1.12.0": "npm:@run-slicer/vf@^0.5.0-1.11.2", "@xyflow/react": "^12.10.1", "antd": "^6.3.2", "comlink": "^4.4.2", diff --git a/src/logic/vineflower/vf-1.12.0.ts b/src/logic/vineflower/vf-1.12.0.ts new file mode 100644 index 0000000..64af0f6 --- /dev/null +++ b/src/logic/vineflower/vf-1.12.0.ts @@ -0,0 +1,34 @@ +import wasmPath from "vf-1.12.0/vf.wasm?url"; +import { load } from "vf-1.12.0/vf.wasm-runtime.js"; +import type * as vf from "vf-1.12.0"; + +let runtime: typeof vf | null = null; +let runtimePreferWasm = true; + +export async function loadRuntime(preferWasm: boolean) { + if (!runtime || runtimePreferWasm !== preferWasm) { + runtimePreferWasm = preferWasm; + console.log(`Loading VineFlower 1.12.0 ${preferWasm ? "WASM" : "JavaScript"} runtime`); + + let loadJs = !preferWasm; + if (preferWasm) { + try { + const { exports } = await load(wasmPath, { noAutoImports: true }); + runtime = exports; + loadJs = false; + } catch (e) { + console.warn("Failed to load WASM module (non-compliant browser?), falling back to JS implementation", e); + loadJs = true; + } + } + + if (loadJs) { + runtime = await import("vf-1.12.0/vf.runtime.js"); + } + } +} + +export const decompile: typeof vf.decompile = async (name: string | string[], options?: vf.Config) => { + if (!runtime) throw "No runtime loaded"; + return await runtime.decompile(name, options); +}; diff --git a/src/logic/vineflower/vineflower.ts b/src/logic/vineflower/vineflower.ts index 4e792be..e268b1a 100644 --- a/src/logic/vineflower/vineflower.ts +++ b/src/logic/vineflower/vineflower.ts @@ -1,10 +1,13 @@ -const DEFAULT_VERSION = "1.11.2"; -export type Version = "1.11.2"; +const DEFAULT_VERSION = "1.12.0"; +export type Version = "1.11.2" | "1.12.0"; export async function loadRuntime(preferWasm: boolean, version: Version = DEFAULT_VERSION) { if (version === "1.11.2") { - const vf1112 = await import("./vf-1.11.2"); - return await vf1112.loadRuntime(preferWasm); + const vf = await import("./vf-1.11.2"); + return await vf.loadRuntime(preferWasm); + } else if (version === "1.12.0") { + const vf = await import("./vf-1.12.0"); + return await vf.loadRuntime(preferWasm); } throw new Error(`Unsupported Vineflower version: ${version}`); @@ -12,8 +15,11 @@ export async function loadRuntime(preferWasm: boolean, version: Version = DEFAUL export async function decompile(name: string | string[], options?: Config, version: Version = DEFAULT_VERSION) { if (version === "1.11.2") { - const vf1112 = await import("./vf-1.11.2"); - return await vf1112.decompile(name, options); + const vf = await import("./vf-1.11.2"); + return await vf.decompile(name, options); + } else if (version === "1.12.0") { + const vf = await import("./vf-1.12.0"); + return await vf.decompile(name, options); } throw new Error(`Unsupported Vineflower version: ${version}`); diff --git a/src/types/vf-runtime.d.ts b/src/types/vf-runtime.d.ts index 42a54c0..d8bd746 100644 --- a/src/types/vf-runtime.d.ts +++ b/src/types/vf-runtime.d.ts @@ -11,4 +11,19 @@ declare module "vf-1.11.2/vf.runtime.js" { declare module "vf-1.11.2" { export * from "@run-slicer/vf"; +} + +declare module "vf-1.12.0/vf.wasm-runtime.js" { + export function load( + wasmPath: string, + options?: { noAutoImports?: boolean; } + ): Promise<{ exports: typeof import("@run-slicer/vf"); }>; +} + +declare module "vf-1.12.0/vf.runtime.js" { + export * from "@run-slicer/vf"; +} + +declare module "vf-1.12.0" { + export * from "@run-slicer/vf"; } \ No newline at end of file From 179c71a5f4efb5e64abf7c93e2844d8c09676ce1 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 28 Apr 2026 20:21:07 +0100 Subject: [PATCH 03/13] First pass on setting VF version in the workers. --- src/logic/Decompiler.ts | 4 ++-- src/logic/Permalink.ts | 13 ++++++++----- src/logic/State.ts | 4 ++++ src/logic/vineflower/versions.ts | 22 ++++++++++++++++++++++ src/logic/vineflower/vineflower.ts | 7 +++---- src/ui/JarDecompilerModal.tsx | 3 ++- src/workers/decompile/client.ts | 17 ++++++++++++++--- src/workers/decompile/worker.ts | 13 +++++++++---- 8 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 src/logic/vineflower/versions.ts diff --git a/src/logic/Decompiler.ts b/src/logic/Decompiler.ts index e2fe7fb..d69e994 100644 --- a/src/logic/Decompiler.ts +++ b/src/logic/Decompiler.ts @@ -4,7 +4,7 @@ import { combineLatest, distinctUntilChanged, from, map, Observable, of, shareReplay, switchMap, tap, throttleTime } from "rxjs"; import { minecraftJar, type MinecraftJar } from "./MinecraftApi"; -import { selectedFile } from "./State"; +import { selectedFile, vineflowerVersion } from "./State"; import { bytecode, displayLambdas } from "./Settings"; import type { Options } from "./vineflower/vineflower"; import type { DecompileResult } from "../workers/decompile/types"; @@ -71,7 +71,7 @@ export async function getClassBytecode(className: string, jar: Jar) { export async function decompileClass(className: string, jar: Jar) { try { decompilerCounter.next(decompilerCounter.value + 1); - return await worker.decompileClass(className, jar); + return await worker.decompileClass(className, jar, vineflowerVersion.value); } finally { decompilerCounter.next(decompilerCounter.value - 1); } diff --git a/src/logic/Permalink.ts b/src/logic/Permalink.ts index fba5c46..b440993 100644 --- a/src/logic/Permalink.ts +++ b/src/logic/Permalink.ts @@ -1,6 +1,7 @@ import { combineLatest } from "rxjs"; import { resetPermalinkAffectingSettings, supportsPermalinking } from "./Settings"; -import { diffLeftSelectedMinecraftVersion, diffView, selectedFile, selectedLines, selectedMinecraftVersion } from "./State"; +import { diffLeftSelectedMinecraftVersion, diffView, vineflowerVersion, selectedFile, selectedLines, selectedMinecraftVersion } from "./State"; +import { vineflowerVersionToPermalinkVersion } from "./vineflower/versions"; export interface State { version: number; // Allows us to change the permalink structure in the future @@ -16,7 +17,7 @@ export interface State { } const DEFAULT_STATE: State = { - version: 0, + version: 2, minecraftVersion: "", file: undefined, selectedLines: null @@ -117,14 +118,16 @@ if (typeof window !== "undefined") { selectedFile, selectedLines, supportsPermalinking, - diffView + diffView, + vineflowerVersion ]).subscribe(([ minecraftVersion, diffLeftMinecraftVersion, file, selectedLines, supported, - diffView + diffView, + vineflowerVersion ]) => { if (!file && !diffView) { document.title = "mcsrc.dev"; @@ -146,7 +149,7 @@ if (typeof window !== "undefined") { return; } - let url = '/1/'; + let url = `/${vineflowerVersionToPermalinkVersion(vineflowerVersion)}/`; if (diffView) { url += `diff/${diffLeftMinecraftVersion}/${minecraftVersion}`; diff --git a/src/logic/State.ts b/src/logic/State.ts index d06b158..2130c1e 100644 --- a/src/logic/State.ts +++ b/src/logic/State.ts @@ -2,6 +2,7 @@ import { BehaviorSubject } from "rxjs"; import { pairwise } from "rxjs/operators"; import { Tab, CodeTab } from "./tabs"; import { getInitialState } from "./Permalink"; +import { DEFAULT_VERSION, getVersionFromPermalink, type Version } from "./vineflower/versions"; const initialState = getInitialState(); @@ -17,6 +18,7 @@ export const openTabs = new BehaviorSubject(initialTab ? [initialTab] : [ export const tabHistory = new BehaviorSubject(initialState.file ? [initialState.file] : []); export const searchQuery = new BehaviorSubject(""); export const referencesQuery = new BehaviorSubject(""); +export const vineflowerVersion = new BehaviorSubject(getVersionFromPermalink(initialState.version)); export interface SelectedLines { line: number; @@ -28,8 +30,10 @@ export const diffView = new BehaviorSubject(!!initialState.diff); export const diffLeftSelectedMinecraftVersion = new BehaviorSubject(initialState.diff?.leftMinecraftVersion ?? null); // Reset selected lines when file changes (skip initial emission to preserve permalink selection) +// Also reset the permalink version back to the latest selectedFile.pipe(pairwise()).subscribe(([previousFile, currentFile]) => { if (previousFile !== currentFile) { selectedLines.next(null); + vineflowerVersion.next(DEFAULT_VERSION); } }); diff --git a/src/logic/vineflower/versions.ts b/src/logic/vineflower/versions.ts new file mode 100644 index 0000000..fb997c3 --- /dev/null +++ b/src/logic/vineflower/versions.ts @@ -0,0 +1,22 @@ +export const DEFAULT_VERSION = "1.12.0"; +export type Version = "1.11.2" | "1.12.0"; + +export function getVersionFromPermalink(permalinkVersion: number): Version { + switch (permalinkVersion) { + case 1: + return "1.11.2"; + case 2: + return "1.12.0"; + default: + return DEFAULT_VERSION; + } +} + +export function vineflowerVersionToPermalinkVersion(vineflowerVersion: Version): number { + switch (vineflowerVersion) { + case "1.11.2": + return 1; + case "1.12.0": + return 2; + } +} \ No newline at end of file diff --git a/src/logic/vineflower/vineflower.ts b/src/logic/vineflower/vineflower.ts index e268b1a..e4c1957 100644 --- a/src/logic/vineflower/vineflower.ts +++ b/src/logic/vineflower/vineflower.ts @@ -1,7 +1,6 @@ -const DEFAULT_VERSION = "1.12.0"; -export type Version = "1.11.2" | "1.12.0"; +import { type Version, DEFAULT_VERSION } from "./versions"; -export async function loadRuntime(preferWasm: boolean, version: Version = DEFAULT_VERSION) { +export async function loadRuntime(preferWasm: boolean, version: Version) { if (version === "1.11.2") { const vf = await import("./vf-1.11.2"); return await vf.loadRuntime(preferWasm); @@ -13,7 +12,7 @@ export async function loadRuntime(preferWasm: boolean, version: Version = DEFAUL throw new Error(`Unsupported Vineflower version: ${version}`); } -export async function decompile(name: string | string[], options?: Config, version: Version = DEFAULT_VERSION) { +export async function decompile(version: Version, name: string | string[], options?: Config) { if (version === "1.11.2") { const vf = await import("./vf-1.11.2"); return await vf.decompile(name, options); diff --git a/src/ui/JarDecompilerModal.tsx b/src/ui/JarDecompilerModal.tsx index 949ce7e..30bb930 100644 --- a/src/ui/JarDecompilerModal.tsx +++ b/src/ui/JarDecompilerModal.tsx @@ -6,6 +6,7 @@ import { BooleanOption, NumberOption } from "./SettingsModal"; import { decompilerSplits, decompilerThreads, MAX_THREADS, preferWasmDecompiler } from "../logic/Settings"; import { decompileEntireJar, deleteCache, type DecompileEntireJarTask } from "../workers/decompile/client"; import { minecraftJar } from "../logic/MinecraftApi"; +import { DEFAULT_VERSION } from "../logic/vineflower/versions"; const modalOpen = new BehaviorSubject(false); @@ -26,7 +27,7 @@ export const JarDecompilerModal = () => { modalOpen.next(false); if (!jar) return; - const task = decompileEntireJar(jar.jar, { + const task = decompileEntireJar(jar.jar, DEFAULT_VERSION, { threads: decompilerThreads.value, splits: decompilerSplits.value, logger(progress, current, total) { diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 934f493..6d58516 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -3,6 +3,7 @@ import type * as vf from "../../logic/vineflower/vineflower"; import { DecompileJar, type DecompileResult } from "./types"; import type { Jar } from "../../utils/Jar"; import type { DecompileWorker } from "./worker"; +import { DEFAULT_VERSION, type Version } from "../../logic/vineflower/versions"; function createWorker() { const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module", name: "decompiler" }); @@ -13,6 +14,7 @@ type WorkerInstance = ReturnType; const MAX_THREADS = navigator.hardwareConcurrency || 4; let workers: WorkerInstance[] = []; let preferWasmRuntime = true; +let version: Version = DEFAULT_VERSION; async function ensureWorkers(count: number) { count = Math.min(count, MAX_THREADS); @@ -22,7 +24,7 @@ async function ensureWorkers(count: number) { { length: count - workers.length }, () => createWorker()); - await Promise.all(newWorkers.map(w => w.loadVFRuntime(preferWasmRuntime))); + await Promise.all(newWorkers.map(w => w.loadVFRuntime(preferWasmRuntime, version))); workers.push(...newWorkers); } @@ -48,6 +50,13 @@ export async function setRuntime(preferWasm: boolean) { workers = []; } +async function setVersion(newVersion: Version) { + if (version === newVersion) return; + version = newVersion; + await Promise.all(workers.map(w => w.scheduleClose())); + workers = []; +} + export async function setOptions(options: vf.Options) { const sab = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT); const state = new Uint32Array(sab); @@ -72,7 +81,7 @@ export type DecompileEntireJarTask = { stop: () => void; }; -export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions): DecompileEntireJarTask { +export function decompileEntireJar(jar: Jar, version: Version, options?: DecompileEntireJarOptions): DecompileEntireJarTask { const sab = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT); const state = new Uint32Array(sab); state[0] = 0; @@ -92,6 +101,7 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions options.logger!(classNames[i], ++current, classNames.length); }) : undefined; + await setVersion(version); await ensureWorkers(optThreads); const result = await Promise.all((workers .slice(0, optThreads)) @@ -109,7 +119,7 @@ export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions }; } -export async function decompileClass(className: string, jar: Jar): Promise { +export async function decompileClass(className: string, jar: Jar, version: Version): Promise { className = className.replace(".class", ""); const entry = jar.entries[`${className}.class`]; @@ -121,6 +131,7 @@ export async function decompileClass(className: string, jar: Jar): Promise | undefined = undefined; #promiseCount = 0; + #version: Version; promiseCount = () => this.#promiseCount; async schedule(fn: () => Promise): Promise { @@ -30,7 +32,8 @@ export class DecompileWorker { results3: Table, }; - constructor() { + constructor(version: Version = DEFAULT_VERSION) { + this.#version = version; this.db.version(4).stores({ options: "key", results3: "[className+checksum+language]", @@ -74,8 +77,10 @@ export class DecompileWorker { await this.db.options.bulkAdd(Object.entries(options).map(([k, v]) => ({ key: k, value: v }))); }); - loadVFRuntime = (preferWasm: boolean) => this.schedule(() => - vf.loadRuntime(preferWasm)); + loadVFRuntime = (preferWasm: boolean, version: Version) => this.schedule(() => { + this.#version = version; + return vf.loadRuntime(preferWasm, this.#version); + }); clear = (): Promise => this.schedule(async () => { const count = await this.db.results3.count(); @@ -180,7 +185,7 @@ export class DecompileWorker { let currentTokens: Token[] | undefined; let currentClassName: string | undefined; - const sources = await vf.decompile(classNames, { + const sources = await vf.decompile(this.#version, classNames, { source: async (name) => { const data = await classData[name]?.data; From 4f8b872439f85b929c699ff6f5cb37f24b1dc4f8 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 29 Apr 2026 22:02:12 +0100 Subject: [PATCH 04/13] Update vf 1.12.0 dep --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46529b9..dee981a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "react-dom": "^19.2.4", "rxjs": "^7.8.2", "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", - "vf-1.12.0": "npm:@run-slicer/vf@^0.5.0-1.11.2" + "vf-1.12.0": "npm:@run-slicer/vf@^0.6.1-1.12.0" }, "devDependencies": { "@playwright/test": "^1.58.2", @@ -5361,9 +5361,9 @@ }, "node_modules/vf-1.12.0": { "name": "@run-slicer/vf", - "version": "0.5.0-1.11.2", - "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.5.0-1.11.2.tgz", - "integrity": "sha512-8+otfmuAuuEl1bo6LPp+qeR2TpY5pCccLrONK6/reJpCWdrU+g9++j4Eec1fqqv/DRX7nB79Xp9ZwaETR1mhXA==", + "version": "0.6.1-1.12.0", + "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.6.1-1.12.0.tgz", + "integrity": "sha512-+RkAV6s1YLnmbQsfuH6lu0oIC5Ro1Y3u0bqV45SKLsd/tnqAJtgLrxqOs1VNmWXH7BeErfENIJM3xqrPXHDNBQ==", "license": "Apache License 2.0, MIT" }, "node_modules/vite": { diff --git a/package.json b/package.json index 93f4e1a..ae511e9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@katana-project/zip": "^0.7.1", "@monaco-editor/react": "^4.7.0", "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", - "vf-1.12.0": "npm:@run-slicer/vf@^0.5.0-1.11.2", + "vf-1.12.0": "npm:@run-slicer/vf@^0.6.1-1.12.0", "@xyflow/react": "^12.10.1", "antd": "^6.3.2", "comlink": "^4.4.2", From 1585f04732e3fb09bdc365e1b72c417af3b6134f Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 29 Apr 2026 22:06:00 +0100 Subject: [PATCH 05/13] Include decompiler version in db cache key --- src/workers/decompile/client.ts | 2 ++ src/workers/decompile/types.ts | 2 ++ src/workers/decompile/worker.ts | 36 +++++++++++++++++---------------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 6d58516..f1bc7dd 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -129,6 +129,7 @@ export async function decompileClass(className: string, jar: Jar, version: Versi source: `// Class not found: ${className}`, tokens: [], language: "java", + version, }; await setVersion(version); @@ -146,6 +147,7 @@ export async function getClassBytecode(className: string, jar: Jar): Promise, - results3: Table, + results4: Table, }; constructor(version: Version = DEFAULT_VERSION) { this.#version = version; - this.db.version(4).stores({ + this.db.version(5).stores({ options: "key", - results3: "[className+checksum+language]", + results4: "[className+checksum+language+version]", // clear old data + results3: null, results2: null, results: null, }); @@ -70,7 +71,7 @@ export class DecompileWorker { } if (changed || notVisited.size > 0) { - await this.db.results3.clear(); + await this.db.results4.clear(); } await this.db.options.clear(); @@ -83,8 +84,8 @@ export class DecompileWorker { }); clear = (): Promise => this.schedule(async () => { - const count = await this.db.results3.count(); - await this.db.results3.clear(); + const count = await this.db.results4.count(); + await this.db.results4.clear(); return count; }); @@ -123,9 +124,9 @@ export class DecompileWorker { const checksum = jar.proxy[className]?.checksum; if (!checksum) continue; - const dbCount = await this.db.results3 - .where("[className+checksum+language]") - .equals([className, checksum, "java"]) + const dbCount = await this.db.results4 + .where("[className+checksum+language+version]") + .equals([className, checksum, "java", this.#version]) .count(); if (dbCount >= 1) { @@ -157,7 +158,7 @@ export class DecompileWorker { try { const jar = new DecompileJar(await openJar(jarName, jarBlob)); const checksum = jar.proxy[className]?.checksum; - const dbResult = await this.db.results3.get([className, checksum, "java"]); + const dbResult = await this.db.results4.get([className, checksum, "java", this.#version]); if (dbResult) return dbResult; const result = await this.#decompile(jar.classes, [className], jar.proxy); @@ -169,7 +170,8 @@ export class DecompileWorker { checksum: 0, source: `// Error during decompilation: ${(e as Error).message}`, tokens: [], - language: "java" + language: "java", + version: this.#version }; } }); @@ -269,27 +271,27 @@ export class DecompileWorker { } tokens.sort((a, b) => a.start - b.start); - res.push({ className, checksum, source, tokens, language: "java" }); + res.push({ className, checksum, source: sourceStr, tokens, language: "java", version: this.#version }); } - await this.db.results3.bulkPut(res); + await this.db.results4.bulkPut(res); return res; } #indexer = new JarIndexer(); getClassBytecode = (className: string, checksum: number, classData: ArrayBufferLike[]): Promise => this.schedule(async () => { - let result = await this.db.results3.get([className, checksum, "bytecode"]); + let result = await this.db.results4.get([className, checksum, "bytecode", this.#version]); if (result) return result; try { const bytecode = await this.#indexer.getBytecode(classData); - result = { className, checksum, source: bytecode, tokens: [], language: "bytecode" }; + result = { className, checksum, source: bytecode, tokens: [], language: "bytecode", version: this.#version }; } catch (e) { console.error(`Error during bytecode retrieval of class '${className}':`, e); - result = { className, checksum, source: `// Error during bytecode retrieval: ${(e as Error).message}`, tokens: [], language: "bytecode" }; + result = { className, checksum, source: `// Error during bytecode retrieval: ${(e as Error).message}`, tokens: [], language: "bytecode", version: this.#version }; } - await this.db.results3.put(result); + await this.db.results4.put(result); return result; }); } From 234c916c7398e11ab8b34d0ca6d8aa6d42f9d8cc Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 29 Apr 2026 22:16:58 +0100 Subject: [PATCH 06/13] Add DecompilerVersionWarning --- src/ui/DecompilerVersionWarning.tsx | 32 ++++++++++++++++++ src/ui/FilepathHeader.tsx | 51 +++++++++++++++++------------ src/workers/decompile/worker.ts | 7 ++-- 3 files changed, 66 insertions(+), 24 deletions(-) create mode 100644 src/ui/DecompilerVersionWarning.tsx diff --git a/src/ui/DecompilerVersionWarning.tsx b/src/ui/DecompilerVersionWarning.tsx new file mode 100644 index 0000000..26e7d93 --- /dev/null +++ b/src/ui/DecompilerVersionWarning.tsx @@ -0,0 +1,32 @@ +import { theme, Tooltip } from "antd"; +import { useObservable } from "../utils/UseObservable"; +import { vineflowerVersion } from "../logic/State"; +import { DEFAULT_VERSION } from "../logic/vineflower/versions"; + +export const DecompilerVersionWarning = () => { + const { token } = theme.useToken(); + const version = useObservable(vineflowerVersion); + const isNonDefaultVersion = version !== DEFAULT_VERSION; + + if (!isNonDefaultVersion) return null; + + return ( + +
+ Note: Using legacy decompiler (Vineflower {version}) +
+
+ ); +}; diff --git a/src/ui/FilepathHeader.tsx b/src/ui/FilepathHeader.tsx index 2b1d399..88d5218 100644 --- a/src/ui/FilepathHeader.tsx +++ b/src/ui/FilepathHeader.tsx @@ -3,6 +3,7 @@ import { useObservable } from "../utils/UseObservable"; import { getDiffChanges } from "../logic/Diff"; import { combineLatest, map } from "rxjs"; import { selectedFile, diffView } from "../logic/State"; +import { DecompilerVersionWarning } from "./DecompilerVersionWarning"; const changeInfoObs = combineLatest([selectedFile, getDiffChanges(), diffView]).pipe( map(([file, changes, isDiff]) => { @@ -22,34 +23,42 @@ export const FilepathHeader = () => { width: "100%", boxSizing: "border-box", alignItems: "center", - justifyContent: "left", + justifyContent: "space-between", padding: ".25rem 1rem", fontFamily: token.fontFamily, }}>
- {info.replace(".class", "").split("/").map((path, i, arr) => ( - - {path} - {i < arr.length - 1 && /} - - ))} -
- {changeInfo && ( -
- {changeInfo.deletions !== undefined && changeInfo.deletions > 0 && ( - -{changeInfo.deletions} - )} - {changeInfo.additions !== undefined && changeInfo.additions > 0 && ( - +{changeInfo.additions} - )} +
+ {info.replace(".class", "").split("/").map((path, i, arr) => ( + + {path} + {i < arr.length - 1 && /} + + ))}
- )} + {changeInfo && ( +
+ {changeInfo.deletions !== undefined && changeInfo.deletions > 0 && ( + -{changeInfo.deletions} + )} + {changeInfo.additions !== undefined && changeInfo.additions > 0 && ( + +{changeInfo.additions} + )} +
+ )} +
+ ); }; diff --git a/src/workers/decompile/worker.ts b/src/workers/decompile/worker.ts index 3bb26c2..c5d4cf2 100644 --- a/src/workers/decompile/worker.ts +++ b/src/workers/decompile/worker.ts @@ -249,11 +249,12 @@ export class DecompileWorker { const res: DecompileResult[] = []; for (const [className, source] of Object.entries(sources)) { const checksum = classData[className]?.checksum ?? 0; - const tokens = allTokens[source] ?? []; + const sourceStr = source as string; + const tokens = allTokens[sourceStr] ?? []; const importRegex = /^\s*import\s+(?!static\b)([^\s;]+)\s*;/gm; - let match = null; - while ((match = importRegex.exec(source)) !== null) { + let match: RegExpExecArray | null = null; + while ((match = importRegex.exec(sourceStr)) !== null) { const importPath = match[1].replaceAll('.', '/'); if (importPath.endsWith('*')) { continue; From f61b2ac6f442393e92a58b4198fb502306492db8 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 29 Apr 2026 22:18:34 +0100 Subject: [PATCH 07/13] Link to latest version --- src/ui/FileList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/FileList.tsx b/src/ui/FileList.tsx index 5d46005..5a6f915 100644 --- a/src/ui/FileList.tsx +++ b/src/ui/FileList.tsx @@ -14,6 +14,7 @@ import { selectedFile, referencesQuery } from '../logic/State'; import { compactPackages } from '../logic/Settings'; import { jarIndex, type ClassData } from '../workers/jar-index/client'; import { ClassDataIcon, JavaIcon, PackageIcon } from './intellij-icons'; +import { DEFAULT_VERSION, vineflowerVersionToPermalinkVersion } from '../logic/vineflower/versions'; const classData: Observable | null> = jarIndex.pipe( switchMap(jarIndex => from(jarIndex.getClassData()).pipe( @@ -151,7 +152,7 @@ const getMenuItems = ( const packagePath = path.replace(/\//g, '.').replace('.class', ''); const filename = path.split('/').pop() || ''; const linkPath = path.replace('.class', ''); - const link = jar ? `https://mcsrc.dev/1/${jar.version}/${linkPath}` : ''; + const link = jar ? `https://mcsrc.dev/${vineflowerVersionToPermalinkVersion(DEFAULT_VERSION)}/${jar.version}/${linkPath}` : ''; const renderLabel = (title: string, value: string) => (
From 1e1968539a8cd3b0fa1cf95b4f83f224b4f446a0 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 6 May 2026 09:14:51 +0100 Subject: [PATCH 08/13] Fix tests --- src/logic/Permalink.test.ts | 38 ++++++++++++++++++++++++ tests/permalink.spec.ts | 58 ++++++++++++++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/logic/Permalink.test.ts b/src/logic/Permalink.test.ts index 7037346..e6d8b11 100644 --- a/src/logic/Permalink.test.ts +++ b/src/logic/Permalink.test.ts @@ -197,6 +197,44 @@ describe('Permalink', () => { }); }); + describe('Version 2 URLs (/2/)', () => { + it('should parse a /2/ permalink with version 2', () => { + const state = parsePathToState('2/1.21/net/minecraft/ChatFormatting')!; + + expect(state.version).toBe(2); + expect(state.minecraftVersion).toBe('1.21'); + expect(state.file).toBe('net/minecraft/ChatFormatting.class'); + expect(state.selectedLines).toBe(null); + }); + + it('should parse a /2/ permalink with line numbers', () => { + const state = parsePathToState('2/1.21.4/net/minecraft/server/MinecraftServer#L100-200')!; + + expect(state.version).toBe(2); + expect(state.minecraftVersion).toBe('1.21.4'); + expect(state.file).toBe('net/minecraft/server/MinecraftServer.class'); + expect(state.selectedLines).toEqual({ line: 100, lineEnd: 200 }); + }); + + it('should parse a /2/ diff permalink', () => { + const state = parsePathToState('2/diff/1.21/1.21.4/net/minecraft/ChatFormatting')!; + + expect(state.version).toBe(2); + expect(state.minecraftVersion).toBe('1.21.4'); + expect(state.file).toBe('net/minecraft/ChatFormatting.class'); + expect(state.diff).toEqual({ leftMinecraftVersion: '1.21' }); + }); + + it('should parse a /2/ diff permalink without a file', () => { + const state = parsePathToState('2/diff/1.21/1.21.4')!; + + expect(state.version).toBe(2); + expect(state.minecraftVersion).toBe('1.21.4'); + expect(state.file).toBeUndefined(); + expect(state.diff).toEqual({ leftMinecraftVersion: '1.21' }); + }); + }); + describe('Real-world Examples', () => { it('should parse multiline permalink', () => { const state = parsePathToState('1/1.21.4/net/minecraft/server/MinecraftServer#L250-260')!; diff --git a/tests/permalink.spec.ts b/tests/permalink.spec.ts index 52b4589..dba81c6 100644 --- a/tests/permalink.spec.ts +++ b/tests/permalink.spec.ts @@ -6,7 +6,10 @@ test.describe('Permalinks and Line Highlighting', () => { await setupTest(page); }); - test('Permalink with line range highlights multiple lines (new format)', async ({ page }) => { + test('Permalink with line range highlights multiple lines (/1/ format)', async ({ page }) => { + const consoleLogs: string[] = []; + page.on('console', msg => consoleLogs.push(msg.text())); + await page.goto('/1/26.1-snapshot-1/net/minecraft/SystemReport#L87-90'); await waitForDecompiledContent(page, 'class SystemReport'); @@ -14,6 +17,24 @@ test.describe('Permalinks and Line Highlighting', () => { const editor = page.locator('.monaco-editor'); const highlightedLines = editor.locator('.highlighted-line'); await expect(highlightedLines.first()).toBeVisible(); + + await expect(page.getByText('Note: Using legacy decompiler (Vineflower 1.11.2)')).toBeVisible(); + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.11.2'))).toBe(true); + }); + + test('Permalink with line range highlights multiple lines (/2/ format)', async ({ page }) => { + const consoleLogs: string[] = []; + page.on('console', msg => consoleLogs.push(msg.text())); + + await page.goto('/2/26.1-snapshot-1/net/minecraft/SystemReport#L87-90'); + + await waitForDecompiledContent(page, 'class SystemReport'); + + const editor = page.locator('.monaco-editor'); + const highlightedLines = editor.locator('.highlighted-line'); + await expect(highlightedLines.first()).toBeVisible(); + + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.12.0'))).toBe(true); }); test('Permalink with line range highlights multiple lines (old hash format)', async ({ page }) => { @@ -41,7 +62,7 @@ test.describe('Permalinks and Line Highlighting', () => { // Wait for URL to update await page.waitForTimeout(10); const urlAfterFirstClick = page.url(); - expect(urlAfterFirstClick).toMatch(/\/1\/.*#L\d+$/); + expect(urlAfterFirstClick).toMatch(/\/2\/.*#L\d+$/); // Shift-click on a different line to create range await lineNumbers.nth(5).click({ modifiers: ['Shift'] }); @@ -50,7 +71,7 @@ test.describe('Permalinks and Line Highlighting', () => { await page.waitForTimeout(10); // Check that URL now contains a line range (new path-based format) - expect(page.url()).toMatch(/\/1\/.*#L\d+-\d+$/); + expect(page.url()).toMatch(/\/2\/.*#L\d+-\d+$/); expect(page.url()).not.toEqual(urlAfterFirstClick); // Check that lines are highlighted @@ -58,7 +79,10 @@ test.describe('Permalinks and Line Highlighting', () => { await expect(highlightedLine.first()).toBeVisible(); }); - test('Diff permalink restores left and right versions and opens diff view', async ({ page }) => { + test('Diff permalink restores left and right versions and opens diff view (/1/ format)', async ({ page }) => { + const consoleLogs: string[] = []; + page.on('console', msg => consoleLogs.push(msg.text())); + await page.goto('/1/diff/26.1-mock-1/26.1-mock-2/net/minecraft/client/renderer/LevelRenderer'); const diffEditor = page.locator('.monaco-diff-editor'); @@ -74,5 +98,31 @@ test.describe('Permalinks and Line Highlighting', () => { await expect(decompilingMessage).toBeHidden(); await expect(diffEditor).toContainText('net.minecraft.client.renderer'); + + await expect(page.getByText('Note: Using legacy decompiler (Vineflower 1.11.2)')).toBeVisible(); + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.11.2'))).toBe(true); + }); + + test('Diff permalink restores left and right versions and opens diff view (/2/ format)', async ({ page }) => { + const consoleLogs: string[] = []; + page.on('console', msg => consoleLogs.push(msg.text())); + + await page.goto('/2/diff/26.1-mock-1/26.1-mock-2/net/minecraft/client/renderer/LevelRenderer'); + + const diffEditor = page.locator('.monaco-diff-editor'); + await expect(diffEditor).toBeVisible(); + + const leftVersionSelect = page.locator('.ant-select').nth(0); + const rightVersionSelect = page.locator('.ant-select').nth(1); + + await expect(leftVersionSelect).toContainText('26.1-mock-1'); + await expect(rightVersionSelect).toContainText('26.1-mock-2'); + + const decompilingMessage = page.getByText('Decompiling...'); + await expect(decompilingMessage).toBeHidden(); + + await expect(diffEditor).toContainText('net.minecraft.client.renderer'); + + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.12.0'))).toBe(true); }); }); From 853e5cb82235f085006cf8582432435a0585d2e8 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 6 May 2026 09:22:21 +0100 Subject: [PATCH 09/13] Upgrade diff links to /2 --- src/logic/Permalink.test.ts | 6 +++--- src/logic/Permalink.ts | 2 +- tests/permalink.spec.ts | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/logic/Permalink.test.ts b/src/logic/Permalink.test.ts index e6d8b11..51f5ad8 100644 --- a/src/logic/Permalink.test.ts +++ b/src/logic/Permalink.test.ts @@ -151,7 +151,7 @@ describe('Permalink', () => { it('should parse a diff permalink', () => { const state = parsePathToState('1/diff/1.21/1.21.4/net/minecraft/ChatFormatting')!; - expect(state.version).toBe(1); + expect(state.version).toBe(2); expect(state.minecraftVersion).toBe('1.21.4'); expect(state.file).toBe('net/minecraft/ChatFormatting.class'); expect(state.selectedLines).toBe(null); @@ -179,7 +179,7 @@ describe('Permalink', () => { it('should parse a diff permalink without a file', () => { const state = parsePathToState('1/diff/1.21/1.21.4')!; - expect(state.version).toBe(1); + expect(state.version).toBe(2); expect(state.minecraftVersion).toBe('1.21.4'); expect(state.file).toBeUndefined(); expect(state.selectedLines).toBe(null); @@ -251,7 +251,7 @@ describe('Permalink', () => { it('should parse a real-world diff permalink', () => { const state = parsePathToState('1/diff/1.21.4/1.21.5/net/minecraft/server/MinecraftServer')!; - expect(state.version).toBe(1); + expect(state.version).toBe(2); expect(state.minecraftVersion).toBe('1.21.5'); expect(state.file).toBe('net/minecraft/server/MinecraftServer.class'); expect(state.diff).toEqual({ leftMinecraftVersion: '1.21.4' }); diff --git a/src/logic/Permalink.ts b/src/logic/Permalink.ts index b440993..c2e3a87 100644 --- a/src/logic/Permalink.ts +++ b/src/logic/Permalink.ts @@ -52,7 +52,7 @@ export const parsePathToState = (path: string): State | null => { const rightMinecraftVersion = decodeURIComponent(segments[3]); const filePath = segments.slice(4).join('/'); return { - version, + version: DEFAULT_STATE.version, // The diff format didnt change from /1/ to /2/, so we can just blindly upgrade all diff permalinks to the new decompiler. minecraftVersion: rightMinecraftVersion, file: filePath ? filePath + (filePath.endsWith('.class') ? '' : '.class') : undefined, selectedLines: null, diff --git a/tests/permalink.spec.ts b/tests/permalink.spec.ts index dba81c6..e2f312b 100644 --- a/tests/permalink.spec.ts +++ b/tests/permalink.spec.ts @@ -99,8 +99,7 @@ test.describe('Permalinks and Line Highlighting', () => { await expect(diffEditor).toContainText('net.minecraft.client.renderer'); - await expect(page.getByText('Note: Using legacy decompiler (Vineflower 1.11.2)')).toBeVisible(); - expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.11.2'))).toBe(true); + expect(consoleLogs.some(log => log.includes('Loading VineFlower 1.12.0'))).toBe(true); }); test('Diff permalink restores left and right versions and opens diff view (/2/ format)', async ({ page }) => { From 15dfc97ecd404a276d3ab88e26c938e86af1130e Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 6 May 2026 09:40:21 +0100 Subject: [PATCH 10/13] Work around webkit test issues? --- tests/test-utils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test-utils.ts b/tests/test-utils.ts index eff0cf7..607392f 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -77,7 +77,12 @@ async function setupNetworkMocking(page: Page) { export async function setupTest(page: Page) { await setupNetworkMocking(page); - await page.addInitScript(() => { + const isWebKit = page.context().browser()?.browserType().name() === 'webkit'; + await page.addInitScript((preferWasm) => { localStorage.setItem('setting_eula', 'true'); - }); + if (!preferWasm) { + // Use JS runtime to avoid WASM compatibility issues in WebKit + localStorage.setItem('setting_prefer_wasm_decompiler', 'false'); + } + }, !isWebKit); } From 0999c22ac6143a9c30b108e932645f1e30abb4f9 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 6 May 2026 19:56:34 +0100 Subject: [PATCH 11/13] Add more logging + misc fixes --- src/logic/vineflower/vf-1.11.2.ts | 23 ++++++++++++++++------- src/logic/vineflower/vf-1.12.0.ts | 23 ++++++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/logic/vineflower/vf-1.11.2.ts b/src/logic/vineflower/vf-1.11.2.ts index 8a259c2..54701a7 100644 --- a/src/logic/vineflower/vf-1.11.2.ts +++ b/src/logic/vineflower/vf-1.11.2.ts @@ -7,28 +7,37 @@ let runtimePreferWasm = true; export async function loadRuntime(preferWasm: boolean) { if (!runtime || runtimePreferWasm !== preferWasm) { - runtimePreferWasm = preferWasm; - console.log(`Loading VineFlower 1.11.2 ${preferWasm ? "WASM" : "JavaScript"} runtime`); + console.log(`Loading VineFlower 1.11.2 ${preferWasm ? "WASM" : "JavaScript"} runtime (previous: ${runtime ? (runtimePreferWasm ? "WASM" : "JS") : "none"})`); - let loadJs = !preferWasm; if (preferWasm) { try { const { exports } = await load(wasmPath, { noAutoImports: true }); runtime = exports; - loadJs = false; + runtimePreferWasm = preferWasm; + console.log("VineFlower 1.11.2 WASM runtime loaded successfully"); + return; } catch (e) { console.warn("Failed to load WASM module (non-compliant browser?), falling back to JS implementation", e); - loadJs = true; + if (runtime) { + console.log("VineFlower 1.11.2 keeping existing JS runtime"); + return; + } } } - if (loadJs) { + try { runtime = await import("vf-1.11.2/vf.runtime.js"); + console.log("VineFlower 1.11.2 JS runtime loaded successfully"); + } catch (e) { + throw new Error(`Failed to load JS runtime: ${e}`); } + runtimePreferWasm = preferWasm; + } else { + console.log(`VineFlower 1.11.2 reusing existing ${runtimePreferWasm ? "WASM" : "JS"} runtime`); } } export const decompile: typeof vf.decompile = async (name: string | string[], options?: vf.Config) => { - if (!runtime) throw "No runtime loaded"; + if (!runtime) throw new Error("No runtime loaded"); return await runtime.decompile(name, options); }; diff --git a/src/logic/vineflower/vf-1.12.0.ts b/src/logic/vineflower/vf-1.12.0.ts index 64af0f6..78e1cbb 100644 --- a/src/logic/vineflower/vf-1.12.0.ts +++ b/src/logic/vineflower/vf-1.12.0.ts @@ -7,28 +7,37 @@ let runtimePreferWasm = true; export async function loadRuntime(preferWasm: boolean) { if (!runtime || runtimePreferWasm !== preferWasm) { - runtimePreferWasm = preferWasm; - console.log(`Loading VineFlower 1.12.0 ${preferWasm ? "WASM" : "JavaScript"} runtime`); + console.log(`Loading VineFlower 1.12.0 ${preferWasm ? "WASM" : "JavaScript"} runtime (previous: ${runtime ? (runtimePreferWasm ? "WASM" : "JS") : "none"})`); - let loadJs = !preferWasm; if (preferWasm) { try { const { exports } = await load(wasmPath, { noAutoImports: true }); runtime = exports; - loadJs = false; + runtimePreferWasm = preferWasm; + console.log("VineFlower 1.12.0 WASM runtime loaded successfully"); + return; } catch (e) { console.warn("Failed to load WASM module (non-compliant browser?), falling back to JS implementation", e); - loadJs = true; + if (runtime) { + console.log("VineFlower 1.12.0 keeping existing JS runtime"); + return; + } } } - if (loadJs) { + try { runtime = await import("vf-1.12.0/vf.runtime.js"); + console.log("VineFlower 1.12.0 JS runtime loaded successfully"); + } catch (e) { + throw new Error(`Failed to load JS runtime: ${e}`); } + runtimePreferWasm = preferWasm; + } else { + console.log(`VineFlower 1.12.0 reusing existing ${runtimePreferWasm ? "WASM" : "JS"} runtime`); } } export const decompile: typeof vf.decompile = async (name: string | string[], options?: vf.Config) => { - if (!runtime) throw "No runtime loaded"; + if (!runtime) throw new Error("No runtime loaded"); return await runtime.decompile(name, options); }; From d61a1683ecb9a489b0418e8c05c34dafaf19e3c6 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 13 May 2026 12:01:31 +0100 Subject: [PATCH 12/13] Update @run-slicer/vf --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index dee981a..b55ad37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "react-dom": "^19.2.4", "rxjs": "^7.8.2", "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", - "vf-1.12.0": "npm:@run-slicer/vf@^0.6.1-1.12.0" + "vf-1.12.0": "npm:@run-slicer/vf@^0.6.2-1.12.0" }, "devDependencies": { "@playwright/test": "^1.58.2", @@ -5361,9 +5361,9 @@ }, "node_modules/vf-1.12.0": { "name": "@run-slicer/vf", - "version": "0.6.1-1.12.0", - "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.6.1-1.12.0.tgz", - "integrity": "sha512-+RkAV6s1YLnmbQsfuH6lu0oIC5Ro1Y3u0bqV45SKLsd/tnqAJtgLrxqOs1VNmWXH7BeErfENIJM3xqrPXHDNBQ==", + "version": "0.6.2-1.12.0", + "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.6.2-1.12.0.tgz", + "integrity": "sha512-vYW2lscCemN2xwHcQvucQ00/j9dwsHwfBikCOReUKY/hb0gUp8vzukLQuXS9N++R8wwOuPV73TeQ+rrz2aA7Mw==", "license": "Apache License 2.0, MIT" }, "node_modules/vite": { diff --git a/package.json b/package.json index ae511e9..a253c52 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@katana-project/zip": "^0.7.1", "@monaco-editor/react": "^4.7.0", "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", - "vf-1.12.0": "npm:@run-slicer/vf@^0.6.1-1.12.0", + "vf-1.12.0": "npm:@run-slicer/vf@^0.6.2-1.12.0", "@xyflow/react": "^12.10.1", "antd": "^6.3.2", "comlink": "^4.4.2", From dbfa7cb6fc052c5b9009881629b095bb8ac38076 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Fri, 22 May 2026 09:32:07 +0100 Subject: [PATCH 13/13] Update VF --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b55ad37..37a614a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "react-dom": "^19.2.4", "rxjs": "^7.8.2", "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", - "vf-1.12.0": "npm:@run-slicer/vf@^0.6.2-1.12.0" + "vf-1.12.0": "npm:@run-slicer/vf@^0.6.3-1.12.0" }, "devDependencies": { "@playwright/test": "^1.58.2", @@ -5361,9 +5361,9 @@ }, "node_modules/vf-1.12.0": { "name": "@run-slicer/vf", - "version": "0.6.2-1.12.0", - "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.6.2-1.12.0.tgz", - "integrity": "sha512-vYW2lscCemN2xwHcQvucQ00/j9dwsHwfBikCOReUKY/hb0gUp8vzukLQuXS9N++R8wwOuPV73TeQ+rrz2aA7Mw==", + "version": "0.6.3-1.12.0", + "resolved": "https://registry.npmjs.org/@run-slicer/vf/-/vf-0.6.3-1.12.0.tgz", + "integrity": "sha512-Sz0HU2ScV1c1rs0Wx4nfhAiigXw08Mvydc3R2kROoIN6iVyAZAr1rNJpWMXabbTfxtu9TmPr9YdByl5aK/PQWQ==", "license": "Apache License 2.0, MIT" }, "node_modules/vite": { diff --git a/package.json b/package.json index a253c52..029728f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@katana-project/zip": "^0.7.1", "@monaco-editor/react": "^4.7.0", "vf-1.11.2": "npm:@run-slicer/vf@^0.5.0-1.11.2", - "vf-1.12.0": "npm:@run-slicer/vf@^0.6.2-1.12.0", + "vf-1.12.0": "npm:@run-slicer/vf@^0.6.3-1.12.0", "@xyflow/react": "^12.10.1", "antd": "^6.3.2", "comlink": "^4.4.2",