GioGio 的 Blog

没有启动器怎么下载并启动原版MC?附带下载渠道

2026-10-21mc,我的世界,教程,pcl2,pcl,html,启动器,powershell,脚本
## 使用Windows自带的powershell脚本下载并启动原版Minecraft
### 一、前置准备
- version.json(必须)
- powershell 5.1(2.0-5.1、7)
- ps脚本执行代码(文章末尾)
- 与 mc 版本对应的 Java(如 1.20.6 需要 Java 21)
- 脑子(必须)
### 二、说明
- 仅提供最基础的启动功能,如果需要高级功能,请移步mc wiki查阅启动项。
- 请将ps1脚本与json放至同一目录下。
- 请使用与 mc 版本对应的 Java,在 cmd 输入 `java -version` 查看。脚本运行时会打印该版本要求的 `java: 21+`
- 不设 `MC_JAVA` 时优先用环境里的 Java(`JAVA_HOME`,其次 `PATH` 里的 `javaw.exe`);设了 `MC_JAVA` 则优先生成到 `run.bat` 里
- 启动失败大概率是Java版本不一致
- 由于json结构问题,使用脚本来批量下载是比较合理的。
- 提供基础的并发下载
- 自动生成`run.bat`用于启动。
- client.jar 不走镜像。它来自 piston-data.mojang.com国内下载慢的话需要额外改 UR 加上这条,或预先手动放好对应版本的client.jar
- 无法自动使用模组加载器
- 注意尽量使用**全英文路径**。
- 默认使用官方源和环境Java。要换下载源或指定 Java,可以改脚本前两行的 `else { "" }`,也可以直接用环境变量(推荐,不用改脚本):

```powershell
# 换下载源(留空则为官方源)
$env:MC_MIRROR = "https://bmclapi2.bangbang93.com"
# 指定 Java:目录、bin 目录、或 javaw.exe 完整路径都行
$env:MC_JAVA = "C:\Program Files\Java\jdk-21"
# 其他:并发线程数 1-64;设置 MC_NO_PROGRESS 可关闭进度条
$env:MC_THREADS = 16

# 然后在同一窗口里运行脚本
.\jiaoben.ps1
```

对应的脚本内默认值(改这里则不依赖环境变量):
```powershell
# 留空则为官方源
$MIRROR = if ($env:MC_MIRROR) { $env:MC_MIRROR } else { "https://bmclapi2.bangbang93.com" }
# 留空则自动探测:先 JAVA_HOME,再 PATH 里的 javaw.exe
$JAVA_HINT = if ($env:MC_JAVA) { $env:MC_JAVA } else { "" }
```
### 三、如何获取json和脚本
进入[Minecraft 原版版本 JSON 获取](https://giogio.top/mc-download)
![操作示意图](https://giogio.top/api/articles/image/16/?name=1.png)
点进去会出现一堆代码,直接**右键→另存为**或者快捷键`win`+`s`下载,并命名为 `version.json`(文件名必须是 `version.json`,脚本按这个固定名字查找)

将文章最后的代码全部复制,并在文件夹下创建后缀名为`.ps1`的文件(如 `jiaoben.ps1`),**使用记事本打开**粘贴进去保存。

> 建议用记事本另存为时选择 **UTF-8** 编码;如果保存成 ANSI,中文注释可能乱码(脚本仍能运行,乱码只影响注释)。
完成后文件夹下会有
```plaintext
mc1.20.6/
├─ version.json
└─ jiaoben.ps1
```
在该文件夹里打开 **PowerShell**(在地址栏输入 `powershell` 回车,或按住 `Shift` 右键→“在此处打开 PowerShell 窗口”),执行:

```powershell
# 默认下载执行策略下不能直接运行脚本,先放开当前会话(只影响这个窗口)
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\jiaoben.ps1
```

![操作示意图](https://giogio.top/api/articles/image/16/?name=2.png)
等待下载完成后,双击 **run.bat**(脚本会在同目录生成 `run.bat` 和 `run.bat.utf8.bat`,后者是 UTF-8 版,`run.bat` 打不开时改双击它)。

---
### 脚本代码:
```bash
# jiaoben.ps1
# mc(原版)自动依赖下载+run.bat。
# 兼容 Win7 自带的 PowerShell 2.0 一直到 Win11 的 5.1,以及 PS 7。
# PS 2.0 会把中文注释按 ANSI 误读。出现问题请删除全文注释
# 环境变量覆盖:MC_MIRROR、MC_THREADS、MC_NO_PROGRESS、MC_JAVA
# MC_JAVA 可以是 java 目录(...\jdk17)、bin 目录或 javaw.exe 的完整路径;写进 run.bat 后优先使用。
$ErrorActionPreference = 'Stop'
$ProgressPreference    = 'SilentlyContinue'

$MIRROR = if ($env:MC_MIRROR) { $env:MC_MIRROR } else { "" }
$JAVA_HINT = if ($env:MC_JAVA) { $env:MC_JAVA } else { "" }
$THREADS = 8; if ($env:MC_THREADS) { try { $THREADS = [int]$env:MC_THREADS } catch { } }
if ($THREADS -lt 1) { $THREADS = 1 } elseif ($THREADS -gt 64) { $THREADS = 64 }
$MAX_RETRY = 3
$SHOW_PROGRESS = -not $env:MC_NO_PROGRESS
$UseCR = -not [Console]::IsOutputRedirected      # \r 原地刷新只在真实控制台下有意义
$UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) MCDownloader/2.0"

$Dir = Split-Path -Parent $MyInvocation.MyCommand.Definition
if (-not $Dir) { $Dir = (Get-Location).Path }

# 32 位系统 => 只能跑 32 位 JVM => 只能用 -x86 的 natives 包
$script:Is32 = $false
if ($env:PROCESSOR_ARCHITECTURE -eq 'x86' -and -not $env:PROCESSOR_ARCHITEW6432) { $script:Is32 = $true }

function J { param([string]$Base, [string]$Child) if ($Base) { $Base.TrimEnd('\') + '\' + ($Child -replace '/', '\').TrimStart('\') } else { $Child } }
function MK { param([string]$P) if ($P -and -not (Test-Path -LiteralPath $P)) { New-Item -ItemType Directory -Force -Path $P | Out-Null } }

# ------------------------------------------------------------------ JSON 解析 --
function JV {
    # 递归下降读取任意值;状态保存在 $script:T / $script:P
    while ($script:P -lt $script:T.Length -and " `t`r`n".IndexOf($script:T[$script:P]) -ge 0) { $script:P++ }
    $c = $script:T[$script:P]
    if ($c -eq '{') {
        $script:P++; $h = @{}
        if ($script:T[$script:P] -eq '}') { $script:P++; return $h }
        while ($true) {
            $k = JS
            while ($script:T[$script:P] -ne ':') { $script:P++ }
            $script:P++
            $h[$k] = JV
            while (" `t`r`n".IndexOf($script:T[$script:P]) -ge 0) { $script:P++ }
            if ($script:T[$script:P] -eq '}') { $script:P++; return $h }
            $script:P++
        }
    }
    if ($c -eq '[') {
        $script:P++; $l = New-Object System.Collections.ArrayList
        if ($script:T[$script:P] -eq ']') { $script:P++; return , $l.ToArray() }
        while ($true) {
            [void]$l.Add((JV))
            while (" `t`r`n".IndexOf($script:T[$script:P]) -ge 0) { $script:P++ }
            if ($script:T[$script:P] -eq ']') { $script:P++; return , $l.ToArray() }
            $script:P++
        }
    }
    if ($c -eq '"') { return (JS) }
    if ($c -eq 't') { $script:P += 4; return $true }
    if ($c -eq 'f') { $script:P += 5; return $false }
    if ($c -eq 'n') { $script:P += 4; return $null }
    $s = $script:P
    while ($script:P -lt $script:T.Length -and '+-.eE0123456789'.IndexOf($script:T[$script:P]) -ge 0) { $script:P++ }
    [double]::Parse($script:T.Substring($s, $script:P - $s), [Globalization.CultureInfo]::InvariantCulture)
}

function JS {
    # 读取当前位置的 JSON 字符串
    while (" `t`r`n".IndexOf($script:T[$script:P]) -ge 0) { $script:P++ }
    $script:P++
    $s = $script:P; $esc = $false
    while ($true) {
        $c = $script:T[$script:P]
        if ($c -eq '\') { $esc = $true; $script:P += 2; continue }
        if ($c -eq '"') { break }
        $script:P++
    }
    $r = $script:T.Substring($s, $script:P - $s); $script:P++
    if (-not $esc) { return $r }
    $sb = New-Object Text.StringBuilder; $i = 0
    while ($i -lt $r.Length) {
        $c = $r[$i]
        if ($c -ne '\' -or $i + 1 -ge $r.Length) { [void]$sb.Append($c); $i++; continue }
        $e = $r[$i + 1]
        if ($e -eq 'u' -and $i + 5 -lt $r.Length) {
            $n = [Convert]::ToInt32($r.Substring($i + 2, 4), 16)
            if ($n -ge 0xD800 -and $n -le 0xDBFF -and $i + 11 -lt $r.Length -and $r[$i + 6] -eq '\') {
                $lo = [Convert]::ToInt32($r.Substring($i + 8, 4), 16)
                [void]$sb.Append([char]::ConvertFromUtf32(0x10000 + ($n - 0xD800) * 0x400 + ($lo - 0xDC00)))
                $i += 12; continue
            }
            [void]$sb.Append([char]$n); $i += 6; continue
        }
        $m = @{ '"' = '"'; '\' = '\'; '/' = '/'; b = [char]8; f = [char]12; n = [char]10; r = [char]13; t = [char]9 }
        if ($m.ContainsKey([string]$e)) { [void]$sb.Append($m[[string]$e]); $i += 2 } else { [void]$sb.Append($c); $i++ }
    }
    $sb.ToString()
}

function Json { param([string]$Text) $script:T = $Text.TrimStart([char]65279); $script:P = 0; JV }

function V { param($O, [string]$K) if ($O -is [hashtable] -and $O.ContainsKey($K)) { $O[$K] } else { $null } }

function ReadText {
    param([string]$P)
    $b = [IO.File]::ReadAllBytes($P)
    try { return (New-Object Text.UTF8Encoding($false, $true)).GetString($b) } catch { return [Text.Encoding]::Default.GetString($b) }
}

# -------------------------------------------------------------------- 网络层 --
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { }

function UR { param([string]$U)   # 套用镜像地址
    if (-not $MIRROR) { return $U }
    $m = $MIRROR.TrimEnd('/')
    if ($U.StartsWith("https://libraries.minecraft.net"))          { return $U.Replace("https://libraries.minecraft.net", "$m/libraries") }
    if ($U.StartsWith("https://resources.download.minecraft.net")) { return $U.Replace("https://resources.download.minecraft.net", "$m/resources") }
    $U
}

function Grab { param([string]$U, [string]$P)   # 单文件下载,不打印过程
    if ((Test-Path -LiteralPath $P) -and (Get-Item -LiteralPath $P).Length -gt 0) { return $true }
    MK (Split-Path -Parent $P)
    $t = "$P.part"
    for ($a = 1; $a -le $MAX_RETRY; $a++) {
        $w = New-Object Net.WebClient
        try {
            $w.Headers.Add("User-Agent", $UA); $w.DownloadFile((UR $U), $t)
            if ((Get-Item -LiteralPath $t).Length -gt 0) { Move-Item -LiteralPath $t -Destination $P -Force; return $true }
        } catch {
            if (Test-Path -LiteralPath $t) { Remove-Item -LiteralPath $t -Force -ErrorAction SilentlyContinue }
            if ($a -lt $MAX_RETRY) { Start-Sleep -Seconds (2 * $a) }
        } finally { $w.Dispose() }
    }
    Write-Host "  [FAIL] $U"
    $false
}

# ---------------------------------------------------------------- 并发下载 --
# 在 worker runspace 内执行。结果写入线程安全队列,
# 因为 BeginInvoke 不会捕获 AddScript 的管道输出。
$Worker = @'
param($Jobs, $Retry, $UA, $Sink)
foreach ($j in $Jobs) {
    $url, $dst = $j
    $ok = $false
    if (Test-Path -LiteralPath $dst) { $ok = (Get-Item -LiteralPath $dst).Length -gt 0 }
    if (-not $ok) {
        $tmp = "$dst.part"
        $d = Split-Path -Parent $dst
        if (-not (Test-Path -LiteralPath $d)) { New-Item -ItemType Directory -Force -Path $d | Out-Null }
        for ($a = 1; $a -le $Retry -and -not $ok; $a++) {
            $w = New-Object Net.WebClient
            try {
                $w.Headers.Add("User-Agent", $UA)
                $w.DownloadFile($url, $tmp)
                if ((Get-Item -LiteralPath $tmp).Length -gt 0) { Move-Item -LiteralPath $tmp -Destination $dst -Force; $ok = $true }
            } catch {
                if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }
                if ($a -lt $Retry) { Start-Sleep -Seconds (2 * $a) }
            } finally { $w.Dispose() }
        }
    }
    $Sink.Enqueue($ok)
}
'@

function Human { param([double]$S) if ($S -lt 0 -or $S -gt 359999) { "--:--" } elseif ($S -ge 3600) { '{0}:{1:00}:{2:00}' -f [int]($S / 3600), [int]($S % 3600 / 60), [int]($S % 60) } else { '{0:00}:{1:00}' -f [int]($S / 60), [int]($S % 60) } }

# $Items:元素为 @(url, 保存路径) 的数组
function Batch { param($Items, [string]$Label)
    $all = @($Items); $total = $all.Count
    if ($total -eq 0) { return 0 }

    $jobs = @()
    foreach ($it in $all) { if (-not ((Test-Path -LiteralPath $it[1]) -and (Get-Item -LiteralPath $it[1]).Length -gt 0)) { $jobs += , @((UR $it[0]), $it[1]) } }
    $skip = $total - $jobs.Count
    Write-Host ""
    Write-Host ("{0}: {1} file(s), {2} present, {3} to download, {4} thread(s)" -f $Label, $total, $skip, $jobs.Count, $THREADS)
    if ($jobs.Count -eq 0) { return $total }

    # 按线程数切成连续的分块,每块交给一个 worker
    $n = [Math]::Min($THREADS, $jobs.Count)
    $size = [Math]::Ceiling($jobs.Count / $n)
    $runs = @()
    for ($i = 0; $i -lt $jobs.Count; $i += $size) { $runs += , @($jobs[$i..([Math]::Min($i + $size - 1, $jobs.Count - 1))]) }

    $sink = New-Object 'System.Collections.Concurrent.ConcurrentQueue[bool]'
    $pool = $null
    if ($runs.Count -gt 1) {
        try { $pool = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool($runs.Count, $runs.Count); $pool.Open() } catch { $pool = $null }
    }

    if (-not $pool) {                                   # 无法并发时退化为顺序下载
        & ([ScriptBlock]::Create($Worker)) $runs[0] $MAX_RETRY $UA $sink
        $ok = 0; $v = $false
        while ($sink.TryDequeue([ref]$v)) { if ($v) { $ok++ } }
        Write-Host ("{0}: done, ok {1}, failed {2}" -f $Label, ($ok + $skip), ($jobs.Count - $ok))
        return $ok + $skip
    }

    $hs = @()
    foreach ($r in $runs) {
        $ps = [PowerShell]::Create(); $ps.RunspacePool = $pool
        [void]$ps.AddScript($Worker).AddArgument($r).AddArgument($MAX_RETRY).AddArgument($UA).AddArgument($sink)
        $hs += , @($ps, $ps.BeginInvoke())
    }

    $ok = 0; $done = 0; $spin = 0; $marks = '|', '/', '-', '\'
    $last = Get-Date; $prev = 0; $rate = 0.0
    try {
        do {
            Start-Sleep -Milliseconds 300
            $v = $false
            while ($sink.TryDequeue([ref]$v)) { $done++; if ($v) { $ok++ } }
            if (-not $SHOW_PROGRESS) { continue }
            $now = Get-Date; $span = ($now - $last).TotalSeconds
            if ($span -ge 1) { $rate = ($done - $prev) / $span; $prev = $done; $last = $now }
            $p = $skip + $done
            $eta = if ($rate -gt 0.2) { Human (($total - $p) / $rate) } else { "--:--" }
            $line = "{0}: {1,3}% [{2}/{3}] fail {4} {5} {6:F1}/s eta {7}" -f $Label, [int]($p * 100 / $total), $p, $total, ($done - $ok), $marks[($spin++) % 4], $rate, $eta
            if ($UseCR) { Write-Host ("`r" + $line.PadRight(100)) -NoNewline } else { Write-Host $line }
        } while (@($hs | Where-Object { $_[0].InvocationStateInfo.State -eq 'Running' }).Count -gt 0)

        foreach ($h in $hs) {
            $h[0].EndInvoke($h[1]) | Out-Null
            $h[0].Streams.Error | ForEach-Object { Write-Host ("  [warn] " + $_.ToString().Split("`n")[0]) }
            $h[0].Dispose()
        }
        $v = $false
        while ($sink.TryDequeue([ref]$v)) { $done++; if ($v) { $ok++ } }
    } finally { if ($pool) { $pool.Close(); $pool.Dispose() } }

    if ($SHOW_PROGRESS -and $UseCR) { Write-Host "" }
    Write-Host ("{0}: done, ok {1}, failed {2}" -f $Label, ($ok + $skip), ($jobs.Count - $ok))
    $ok + $skip
}

# ---------------------------------------------------------------------- 解压 --
# 先试 .NET ZipFile(.NET 4.5+),失败再退回 Shell.Application(Win2000 起都有)
function Unzip { param([string]$Zip, [string]$To)
    MK $To
    try {
        Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop
        $z = [IO.Compression.ZipFile]::OpenRead($Zip)
        try {
            foreach ($e in $z.Entries) {
                if ($e.Length -le 0) { continue }
                $p = J $To $e.FullName
                MK (Split-Path -Parent $p)
                [IO.Compression.ZipFileExtensions]::ExtractToFile($e, $p, $true)
            }
        } finally { $z.Dispose() }
        return $true
    } catch { }
    try {
        $sh = New-Object -ComObject Shell.Application
        $src = $sh.NameSpace($Zip); $dst = $sh.NameSpace($To)
        if (-not $src -or -not $dst) { throw "cannot open" }
        $items = $src.Items(); $want = @($items).Count
        $dst.CopyHere($items, 0x14)
        for ($i = 0; $i -lt 600; $i++) {
            if (@(Get-ChildItem -LiteralPath $To -Recurse -File -ErrorAction SilentlyContinue).Count -ge $want - 2) { break }
            Start-Sleep -Milliseconds 200
        }
        return $true
    } catch { return $false }
}

# --------------------------------------------------------------- 库匹配规则 --
# Mojang rules:默认拒绝,最后一条命中的规则生效;只认 features 为 false 的情况。
function Allow { param($Rules)
    if ($Rules -eq $null) { return $true }
    if ($Rules -isnot [array]) { $Rules = @($Rules) }
    $a = $false
    foreach ($r in $Rules) {
        if ($r -isnot [hashtable]) { continue }
        $act = V $r "action"; if (-not $act) { continue }
        $m = $true
        $os = V $r "os"
        if ($os) {
            $n = V $os "name"; if ($n -and $n -ne "windows") { $m = $false }
            $ar = V $os "arch"; if ($m -and $ar -and $ar -eq "arm64") { $m = $false }
        }
        $f = V $r "features"
        if ($m -and $f) { foreach ($k in $f.Keys) { if ($f[$k] -eq $true) { $m = $false; break } } }
        if ($m) { $a = ($act -eq "allow") }
    }
    $a
}

# ====================================================================== 主流程 ===
Write-Host "=============================================="
Write-Host " Minecraft vanilla setup"
Write-Host " dir     : $Dir"
Write-Host " mirror  : $(if ($MIRROR) { $MIRROR } else { 'mojang (official)' })"
Write-Host " java    : $(if ($JAVA_HINT) { $JAVA_HINT } else { 'auto detect' })"
Write-Host "=============================================="

$verPath = J $Dir "version.json"
if (-not (Test-Path -LiteralPath $verPath)) { Write-Host "[ERROR] version.json not found"; exit 1 }

$LIB = J $Dir "libraries"; $NatDir = J $Dir "natives"; $AST = J $Dir "assets"
MK $LIB; MK $NatDir; MK (J $AST "indexes"); MK (J $AST "objects")

$ver  = Json (ReadText $verPath)
$id   = V $ver "id"; $main = V $ver "mainClass"
$ain  = V $ver "assetIndex"; $aid = V $ain "id"; $aurl = V $ain "url"
$java = V (V $ver "javaVersion") "majorVersion"
Write-Host ""
Write-Host "version : $id    assets: $aid    java: $java+"
Write-Host ""

# 1) 客户端 jar
$c = V (V $ver "downloads") "client"
if ($c) { Write-Host "downloading client.jar ..."; [void](Grab (V $c "url") (J $Dir "client.jar")) }

# 2) 库文件(并发下载)+ 解压 natives
$libs = @(V $ver "libraries"); $cp = @(); $items = @(); $natives = @()
foreach ($l in $libs) {
    if ($l -isnot [hashtable]) { continue }
    if (-not (Allow (V $l "rules"))) { continue }
    $nm = V $l "name"
    if ($nm -match ':natives-(linux|macos)') { continue }
    if ($nm -match ':natives-windows-arm64') { continue }    # arm64 上跑 x64 版本即可
    $dl = V $l "downloads"; if (-not $dl) { continue }

    $art = V $dl "artifact"; $isNat = [bool]($nm -match ':natives-windows')
    $no = V $l "natives"                                      # 旧版 JSON 的写法
    if ($no) {
        $k = V $no "windows"
        $cls = V $dl "classifiers"
        if ($k -and $cls -and (V $cls $k)) { $art = V $cls $k; $isNat = $true }
    }
    if (-not $art -or -not (V $art "path") -or -not (V $art "url")) { continue }

    $save = J $LIB (V $art "path")
    if ($isNat) { $natives += $save } else { $cp += $save }
    $items += , @((V $art "url"), $save)
}
$libOk = Batch $items "libraries"

foreach ($s in $natives) {
    if (-not (Test-Path -LiteralPath $s)) { continue }
    # 64 位 JVM 跳过 32 位 natives(反之亦然)。混用会让错误位数的 lwjgl.dll
    # 覆盖进 natives\,导致启动时报 Failed to locate library。
    $isX86 = (Split-Path -Leaf $s) -match '-x86\.jar$'
    if ($isX86 -and -not $script:Is32) { continue }
    if (-not $isX86 -and $script:Is32) { continue }
    Write-Host ("  [unzip] " + (Split-Path -Leaf $s))
    if (-not (Unzip $s $NatDir)) { Write-Host "  [WARN] unzip failed: $s" }
}
# 把 windows/x64/<包名>/*.dll 打平到 natives/*.dll,这样 -Djava.library.path=natives 才找得到
$nroot = (Get-Item -LiteralPath $NatDir).FullName
Get-ChildItem -LiteralPath $NatDir -Recurse -Filter *.dll -ErrorAction SilentlyContinue | ForEach-Object {
    if ($_.DirectoryName -ne $nroot) { $t = J $NatDir $_.Name; if (-not (Test-Path -LiteralPath $t)) { Copy-Item -LiteralPath $_.FullName -Destination $t -Force } }
}
$dlls = @(Get-ChildItem -LiteralPath $NatDir -Filter *.dll -ErrorAction SilentlyContinue).Count
Write-Host ""
Write-Host "libraries: $libOk/$($items.Count) ok    natives: $dlls dll(s)"
if ($dlls -eq 0) { Write-Host "  [WARN] no dll extracted" }

# 3) log4j 配置
$logArg = @()
$lc = V (V $ver "logging") "client"
if ($lc) {
    $lf = V $lc "file"; $lid = (V $lf "id"); if (-not $lid) { $lid = "client.xml" }
    $rel = J (J "assets" "log_configs") $lid
    if ((V $lf "url") -and -not (Test-Path -LiteralPath (J $Dir $rel))) { [void](Grab (V $lf "url") (J $Dir $rel)) }
    if (Test-Path -LiteralPath (J $Dir $rel)) { $logArg = @("-Dlog4j.configurationFile=" + ($rel -replace '/', '\')) }
}

# 4) 资源索引 + 资源文件(并发下载)
$irel = J (J "assets" "indexes") "$aid.json"; $ifull = J $Dir $irel
if ($aurl -and -not (Test-Path -LiteralPath $ifull)) { [void](Grab $aurl $ifull) }
if (Test-Path -LiteralPath $ifull) {
    $obj = V (Json (ReadText $ifull)) "objects"
    if ($obj) {
        $ai = @()
        foreach ($k in $obj.Keys) {
            $h = V $obj[$k] "hash"; if (-not $h) { continue }
            $ai += , @("https://resources.download.minecraft.net/$($h.Substring(0,2))/$h", (J (J (J $AST "objects") $h.Substring(0,2)) $h))
        }
        [void](Batch $ai "assets")
    }
}

# 5) 生成 run.bat
Write-Host ""
Write-Host "generating run.bat ..."
$cpAll = ($cp -join ';') + ';' + (J $Dir "client.jar")

$jvm = "-Xms2G -Xmx4G -Djava.library.path=natives -Dminecraft.launcher.brand=custom " +
       "-Dminecraft.launcher.version=1.0 -Dminecraft.applet.TargetDirectory=. -Duser.home=. " + ($logArg -join ' ')
$game = "$main --username Player --version `"$id`" --gameDir . --assetsDir assets " +
        "--assetIndex $aid --uuid 00000000000000000000000000000000 --accessToken 0 " +
        "--versionType release --width 960 --height 540"

# 把 MC_JAVA 规范成 javaw.exe 的路径(允许传 java 目录、bin 目录或 exe 本身)
$javaExe = ""
if ($JAVA_HINT) {
    $jp = $JAVA_HINT.Trim().Trim('"')
    switch -Wildcard ($jp.ToLowerInvariant()) {
        '*.exe' { $javaExe = $jp }
        default {
            if (Test-Path -LiteralPath (J $jp "bin\javaw.exe")) { $javaExe = J $jp "bin\javaw.exe" }
            elseif (Test-Path -LiteralPath (J $jp "javaw.exe")) { $javaExe = J $jp "javaw.exe" }
            else { Write-Host ""; Write-Host "[WARN] MC_JAVA not found: $JAVA_HINT (run.bat will auto detect)" }
        }
    }
}

$bat = @(
    "@echo off",
    "setlocal",
    "cd /d `"%~dp0`"",
    "",
    "rem ---- 查找 Java $java+ 运行时 ----",
    "set `"JAVA_EXE=`""
)
# 用户指定的路径排在自动探测之前
if ($javaExe) { $bat += "if exist `"$javaExe`" set `"JAVA_EXE=$javaExe`"" }
$bat += @(
    "if not defined JAVA_EXE if exist `"%~dp0jre\bin\javaw.exe`" set `"JAVA_EXE=%~dp0jre\bin\javaw.exe`"",
    "rem JAVA_EXE 未指定(或指定路径不存在)时,优先用环境变量里的 Java",
    "if not defined JAVA_EXE if defined JAVA_HOME if exist `"%JAVA_HOME%\bin\javaw.exe`" set `"JAVA_EXE=%JAVA_HOME%\bin\javaw.exe`"",
    "if not defined JAVA_EXE for /d %%D in (`"C:\Program Files\Java\jdk-$java*`") do if exist `"%%D\bin\javaw.exe`" set `"JAVA_EXE=%%D\bin\javaw.exe`"",
    "if not defined JAVA_EXE for /d %%D in (`"C:\Program Files\Eclipse Adoptium\jdk-$java*`") do if exist `"%%D\bin\javaw.exe`" set `"JAVA_EXE=%%D\bin\javaw.exe`"",
    "if not defined JAVA_EXE for /d %%D in (`"C:\Program Files\Microsoft\jdk-$java*`") do if exist `"%%D\bin\javaw.exe`" set `"JAVA_EXE=%%D\bin\javaw.exe`"",
    "if not defined JAVA_EXE set `"JAVA_EXE=javaw.exe`"",
    "",
    "`"%JAVA_EXE%`" $jvm -cp `"$cpAll`" $game",
    "if errorlevel 1 goto fail",
    "goto end",
    "",
    ":fail",
    "echo.",
    "echo Launch failed - this version needs Java $java.",
    "echo JAVA_EXE=%JAVA_EXE%",
    "pause",
    "",
    ":end",
    "endlocal"
)

$batText = ($bat -join "`r`n") + "`r`n"
[IO.File]::WriteAllText((J $Dir "run.bat"), $batText, [Text.Encoding]::Default)
[IO.File]::WriteAllText((J $Dir "run.bat.utf8.bat"), $batText, (New-Object Text.UTF8Encoding($false)))

Write-Host ""
Write-Host "done - run run.bat (needs Java $java+)"
if ($javaExe) { Write-Host "java    : $javaExe" }

```

相关文章