Skip to content

Commit c76b2dc

Browse files
committed
CF测试安装
1 parent 0ec5ea8 commit c76b2dc

File tree

6 files changed

+459
-77
lines changed

6 files changed

+459
-77
lines changed

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/autoinst/AutoInstall
22

3-
go 1.22.5
3+
go 1.24.8
4+
5+
require golang.org/x/text v0.30.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
2+
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=

pkg/cf.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package pkg
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"sync"
12+
13+
"github.com/autoinst/AutoInstall/core"
14+
)
15+
16+
type CurseForgeManifest struct {
17+
Minecraft struct {
18+
Version string `json:"version"`
19+
ModLoaders []struct {
20+
ID string `json:"id"`
21+
Primary bool `json:"primary"`
22+
} `json:"modLoaders"`
23+
} `json:"minecraft"`
24+
Overrides string `json:"overrides"`
25+
Files []struct {
26+
ProjectID int `json:"projectID"`
27+
FileID int `json:"fileID"`
28+
Required bool `json:"required"`
29+
} `json:"files"`
30+
}
31+
32+
// resolveCFDownloadURL 使用 CurseForge API 获取可用直链
33+
// 需要环境变量 CF_API_KEY,可在 https://console.curseforge.com/ 申请
34+
func resolveCFDownloadURL(projectID, fileID int) (string, error) {
35+
apiKey := os.Getenv("CF_API_KEY")
36+
if apiKey == "" {
37+
return "", fmt.Errorf("缺少 CF_API_KEY")
38+
}
39+
// 直接获取下载直链
40+
req, err := http.NewRequest("GET", fmt.Sprintf("https://api.curseforge.com/v1/mods/%d/files/%d/download-url", projectID, fileID), nil)
41+
if err != nil {
42+
return "", err
43+
}
44+
req.Header.Set("Accept", "application/json")
45+
req.Header.Set("x-api-key", apiKey)
46+
resp, err := http.DefaultClient.Do(req)
47+
if err != nil {
48+
return "", err
49+
}
50+
defer resp.Body.Close()
51+
if resp.StatusCode != http.StatusOK {
52+
b, _ := io.ReadAll(resp.Body)
53+
return "", fmt.Errorf("CF API 响应异常: %d %s", resp.StatusCode, string(b))
54+
}
55+
var out struct {
56+
Data string `json:"data"`
57+
}
58+
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
59+
return "", err
60+
}
61+
if out.Data == "" {
62+
return "", fmt.Errorf("CF API 未返回下载地址")
63+
}
64+
return out.Data, nil
65+
}
66+
67+
func CurseForge(file string, MaxCon int, Args string) {
68+
// 假定压缩包已在 modpack.go 中解压
69+
// 1) 读取 manifest.json
70+
mf := "manifest.json"
71+
if file != "" && strings.HasSuffix(strings.ToLower(file), ".json") {
72+
mf = file
73+
}
74+
mfPath := filepath.Join("./", mf)
75+
mfFile, err := os.Open(mfPath)
76+
if err != nil {
77+
fmt.Println("未找到 manifest.json,停止 CurseForge 安装流程")
78+
os.Exit(0)
79+
}
80+
defer mfFile.Close()
81+
82+
var manifest CurseForgeManifest
83+
if err := json.NewDecoder(mfFile).Decode(&manifest); err != nil {
84+
panic(fmt.Errorf("解析 manifest.json 失败: %w", err))
85+
}
86+
87+
// 2) 迁移 overrides 内容到根目录
88+
overridesPath := filepath.Join("./", manifest.Overrides)
89+
if manifest.Overrides == "" {
90+
overridesPath = filepath.Join("./", "overrides")
91+
}
92+
if stat, statErr := os.Stat(overridesPath); statErr == nil && stat.IsDir() {
93+
err = filepath.Walk(overridesPath, func(path string, info os.FileInfo, walkErr error) error {
94+
if walkErr != nil {
95+
return walkErr
96+
}
97+
if !info.IsDir() {
98+
relPath, err := filepath.Rel(overridesPath, path)
99+
if err != nil {
100+
return err
101+
}
102+
destPath := filepath.Join("./", relPath)
103+
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
104+
return err
105+
}
106+
if err := os.Rename(path, destPath); err != nil {
107+
return err
108+
}
109+
}
110+
return nil
111+
})
112+
if err != nil {
113+
panic(fmt.Sprintf("移动 overrides 文件失败: %v", err))
114+
}
115+
_ = os.RemoveAll(overridesPath)
116+
}
117+
118+
// 3) 生成 inst.json(Minecraft 版本 + 加载器信息)
119+
inst := core.InstConfig{
120+
Version: manifest.Minecraft.Version,
121+
Download: "bmclapi",
122+
MaxConnections: 32,
123+
Argsment: "-Xmx{maxmen}M -Xms{maxmen}M -XX:+AlwaysPreTouch -XX:+DisableExplicitGC -XX:+ParallelRefProcEnabled -XX:+PerfDisableSharedMem -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1HeapRegionSize=8M -XX:G1HeapWastePercent=5 -XX:G1MaxNewSizePercent=40 -XX:G1MixedGCCountTarget=4 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1NewSizePercent=30 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:G1ReservePercent=20 -XX:InitiatingHeapOccupancyPercent=15 -XX:MaxGCPauseMillis=200 -XX:MaxTenuringThreshold=1 -XX:SurvivorRatio=32 -Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true",
124+
}
125+
// 从 modLoaders 选择 primary 或第一个
126+
loaderID := ""
127+
if len(manifest.Minecraft.ModLoaders) > 0 {
128+
for _, ml := range manifest.Minecraft.ModLoaders {
129+
if ml.Primary {
130+
loaderID = ml.ID
131+
break
132+
}
133+
}
134+
if loaderID == "" {
135+
loaderID = manifest.Minecraft.ModLoaders[0].ID
136+
}
137+
}
138+
// 常见格式: forge-<ver> / fabric-<loader>
139+
if strings.HasPrefix(strings.ToLower(loaderID), "neoforge-") {
140+
inst.Loader = "neoforge"
141+
inst.LoaderVersion = strings.TrimPrefix(loaderID, "neoforge-")
142+
} else if strings.HasPrefix(strings.ToLower(loaderID), "forge-") {
143+
inst.Loader = "forge"
144+
inst.LoaderVersion = strings.TrimPrefix(loaderID, "forge-")
145+
} else if strings.HasPrefix(strings.ToLower(loaderID), "fabric-") {
146+
inst.Loader = "fabric"
147+
inst.LoaderVersion = strings.TrimPrefix(loaderID, "fabric-")
148+
} else {
149+
// 默认回退 fabric(部分清单可能只有 minecraft 原版)
150+
if loaderID == "" {
151+
inst.Loader = "vanilla"
152+
inst.LoaderVersion = ""
153+
} else {
154+
// 未识别时尽量按原样放入 fabric 字段,避免阻断
155+
inst.Loader = "fabric"
156+
inst.LoaderVersion = loaderID
157+
}
158+
}
159+
jsonData, err := json.MarshalIndent(inst, "", " ")
160+
if err != nil {
161+
panic(err)
162+
}
163+
if err := os.WriteFile("inst.json", jsonData, 0777); err != nil {
164+
panic(err)
165+
}
166+
167+
// 4) 下载 mods:通过 CF API 解析下载直链;放置到 ./mods 目录
168+
if len(manifest.Files) == 0 {
169+
return
170+
}
171+
172+
if os.Getenv("CF_API_KEY") == "" {
173+
fmt.Println("未设置 CF_API_KEY,跳过 CurseForge 模组下载。已完成 overrides 应用与 inst.json 生成。")
174+
fmt.Println("如需自动下载 CF 模组,请设置环境变量 CF_API_KEY 后重试。")
175+
return
176+
}
177+
178+
var wg sync.WaitGroup
179+
maxConcurrency := 24
180+
if MaxCon > 0 {
181+
maxConcurrency = MaxCon
182+
}
183+
semaphore := make(chan struct{}, maxConcurrency)
184+
errChan := make(chan error, len(manifest.Files))
185+
186+
modsDir := filepath.Join(".", "mods")
187+
_ = os.MkdirAll(modsDir, os.ModePerm)
188+
189+
for _, mf := range manifest.Files {
190+
if !mf.Required {
191+
continue
192+
}
193+
wg.Add(1)
194+
semaphore <- struct{}{}
195+
196+
go func(entry struct {
197+
ProjectID int `json:"projectID"`
198+
FileID int `json:"fileID"`
199+
Required bool `json:"required"`
200+
}) {
201+
defer func() { <-semaphore; wg.Done() }()
202+
203+
// 解析直链并以 URL 最末文件名保存
204+
url, err := resolveCFDownloadURL(entry.ProjectID, entry.FileID)
205+
if err != nil {
206+
errChan <- err
207+
return
208+
}
209+
// 从 URL 提取文件名
210+
segs := strings.Split(url, "/")
211+
filename := fmt.Sprintf("%d.jar", entry.FileID)
212+
if len(segs) > 0 && segs[len(segs)-1] != "" {
213+
filename = segs[len(segs)-1]
214+
}
215+
dst := filepath.Join(modsDir, filename)
216+
if err := os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
217+
errChan <- err
218+
return
219+
}
220+
fmt.Println("尝试下载:", url)
221+
if err := core.DownloadFile(url, dst); err != nil {
222+
errChan <- fmt.Errorf("下载失败(Project %d, File %d): %v", entry.ProjectID, entry.FileID, err)
223+
return
224+
}
225+
}(mf)
226+
}
227+
228+
wg.Wait()
229+
close(errChan)
230+
for err := range errChan {
231+
if err != nil {
232+
panic(err)
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)