diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6ea7b3..fd36779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,20 +17,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup TinyGo - uses: acifani/setup-tinygo@v2 - with: - tinygo-version: '0.39.0' - - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.25' - - name: Build with TinyGo - run: | - GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} tinygo build -o mcrun-${{ matrix.goos }}-${{ matrix.goarch }}-tinygo . - - name: Build with Go run: | CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -ldflags="-s -w" -o mcrun-${{ matrix.goos }}-${{ matrix.goarch }} . @@ -53,4 +44,21 @@ jobs: run: | $env:CGO_ENABLED="0" $env:GOARCH="${{ matrix.goarch }}" - go build -ldflags="-s -w" -o mcrun-windows-${{ matrix.goarch }}.exe . \ No newline at end of file + go build -ldflags="-s -w" -o mcrun-windows-${{ matrix.goarch }}.exe . + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + + - name: Vet + run: go vet ./... + + - name: Test + run: go test ./... \ No newline at end of file diff --git a/README.md b/README.md index 8601801..b183a9b 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ Install or manage Java versions. mcrun install java [flags] Flags: - --version int Java version to install (8, 11, 17, 21) + --version int Java version to install (8, 11, 17, 21, 25) --system Install system-wide (requires root/admin) --list List installed Java versions --set-default Set as system default after install @@ -180,7 +180,7 @@ mcrun install java --version=21 --set-default -m, --mod string Server type: vanilla, paper, forge, fabric, waterfall, velocity, bungeecord, spigot, craftbukkit, cauldron --mod-version string Mod-specific version (e.g., Paper build number) --version string Minecraft version (e.g., 1.20.4) - --java int Java version override (8, 11, 17, 21) + --java int Java version override (8, 11, 17, 21, 25) --java-path string Custom Java binary path -V, --verbose Enable verbose output ``` @@ -211,14 +211,18 @@ mcrun install java --version=21 --set-default ## Java Version Requirements -mcrun automatically selects the correct Java version based on Minecraft version: +mcrun automatically selects a recommended Java version: it starts from the requirement +in Mojang's version metadata when available and rounds it up to the nearest available +LTS release (e.g. Minecraft 1.17 reports Java 16, mcrun uses Java 17). When Mojang +metadata is unavailable, this fallback table is used: -| Minecraft Version | Required Java | -|-------------------|---------------| +| Minecraft Version | Recommended Java | +|-------------------|------------------| +| 26.2+ (year-based versions) | Java 25 | | 1.21+ | Java 21 | | 1.20.5 - 1.20.6 | Java 21 | | 1.18 - 1.20.4 | Java 17 | -| 1.17 - 1.17.1 | Java 16+ | +| 1.17 - 1.17.1 | Java 17 | | 1.16.5 and older | Java 8 | **Proxy servers** (Waterfall, Velocity, Bungeecord) require **Java 17**. diff --git a/cmd/mcrun/install.go b/cmd/mcrun/install.go index 1d03e62..6be6956 100644 --- a/cmd/mcrun/install.go +++ b/cmd/mcrun/install.go @@ -35,7 +35,7 @@ var installJavaCmd = &cobra.Command{ Short: "Install or manage Java", Long: `Install Java from Eclipse Adoptium or list installed Java versions. -Available LTS versions: 8, 11, 17, 21 +Available LTS versions: 8, 11, 17, 21, 25 Examples: mcrun install java # Install latest LTS Java @@ -55,7 +55,7 @@ Examples: // Install mode version := installJavaVersion if version == 0 { - version = 21 // Default to latest LTS + version = 25 // Default to latest LTS } fmt.Printf("Installing Java %d...\n", version) @@ -92,7 +92,7 @@ func listInstalledJava(manager *java.Manager) error { if len(installations) == 0 { fmt.Println("[WARNING] No Java installations found.") fmt.Println("\nInstall Java with:") - fmt.Println(" mcrun install java --version=21") + fmt.Println(" mcrun install java --version=25") return nil } @@ -136,7 +136,7 @@ func init() { rootCmd.AddCommand(installCmd) installCmd.AddCommand(installJavaCmd) - installJavaCmd.Flags().IntVar(&installJavaVersion, "version", 0, "Java version to install (8, 11, 17, 21)") + installJavaCmd.Flags().IntVar(&installJavaVersion, "version", 0, "Java version to install (8, 11, 17, 21, 25)") installJavaCmd.Flags().BoolVar(&installSystemWide, "system", false, "install system-wide (requires root/admin)") installJavaCmd.Flags().BoolVar(&installListJava, "list", false, "list installed Java versions") installJavaCmd.Flags().BoolVar(&installSetDefault, "set-default", false, "set as system default after install") diff --git a/cmd/mcrun/root.go b/cmd/mcrun/root.go index 128811c..7d03730 100644 --- a/cmd/mcrun/root.go +++ b/cmd/mcrun/root.go @@ -54,7 +54,8 @@ var rootCmd = &cobra.Command{ Cross-platform CLI tool to download and run Minecraft servers with integrated Java management. -Supported server types: vanilla, paper, forge, fabric, spigot, craftbukkit, cauldron`, +Supported server types: vanilla, paper, forge, fabric, spigot, craftbukkit, cauldron, +waterfall, velocity, bungeecord`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { var err error cfg, err = config.Load(cfgFile, serverDir) @@ -85,9 +86,9 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default: ~/.mcrun/config.yaml)") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "V", false, "enable verbose output") rootCmd.PersistentFlags().StringVar(&mcVersion, "version", "", "Minecraft version (e.g., 1.20.4)") - rootCmd.PersistentFlags().StringVarP(&mod, "mod", "m", "vanilla", "server mod type (vanilla, paper, forge, fabric, spigot, craftbukkit, cauldron)") + rootCmd.PersistentFlags().StringVarP(&mod, "mod", "m", "vanilla", "server mod type (vanilla, paper, forge, fabric, spigot, craftbukkit, cauldron, waterfall, velocity, bungeecord)") rootCmd.PersistentFlags().StringVar(&modVersion, "mod-version", "", "mod-specific version") - rootCmd.PersistentFlags().IntVar(&javaVersion, "java", 0, "Java version override (8, 11, 17, 21)") + rootCmd.PersistentFlags().IntVar(&javaVersion, "java", 0, "Java version override (8, 11, 17, 21, 25)") rootCmd.PersistentFlags().StringVar(&javaPath, "java-path", "", "custom Java binary path") rootCmd.PersistentFlags().StringVarP(&serverDir, "dir", "d", ".", "server working directory") } diff --git a/internal/java/detector.go b/internal/java/detector.go index 1b7eeea..84df7b4 100644 --- a/internal/java/detector.go +++ b/internal/java/detector.go @@ -14,7 +14,7 @@ import ( // Installation represents a detected Java installation type Installation struct { Path string // Full path to java binary - Version int // Major version (8, 11, 17, 21) + Version int // Major version (8, 11, 17, 21, 25) FullVersion string // e.g., "21.0.9+10-LTS" Vendor string // e.g., "Eclipse Adoptium" IsSystem bool // System-wide vs user/bundled @@ -71,39 +71,6 @@ func (d *Detector) DetectAll() ([]Installation, error) { return installations, nil } -// FindForVersion finds a Java installation suitable for a Minecraft version -func (d *Detector) FindForVersion(mcVersion string) (*Installation, error) { - required := GetRequiredJavaVersion(mcVersion) - installations, err := d.DetectAll() - if err != nil { - return nil, err - } - - // First try to find exact match - for _, inst := range installations { - if inst.Version == required { - return &inst, nil - } - } - - // Then find any compatible version (higher is ok) - var best *Installation - for i := range installations { - inst := &installations[i] - if inst.Version >= required { - if best == nil || inst.Version < best.Version { - best = inst - } - } - } - - if best != nil { - return best, nil - } - - return nil, fmt.Errorf("no compatible Java found (need Java %d or higher)", required) -} - // getSearchPaths returns paths to search for Java installations func (d *Detector) getSearchPaths() []string { home, _ := os.UserHomeDir() diff --git a/internal/java/manager.go b/internal/java/manager.go index 92e116c..320c82e 100644 --- a/internal/java/manager.go +++ b/internal/java/manager.go @@ -23,36 +23,6 @@ func NewManager(cfg *config.Config) *Manager { } } -// GetForMinecraftVersion finds or installs Java suitable for a Minecraft version -func (m *Manager) GetForMinecraftVersion(ctx context.Context, mcVersion string, autoInstall bool) (*Installation, error) { - required := GetRequiredJavaVersion(mcVersion) - - // Check config for custom path - if m.config != nil && m.config.Java.Paths != nil { - if customPath, ok := m.config.Java.Paths[required]; ok && customPath != "" { - inst, err := m.detector.getInstallationInfo(customPath) - if err == nil && inst.Version >= required { - return inst, nil - } - } - } - - // Try to find existing installation - inst, err := m.detector.FindForVersion(mcVersion) - if err == nil { - return inst, nil - } - - // Auto-install if enabled - if autoInstall || (m.config != nil && m.config.Java.AutoInstall) { - fmt.Printf("Java %d not found. Installing...\n", required) - return m.installer.Install(ctx, required, InstallOptions{}) - } - - return nil, fmt.Errorf("Java %d required for Minecraft %s but not found. Run 'mcrun install java --version=%d' to install", - required, mcVersion, required) -} - // GetByVersion finds or installs a specific Java version func (m *Manager) GetByVersion(ctx context.Context, version int, autoInstall bool) (*Installation, error) { // Check config for custom path diff --git a/internal/java/version_map.go b/internal/java/version_map.go deleted file mode 100644 index 6fe2d42..0000000 --- a/internal/java/version_map.go +++ /dev/null @@ -1,60 +0,0 @@ -package java - -import "fmt" - -// GetRequiredJavaVersion returns the minimum required Java version for a Minecraft version -func GetRequiredJavaVersion(mcVersion string) int { - major, minor, patch := parseMinecraftVersion(mcVersion) - - // MC 1.21+ requires Java 21 - if major >= 1 && minor >= 21 { - return 21 - } - - // MC 1.20.5+ requires Java 21 - if major >= 1 && minor == 20 && patch >= 5 { - return 21 - } - - // MC 1.18 - 1.20.4 requires Java 17 - if major >= 1 && minor >= 18 { - return 17 - } - - // MC 1.17 requires Java 16 (but 17 works fine) - if major >= 1 && minor >= 17 { - return 16 - } - - // Older versions can use Java 8 - return 8 -} - -// GetRecommendedJavaVersion returns the recommended Java version for a Minecraft version -// This may be higher than the minimum required for better performance -func GetRecommendedJavaVersion(mcVersion string) int { - required := GetRequiredJavaVersion(mcVersion) - - // For Java 16 requirement, recommend 17 (LTS) - if required == 16 { - return 17 - } - - return required -} - -// IsJavaCompatible checks if a Java version is compatible with a Minecraft version -func IsJavaCompatible(javaVersion int, mcVersion string) bool { - required := GetRequiredJavaVersion(mcVersion) - return javaVersion >= required -} - -// parseMinecraftVersion parses a Minecraft version string into components -func parseMinecraftVersion(version string) (major, minor, patch int) { - var m, n, p int - fmt.Sscanf(version, "%d.%d.%d", &m, &n, &p) - if m == 0 { - fmt.Sscanf(version, "%d.%d", &m, &n) - } - return m, n, p -} diff --git a/internal/providers/bungeecord.go b/internal/providers/bungeecord.go index cc96fb3..692fbee 100644 --- a/internal/providers/bungeecord.go +++ b/internal/providers/bungeecord.go @@ -107,6 +107,6 @@ func (p *BungeecordProvider) PostDownload(ctx context.Context, jarPath string, j } // GetRecommendedJavaVersion returns Java 17 for modern Bungeecord -func (p *BungeecordProvider) GetRecommendedJavaVersion(_ string) int { +func (p *BungeecordProvider) GetRecommendedJavaVersion(_ context.Context, _ string) int { return 17 } diff --git a/internal/providers/cauldron.go b/internal/providers/cauldron.go index ae91c05..5917696 100644 --- a/internal/providers/cauldron.go +++ b/internal/providers/cauldron.go @@ -85,7 +85,7 @@ func (p *CauldronProvider) PostDownload(ctx context.Context, jarPath string, jav } // GetRecommendedJavaVersion returns the recommended Java version -func (p *CauldronProvider) GetRecommendedJavaVersion(mcVersion string) int { +func (p *CauldronProvider) GetRecommendedJavaVersion(_ context.Context, _ string) int { // Cauldron is old and requires Java 8 return 8 } diff --git a/internal/providers/craftbukkit.go b/internal/providers/craftbukkit.go index f800a64..a9a7b88 100644 --- a/internal/providers/craftbukkit.go +++ b/internal/providers/craftbukkit.go @@ -26,7 +26,8 @@ func (p *CraftBukkitProvider) printPaperSuggestion() { fmt.Println("\n[WARNING] CraftBukkit requires BuildTools compilation which is slow and complex.") fmt.Println(" Suggestion: Use Paper instead - it's a modern fork of CraftBukkit/Spigot") fmt.Println(" that's directly downloadable and offers better performance.") - fmt.Println(" Run: mcrun run --mod=paper --version=\n") + fmt.Println(" Run: mcrun run --mod=paper --version=") + fmt.Println() } // ListVersions returns available Minecraft versions @@ -60,6 +61,6 @@ func (p *CraftBukkitProvider) PostDownload(ctx context.Context, jarPath string, } // GetRecommendedJavaVersion returns the recommended Java version -func (p *CraftBukkitProvider) GetRecommendedJavaVersion(mcVersion string) int { - return GetRecommendedJavaVersion(mcVersion) +func (p *CraftBukkitProvider) GetRecommendedJavaVersion(ctx context.Context, mcVersion string) int { + return GetRecommendedJavaVersion(ctx, mcVersion) } diff --git a/internal/providers/fabric.go b/internal/providers/fabric.go index ff3be8e..c56fa0c 100644 --- a/internal/providers/fabric.go +++ b/internal/providers/fabric.go @@ -126,6 +126,6 @@ func (p *FabricProvider) PostDownload(ctx context.Context, jarPath string, javaP } // GetRecommendedJavaVersion returns the recommended Java version -func (p *FabricProvider) GetRecommendedJavaVersion(mcVersion string) int { - return GetRecommendedJavaVersion(mcVersion) +func (p *FabricProvider) GetRecommendedJavaVersion(ctx context.Context, mcVersion string) int { + return GetRecommendedJavaVersion(ctx, mcVersion) } diff --git a/internal/providers/forge.go b/internal/providers/forge.go index e50a80a..1bd7d93 100644 --- a/internal/providers/forge.go +++ b/internal/providers/forge.go @@ -176,8 +176,8 @@ func (p *ForgeProvider) PostDownload(ctx context.Context, jarPath string, javaPa } // GetRecommendedJavaVersion returns the recommended Java version -func (p *ForgeProvider) GetRecommendedJavaVersion(mcVersion string) int { - return GetRecommendedJavaVersion(mcVersion) +func (p *ForgeProvider) GetRecommendedJavaVersion(ctx context.Context, mcVersion string) int { + return GetRecommendedJavaVersion(ctx, mcVersion) } // compareVersions compares two version strings diff --git a/internal/providers/java_resolver.go b/internal/providers/java_resolver.go new file mode 100644 index 0000000..bb4db2c --- /dev/null +++ b/internal/providers/java_resolver.go @@ -0,0 +1,146 @@ +package providers + +import ( + "context" + "fmt" + "sync" + + "github.com/gameap/minecraft-runner/pkg/api/mojang" +) + +// ltsJavaVersions are the Java LTS releases available from Adoptium +var ltsJavaVersions = []int{8, 11, 17, 21, 25} + +// javaResolver resolves the Java version for a Minecraft version using +// Mojang version metadata, with a heuristic fallback for offline use +type javaResolver struct { + client *mojang.Client + + mu sync.Mutex + cache map[string]int +} + +func newJavaResolver(client *mojang.Client) *javaResolver { + return &javaResolver{ + client: client, + cache: make(map[string]int), + } +} + +var defaultJavaResolver = newJavaResolver(mojang.NewClient()) + +// GetRecommendedJavaVersion returns the recommended Java version for a Minecraft version +func GetRecommendedJavaVersion(ctx context.Context, mcVersion string) int { + return defaultJavaResolver.Resolve(ctx, mcVersion) +} + +// Resolve returns the Java version for a Minecraft version, preferring Mojang metadata +func (r *javaResolver) Resolve(ctx context.Context, mcVersion string) int { + r.mu.Lock() + if version, ok := r.cache[mcVersion]; ok { + r.mu.Unlock() + return version + } + r.mu.Unlock() + + version, err := r.resolveFromMojang(ctx, mcVersion) + if err != nil || version <= 0 { + version = fallbackJavaVersion(mcVersion) + } else { + version = roundUpToLTS(version) + } + + r.mu.Lock() + r.cache[mcVersion] = version + r.mu.Unlock() + + return version +} + +func (r *javaResolver) resolveFromMojang(ctx context.Context, mcVersion string) (int, error) { + manifest, err := r.client.GetVersionManifest(ctx) + if err != nil { + return 0, err + } + + v := manifest.FindVersion(mcVersion) + if v == nil { + return 0, fmt.Errorf("version %s not found in manifest", mcVersion) + } + + detail, err := r.client.GetVersionDetail(ctx, v.URL) + if err != nil { + return 0, err + } + + return detail.JavaVersion.MajorVersion, nil +} + +// roundUpToLTS maps a Java version to the nearest LTS release available +// from Adoptium (e.g. 16 -> 17); versions above the newest known LTS pass through +func roundUpToLTS(version int) int { + for _, lts := range ltsJavaVersions { + if lts >= version { + return lts + } + } + return version +} + +// fallbackJavaVersion guesses the Java version from the Minecraft version +// string when Mojang metadata is unavailable +func fallbackJavaVersion(mcVersion string) int { + if year, ok := parseSnapshotYear(mcVersion); ok { + // Weekly snapshots map by year: 24w+ is the 1.20.5-1.21.x era (Java 21), + // 21w-23w is the 1.17-1.20.4 era (Java 17), older snapshots run on Java 8 + switch { + case year >= 26: + return 25 + case year >= 24: + return 21 + case year >= 21: + return 17 + } + return 8 + } + + major, minor, patch := parseMinecraftVersion(mcVersion) + + // Year-based versions (26.x and later) require Java 25 + if major >= 26 { + return 25 + } + + if major == 1 { + switch { + case minor >= 21: + return 21 + case minor == 20 && patch >= 5: + return 21 + case minor >= 17: + return 17 + } + } + + return 8 +} + +// parseSnapshotYear extracts the year from weekly snapshot IDs like "24w14a" +func parseSnapshotYear(version string) (int, bool) { + var year, week int + if n, _ := fmt.Sscanf(version, "%dw%d", &year, &week); n == 2 { + return year, true + } + return 0, false +} + +// parseMinecraftVersion parses a Minecraft version string into components +func parseMinecraftVersion(version string) (major, minor, patch int) { + // Handle formats like "1.20.4", "1.20", "1.20.4-pre1", etc. + var m, n, p int + fmt.Sscanf(version, "%d.%d.%d", &m, &n, &p) + if m == 0 { + fmt.Sscanf(version, "%d.%d", &m, &n) + } + return m, n, p +} diff --git a/internal/providers/java_resolver_test.go b/internal/providers/java_resolver_test.go new file mode 100644 index 0000000..ae0636b --- /dev/null +++ b/internal/providers/java_resolver_test.go @@ -0,0 +1,164 @@ +package providers + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gameap/minecraft-runner/pkg/api/mojang" +) + +func TestFallbackJavaVersion(t *testing.T) { + tests := []struct { + mcVersion string + want int + }{ + {"1.8.8", 8}, + {"1.16.5", 8}, + {"1.17", 17}, + {"1.17.1", 17}, + {"1.18.2", 17}, + {"1.20.4", 17}, + {"1.20.4-pre1", 17}, + {"1.20.5", 21}, + {"1.21.4", 21}, + {"26.2", 25}, + {"26.3-snapshot-5", 25}, + {"27.1", 25}, + {"26w07a", 25}, + {"25w05a", 21}, + {"24w14a", 21}, + {"23w31a", 17}, + {"21w19a", 17}, + {"20w14a", 8}, + {"", 8}, + {"latest", 8}, + } + + for _, tt := range tests { + if got := fallbackJavaVersion(tt.mcVersion); got != tt.want { + t.Errorf("fallbackJavaVersion(%q) = %d, want %d", tt.mcVersion, got, tt.want) + } + } +} + +func TestRoundUpToLTS(t *testing.T) { + tests := []struct { + version int + want int + }{ + {8, 8}, + {11, 11}, + {16, 17}, + {20, 21}, + {21, 21}, + {22, 25}, + {25, 25}, + {26, 26}, + } + + for _, tt := range tests { + if got := roundUpToLTS(tt.version); got != tt.want { + t.Errorf("roundUpToLTS(%d) = %d, want %d", tt.version, got, tt.want) + } + } +} + +func newMojangTestServer(t *testing.T, javaVersions map[string]int, manifestHits *int) *httptest.Server { + t.Helper() + + var srv *httptest.Server + mux := http.NewServeMux() + + mux.HandleFunc("/manifest.json", func(w http.ResponseWriter, _ *http.Request) { + if manifestHits != nil { + *manifestHits++ + } + entries := make([]string, 0, len(javaVersions)) + for id := range javaVersions { + entries = append(entries, fmt.Sprintf(`{"id":%q,"type":"release","url":"%s/detail/%s.json"}`, id, srv.URL, id)) + } + fmt.Fprintf(w, `{"latest":{"release":"26.2","snapshot":"26.2"},"versions":[%s]}`, strings.Join(entries, ",")) + }) + + mux.HandleFunc("/detail/", func(w http.ResponseWriter, req *http.Request) { + id := strings.TrimSuffix(strings.TrimPrefix(req.URL.Path, "/detail/"), ".json") + fmt.Fprintf(w, `{"id":%q,"javaVersion":{"component":"java-runtime","majorVersion":%d}}`, id, javaVersions[id]) + }) + + srv = httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv +} + +func newTestResolver(srv *httptest.Server) *javaResolver { + return newJavaResolver(mojang.NewClientWithManifestURL(srv.URL + "/manifest.json")) +} + +func TestResolveUsesMojangMajorVersion(t *testing.T) { + srv := newMojangTestServer(t, map[string]int{"26.2": 25, "1.21.4": 21}, nil) + r := newTestResolver(srv) + + if got := r.Resolve(context.Background(), "26.2"); got != 25 { + t.Errorf("Resolve(26.2) = %d, want 25", got) + } + if got := r.Resolve(context.Background(), "1.21.4"); got != 21 { + t.Errorf("Resolve(1.21.4) = %d, want 21", got) + } +} + +func TestResolveRoundsNonLTSUpToLTS(t *testing.T) { + srv := newMojangTestServer(t, map[string]int{"1.17": 16}, nil) + r := newTestResolver(srv) + + if got := r.Resolve(context.Background(), "1.17"); got != 17 { + t.Errorf("Resolve(1.17) = %d, want 17", got) + } +} + +func TestResolveFallsBackWhenMajorVersionMissing(t *testing.T) { + srv := newMojangTestServer(t, map[string]int{"1.8.8": 0}, nil) + r := newTestResolver(srv) + + if got := r.Resolve(context.Background(), "1.8.8"); got != 8 { + t.Errorf("Resolve(1.8.8) = %d, want 8", got) + } +} + +func TestResolveFallsBackWhenMojangUnavailable(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(http.NotFound)) + t.Cleanup(srv.Close) + r := newTestResolver(srv) + + if got := r.Resolve(context.Background(), "26.2"); got != 25 { + t.Errorf("Resolve(26.2) = %d, want 25", got) + } + if got := r.Resolve(context.Background(), "1.20.4"); got != 17 { + t.Errorf("Resolve(1.20.4) = %d, want 17", got) + } +} + +func TestResolveFallsBackWhenVersionMissingFromManifest(t *testing.T) { + srv := newMojangTestServer(t, map[string]int{"1.20.4": 17}, nil) + r := newTestResolver(srv) + + if got := r.Resolve(context.Background(), "26.2"); got != 25 { + t.Errorf("Resolve(26.2) = %d, want 25", got) + } +} + +func TestResolveCachesResult(t *testing.T) { + hits := 0 + srv := newMojangTestServer(t, map[string]int{"26.2": 25}, &hits) + r := newTestResolver(srv) + + r.Resolve(context.Background(), "26.2") + r.Resolve(context.Background(), "26.2") + + if hits != 1 { + t.Errorf("manifest fetched %d times, want 1", hits) + } +} diff --git a/internal/providers/paper.go b/internal/providers/paper.go index ea97fa4..7c5ee90 100644 --- a/internal/providers/paper.go +++ b/internal/providers/paper.go @@ -124,6 +124,6 @@ func (p *PaperProvider) PostDownload(ctx context.Context, jarPath string, javaPa } // GetRecommendedJavaVersion returns the recommended Java version -func (p *PaperProvider) GetRecommendedJavaVersion(mcVersion string) int { - return GetRecommendedJavaVersion(mcVersion) +func (p *PaperProvider) GetRecommendedJavaVersion(ctx context.Context, mcVersion string) int { + return GetRecommendedJavaVersion(ctx, mcVersion) } diff --git a/internal/providers/provider.go b/internal/providers/provider.go index c8a29ff..67d0db6 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -47,7 +47,7 @@ type Provider interface { PostDownload(ctx context.Context, jarPath string, javaPath string) error // GetRecommendedJavaVersion returns the recommended Java version for this MC version - GetRecommendedJavaVersion(mcVersion string) int + GetRecommendedJavaVersion(ctx context.Context, mcVersion string) int } // Registry holds all available providers @@ -84,43 +84,3 @@ func (r *Registry) List() []string { } return names } - -// GetRecommendedJavaVersion returns the recommended Java version for a Minecraft version -func GetRecommendedJavaVersion(mcVersion string) int { - // Parse version to determine Java requirement - // MC 1.21+ requires Java 21 - // MC 1.20.5+ requires Java 21 - // MC 1.18 - 1.20.4 requires Java 17 - // MC 1.17 requires Java 16 - // Older versions can use Java 8 - - major, minor, patch := parseMinecraftVersion(mcVersion) - - if major >= 1 { - if minor >= 21 { - return 21 - } - if minor == 20 && patch >= 5 { - return 21 - } - if minor >= 18 { - return 17 - } - if minor >= 17 { - return 16 - } - } - - return 8 -} - -// parseMinecraftVersion parses a Minecraft version string into components -func parseMinecraftVersion(version string) (major, minor, patch int) { - // Handle formats like "1.20.4", "1.20", "1.20.4-pre1", etc. - var m, n, p int - fmt.Sscanf(version, "%d.%d.%d", &m, &n, &p) - if m == 0 { - fmt.Sscanf(version, "%d.%d", &m, &n) - } - return m, n, p -} diff --git a/internal/providers/spigot.go b/internal/providers/spigot.go index c06d866..f7df897 100644 --- a/internal/providers/spigot.go +++ b/internal/providers/spigot.go @@ -26,7 +26,8 @@ func (p *SpigotProvider) printPaperSuggestion() { fmt.Println("\n[WARNING] Spigot requires BuildTools compilation which is slow and complex.") fmt.Println(" Suggestion: Use Paper instead - it's a drop-in replacement for Spigot") fmt.Println(" that's directly downloadable and offers better performance.") - fmt.Println(" Run: mcrun run --mod=paper --version=\n") + fmt.Println(" Run: mcrun run --mod=paper --version=") + fmt.Println() } // ListVersions returns available Minecraft versions @@ -60,6 +61,6 @@ func (p *SpigotProvider) PostDownload(ctx context.Context, jarPath string, javaP } // GetRecommendedJavaVersion returns the recommended Java version -func (p *SpigotProvider) GetRecommendedJavaVersion(mcVersion string) int { - return GetRecommendedJavaVersion(mcVersion) +func (p *SpigotProvider) GetRecommendedJavaVersion(ctx context.Context, mcVersion string) int { + return GetRecommendedJavaVersion(ctx, mcVersion) } diff --git a/internal/providers/vanilla.go b/internal/providers/vanilla.go index e06a882..0f0b190 100644 --- a/internal/providers/vanilla.go +++ b/internal/providers/vanilla.go @@ -106,6 +106,6 @@ func (p *VanillaProvider) PostDownload(ctx context.Context, jarPath string, java } // GetRecommendedJavaVersion returns the recommended Java version -func (p *VanillaProvider) GetRecommendedJavaVersion(mcVersion string) int { - return GetRecommendedJavaVersion(mcVersion) +func (p *VanillaProvider) GetRecommendedJavaVersion(ctx context.Context, mcVersion string) int { + return GetRecommendedJavaVersion(ctx, mcVersion) } diff --git a/internal/providers/velocity.go b/internal/providers/velocity.go index 62655e0..2c49c0c 100644 --- a/internal/providers/velocity.go +++ b/internal/providers/velocity.go @@ -121,6 +121,6 @@ func (p *VelocityProvider) PostDownload(ctx context.Context, jarPath string, jav } // GetRecommendedJavaVersion returns Java 17 for modern Velocity -func (p *VelocityProvider) GetRecommendedJavaVersion(_ string) int { +func (p *VelocityProvider) GetRecommendedJavaVersion(_ context.Context, _ string) int { return 17 } diff --git a/internal/providers/waterfall.go b/internal/providers/waterfall.go index 449f18a..01122ec 100644 --- a/internal/providers/waterfall.go +++ b/internal/providers/waterfall.go @@ -121,6 +121,6 @@ func (p *WaterfallProvider) PostDownload(ctx context.Context, jarPath string, ja } // GetRecommendedJavaVersion returns Java 17 for modern Waterfall -func (p *WaterfallProvider) GetRecommendedJavaVersion(_ string) int { +func (p *WaterfallProvider) GetRecommendedJavaVersion(_ context.Context, _ string) int { return 17 } diff --git a/internal/server/runner.go b/internal/server/runner.go index bcb39d3..b2a0154 100644 --- a/internal/server/runner.go +++ b/internal/server/runner.go @@ -145,7 +145,7 @@ func (r *Runner) findJava(ctx context.Context, opts RunOptions, provider provide } // Use provider's recommended version (handles both MC servers and proxies) - recommended := provider.GetRecommendedJavaVersion(opts.Version) + recommended := provider.GetRecommendedJavaVersion(ctx, opts.Version) return r.javaManager.GetByVersion(ctx, recommended, true) } diff --git a/pkg/api/mojang/client.go b/pkg/api/mojang/client.go index 65f6ca6..12afea6 100644 --- a/pkg/api/mojang/client.go +++ b/pkg/api/mojang/client.go @@ -15,13 +15,24 @@ const ( // Client is the Mojang API client type Client struct { - http *utils.HTTPClient + http *utils.HTTPClient + manifestURL string } // NewClient creates a new Mojang API client func NewClient() *Client { + return NewClientWithManifestURL(versionManifestURL) +} + +// NewClientWithManifestURL creates a Mojang API client with a custom version +// manifest URL; an empty URL falls back to the official Mojang manifest +func NewClientWithManifestURL(manifestURL string) *Client { + if manifestURL == "" { + manifestURL = versionManifestURL + } return &Client{ - http: utils.NewHTTPClient(false), + http: utils.NewHTTPClient(false), + manifestURL: manifestURL, } } @@ -69,7 +80,7 @@ type VersionDetail struct { // GetVersionManifest fetches the version manifest from Mojang func (c *Client) GetVersionManifest(ctx context.Context) (*VersionManifest, error) { - data, err := c.http.Get(ctx, versionManifestURL) + data, err := c.http.Get(ctx, c.manifestURL) if err != nil { return nil, fmt.Errorf("failed to fetch version manifest: %w", err) } diff --git a/pkg/api/mojang/client_test.go b/pkg/api/mojang/client_test.go new file mode 100644 index 0000000..1b68a19 --- /dev/null +++ b/pkg/api/mojang/client_test.go @@ -0,0 +1,15 @@ +package mojang + +import "testing" + +func TestNewClientManifestURL(t *testing.T) { + if got := NewClient().manifestURL; got != versionManifestURL { + t.Errorf("NewClient() manifestURL = %q, want %q", got, versionManifestURL) + } + if got := NewClientWithManifestURL("").manifestURL; got != versionManifestURL { + t.Errorf("NewClientWithManifestURL(\"\") manifestURL = %q, want %q", got, versionManifestURL) + } + if got := NewClientWithManifestURL("http://example.com/m.json").manifestURL; got != "http://example.com/m.json" { + t.Errorf("NewClientWithManifestURL(custom) manifestURL = %q", got) + } +}