Skip to content

Commit a04b8cd

Browse files
committed
feat: support local Forge/NeoForge patched Minecraft jars (#9)
Adds an opt-in `jarPath` parameter to `decompile_minecraft_version` so users can point at the patched MC JAR from their Forge/NeoForge dev environment and have it flow through every downstream tool (get_minecraft_source, search_minecraft_code, index_minecraft_version, search_indexed, compare_versions) as a peer of vanilla decompilations. The version string is treated as an opaque cache key (convention: `<mc>-<loader>-<loaderVersion>` e.g. `1.21.1-neoforge-21.1.72`), so no existing cache or downstream tool needs to special-case patched variants. The mapping label is trusted; no remapping is performed. JAR content type is detected by scanning the central directory: any .class entry runs VineFlower; otherwise .java entries are extracted directly (covers NFRT/ForgeGradle `-sources.jar` outputs without a slow re-decompile). Also implements the previously stubbed `force` flag: wipes the decompiled directory, decompile_jobs row, and FTS5 index for the target (version, mapping) so re-runs are hermetic. `find_mapping` strips known loader suffixes from patched version strings before tinyfile lookup, so callers can pass the same opaque key without an extra error path. CI: new `patched-jars.yml` workflow generates real patched MC JARs (NeoForge via NeoFormRuntime across MC 1.20.4/1.21.1/1.21.4; Forge 1.20.1 via a minimal ForgeGradle fixture project) and runs the new `__tests__/manual/patched/` suite against each. Triggered on workflow_dispatch and on main pushes that touch the patched-JAR surface; gated out of default PR CI.
1 parent 92ca1d1 commit a04b8cd

16 files changed

Lines changed: 874 additions & 16 deletions

File tree

.github/workflows/patched-jars.yml

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
name: Patched-JAR Tests
2+
3+
# End-to-end coverage for the Forge/NeoForge patched-Minecraft pipeline added
4+
# under issue #9. Generates a real patched MC JAR per matrix entry (NeoForge via
5+
# NeoFormRuntime, Forge via a tiny ForgeGradle fixture project) and runs the
6+
# patched-jar test suite against it.
7+
#
8+
# Manually triggered + on push to main when relevant code changes — patched-JAR
9+
# generation downloads vanilla MC + applies patches and is too heavy for every PR.
10+
11+
on:
12+
workflow_dispatch:
13+
inputs:
14+
matrix_filter:
15+
description: 'Optional matrix filter (substring match on `name` field)'
16+
required: false
17+
default: ''
18+
push:
19+
branches: [main]
20+
paths:
21+
- 'src/services/decompile-service.ts'
22+
- 'src/services/mapping-service.ts'
23+
- 'src/utils/jar-inspector.ts'
24+
- 'src/server/tools.ts'
25+
- '__tests__/manual/patched/**'
26+
- '.github/workflows/patched-jars.yml'
27+
28+
jobs:
29+
neoforge:
30+
name: NeoForge ${{ matrix.mc }} / ${{ matrix.neoforge }}
31+
runs-on: ubuntu-latest
32+
strategy:
33+
fail-fast: false
34+
matrix:
35+
include:
36+
- name: neoforge-1.20.4
37+
mc: '1.20.4'
38+
neoforge: '20.4.248'
39+
- name: neoforge-1.21.1
40+
mc: '1.21.1'
41+
neoforge: '21.1.72'
42+
- name: neoforge-1.21.4
43+
mc: '1.21.4'
44+
neoforge: '21.4.30'
45+
steps:
46+
- uses: actions/checkout@v4
47+
48+
- name: Setup Node.js
49+
uses: actions/setup-node@v4
50+
with:
51+
node-version: '22.x'
52+
cache: 'npm'
53+
54+
- name: Setup Java
55+
uses: actions/setup-java@v4
56+
with:
57+
distribution: 'temurin'
58+
java-version: '21'
59+
60+
- name: Install dependencies
61+
run: npm ci
62+
63+
- name: Build
64+
run: npm run build
65+
66+
- name: Cache NeoFormRuntime working dir + produced JAR
67+
uses: actions/cache@v4
68+
with:
69+
path: |
70+
~/.neoformruntime
71+
nfrt-out
72+
key: nfrt-${{ matrix.mc }}-${{ matrix.neoforge }}-v1
73+
74+
- name: Resolve NeoFormRuntime
75+
# Releases: https://maven.neoforged.net/releases/net/neoforged/neoform-runtime/
76+
env:
77+
NFRT_VERSION: '1.0.30'
78+
run: |
79+
set -euo pipefail
80+
mkdir -p tools
81+
NFRT_URL="https://maven.neoforged.net/releases/net/neoforged/neoform-runtime/${NFRT_VERSION}/neoform-runtime-${NFRT_VERSION}-all.jar"
82+
if [ ! -f tools/nfrt.jar ]; then
83+
curl -fL --retry 3 -o tools/nfrt.jar "$NFRT_URL"
84+
fi
85+
java -jar tools/nfrt.jar --version
86+
87+
- name: Generate patched MC JAR via NFRT
88+
run: |
89+
set -euo pipefail
90+
mkdir -p nfrt-out
91+
OUT="$(pwd)/nfrt-out/${{ matrix.name }}.jar"
92+
if [ ! -f "$OUT" ]; then
93+
java -jar tools/nfrt.jar run \
94+
--neoforge "net.neoforged:neoforge:${{ matrix.neoforge }}:userdev" \
95+
--write-result "compiledWithNeoForge:$OUT"
96+
fi
97+
test -s "$OUT"
98+
echo "PATCHED_JAR_PATH=$OUT" >> "$GITHUB_ENV"
99+
echo "PATCHED_VERSION=${{ matrix.mc }}-neoforge-${{ matrix.neoforge }}" >> "$GITHUB_ENV"
100+
echo "PATCHED_MC_VERSION=${{ matrix.mc }}" >> "$GITHUB_ENV"
101+
echo "PATCHED_LOADER=neoforge" >> "$GITHUB_ENV"
102+
103+
- name: Run patched-JAR test suite
104+
run: npm run test:manual:patched
105+
106+
forge:
107+
name: Forge 1.20.1 / 47.4.0
108+
runs-on: ubuntu-latest
109+
steps:
110+
- uses: actions/checkout@v4
111+
112+
- name: Setup Node.js
113+
uses: actions/setup-node@v4
114+
with:
115+
node-version: '22.x'
116+
cache: 'npm'
117+
118+
- name: Setup Java
119+
uses: actions/setup-java@v4
120+
with:
121+
distribution: 'temurin'
122+
java-version: '17'
123+
124+
- name: Install dependencies
125+
run: npm ci
126+
127+
- name: Build
128+
run: npm run build
129+
130+
- name: Setup Gradle
131+
uses: gradle/actions/setup-gradle@v3
132+
with:
133+
# Gradle 8.5 is the last 8.x line that ForgeGradle 6.0.+ tracks
134+
# cleanly against the 1.20.1 toolchain.
135+
gradle-version: '8.5'
136+
137+
- name: Cache ForgeGradle artifact cache
138+
uses: actions/cache@v4
139+
with:
140+
path: |
141+
~/.gradle/caches/forge_gradle
142+
__tests__/fixtures/forge-1.20.1/build
143+
key: forge-1.20.1-47.4.0-v1
144+
145+
- name: Build patched Forge MC via ForgeGradle fixture
146+
working-directory: __tests__/fixtures/forge-1.20.1
147+
run: |
148+
set -euo pipefail
149+
# `setupDecompWorkspace` materializes the patched MC JAR into the
150+
# ForgeGradle cache. We then copy it into a stable location.
151+
gradle --no-daemon setupDecompWorkspace
152+
OUT="$(pwd)/build/forge-patched.jar"
153+
mkdir -p "$(dirname "$OUT")"
154+
# ForgeGradle 6 stores the patched, mapped MC JAR here:
155+
PATCHED="$(find ~/.gradle/caches/forge_gradle/minecraft_user_repo \
156+
-path '*1.20.1-47.4.0_mapped_official_1.20.1*' -name '*.jar' \
157+
! -name '*sources*' ! -name '*-slim*' | head -n1)"
158+
test -n "$PATCHED" -a -s "$PATCHED"
159+
cp "$PATCHED" "$OUT"
160+
echo "PATCHED_JAR_PATH=$OUT" >> "$GITHUB_ENV"
161+
echo "PATCHED_VERSION=1.20.1-forge-47.4.0" >> "$GITHUB_ENV"
162+
echo "PATCHED_MC_VERSION=1.20.1" >> "$GITHUB_ENV"
163+
echo "PATCHED_LOADER=forge" >> "$GITHUB_ENV"
164+
165+
- name: Run patched-JAR test suite
166+
run: npm run test:manual:patched

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ This is a **Model Context Protocol (MCP) server** that provides AI assistants wi
110110
- `registry/{version}/` - Registry data
111111
- `resources/` - Java tool JARs (VineFlower, tiny-remapper)
112112
- Central cache is shared across workspaces; expect ~400–450 MB per MC version (JAR + mappings + remapped + decompiled + registry).
113+
- Patched-MC variants (Forge/NeoForge — see `decompile_minecraft_version`'s `jarPath`) reuse the same `decompiled/{version}/{mapping}/` layout under a version key like `1.21.1-neoforge-21.1.72`, adding ~250–400 MB per variant on top of vanilla.
113114

114115
## Build & ESM Requirements
115116

@@ -290,7 +291,7 @@ The code should work automatically, but be aware:
290291

291292
### Phase 1 Tools (Core)
292293
1. **`get_minecraft_source`** - Get decompiled source for a Minecraft class
293-
2. **`decompile_minecraft_version`** - Trigger full decompilation of a version
294+
2. **`decompile_minecraft_version`** - Trigger full decompilation of a version. Pass `jarPath` to point at a local Forge/NeoForge **patched** MC JAR (cache key convention: `<mc>-<loader>-<loaderVersion>`, e.g. `1.21.1-neoforge-21.1.72`). Sources JARs (no `.class` entries) are extracted directly; compiled JARs run through VineFlower. `force: true` wipes the decompiled dir, the job row, and the FTS5 index for that (version, mapping).
294295
3. **`list_minecraft_versions`** - List available and cached versions
295296
4. **`get_registry_data`** - Get registry data (blocks, items, entities)
296297

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { stripPatchedVersion } from '../../src/services/mapping-service.js';
3+
4+
describe('stripPatchedVersion', () => {
5+
it('strips NeoForge suffix to vanilla MC version', () => {
6+
expect(stripPatchedVersion('1.21.1-neoforge-21.1.72')).toBe('1.21.1');
7+
});
8+
9+
it('strips Forge suffix to vanilla MC version', () => {
10+
expect(stripPatchedVersion('1.20.1-forge-47.4.0')).toBe('1.20.1');
11+
});
12+
13+
it('handles two-part MC versions (no patch)', () => {
14+
expect(stripPatchedVersion('1.20-forge-46.0.14')).toBe('1.20');
15+
});
16+
17+
it('case-insensitive on loader name', () => {
18+
expect(stripPatchedVersion('1.21.1-NeoForge-21.1.72')).toBe('1.21.1');
19+
});
20+
21+
it('returns vanilla version unchanged', () => {
22+
expect(stripPatchedVersion('1.21.10')).toBe('1.21.10');
23+
expect(stripPatchedVersion('1.20')).toBe('1.20');
24+
});
25+
26+
it('returns unknown-loader strings unchanged (no false stripping)', () => {
27+
expect(stripPatchedVersion('1.21.1-fancyloader-99')).toBe('1.21.1-fancyloader-99');
28+
});
29+
30+
it('returns snapshot-style versions unchanged', () => {
31+
expect(stripPatchedVersion('26.1-snapshot-9')).toBe('26.1-snapshot-9');
32+
});
33+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Forge 1.20.1 Fixture (CI only)
2+
3+
This is **not a mod project** and is not built or distributed. It exists solely
4+
so the `patched-jars.yml` GitHub Actions workflow can run
5+
`./gradlew setupDecompWorkspace` to materialize the patched Minecraft 1.20.1 +
6+
Forge 47.4.0 JAR, which the patched-JAR test suite then exercises.
7+
8+
The Gradle wrapper is **not** committed here — CI installs Gradle via
9+
`gradle/actions/setup-gradle` and invokes `gradle setupDecompWorkspace`
10+
directly. If you need to run this locally, install Gradle 8.5 (or any
11+
ForgeGradle 6.0-compatible version) and run `gradle setupDecompWorkspace`
12+
from this directory.
13+
14+
For local invocation outside CI, prefer running the patched-JAR test suite
15+
against a JAR you already have from your real ForgeGradle dev environment:
16+
17+
```
18+
PATCHED_JAR_PATH=/path/to/your/patched.jar \
19+
PATCHED_VERSION=1.20.1-forge-47.4.0 \
20+
PATCHED_MC_VERSION=1.20.1 \
21+
PATCHED_LOADER=forge \
22+
npm run test:manual:patched
23+
```
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Minimal ForgeGradle fixture — exists ONLY to materialize the patched
2+
// Minecraft 1.20.1 + Forge 47.4.0 JAR for the patched-jars CI workflow.
3+
// We do not compile or ship anything; `setupDecompWorkspace` is the goal.
4+
5+
plugins {
6+
id 'eclipse'
7+
id 'idea'
8+
id 'net.minecraftforge.gradle' version '6.0.+'
9+
id 'java'
10+
}
11+
12+
group = 'mcdxai.fixture'
13+
version = '0.0.0'
14+
15+
java {
16+
toolchain.languageVersion = JavaLanguageVersion.of(17)
17+
}
18+
19+
minecraft {
20+
mappings channel: 'official', version: '1.20.1'
21+
}
22+
23+
repositories {
24+
mavenCentral()
25+
}
26+
27+
dependencies {
28+
minecraft 'net.minecraftforge:forge:1.20.1-47.4.0'
29+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.gradle.jvmargs=-Xmx3G
2+
org.gradle.daemon=false
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
rootProject.name = 'mc-dev-mcp-forge-fixture'
2+
3+
pluginManagement {
4+
repositories {
5+
gradlePluginPortal()
6+
maven { url = 'https://maven.minecraftforge.net/' }
7+
}
8+
}

0 commit comments

Comments
 (0)