Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions assets/bundles/bundle-mdtx.properties
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ settingV2.arcTurretPlaceCheck.name = 炮台放置时显示禁建范围
settingV2.arcTurretPlacementItem.name = 炮台显示不同弹药射程
settingV2.githubMirror.name = GitHub镜像加速(WZ镜像)
settingV2.githubMirror.description = 优化全球服务器列表及Mod浏览器功能
settingV2.githubAcceleration.enabled.name = 启用 GitHub 加速
settingV2.githubAcceleration.cache.name = 启用资源缓存
settingV2.githubAcceleration.cacheExpire.name = 缓存过期时间
settingV2.githubAcceleration.maxRetries.name = 最大重试次数
settingV2.githubAcceleration.proxies.name = 代理列表配置
settingV2.replayRecord.name = 多人游戏录像
settingV2.replayRecord.description = 自动录制游玩过程,输出在saves文件夹\n录像文件较大,记得整理
settingV2.maxSchematicSize.name = 最大选择框(蓝图)大小
Expand Down
5 changes: 5 additions & 0 deletions assets/bundles/bundle-mdtx_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ settingV2.arcTurretPlaceCheck.name = Show No-Build Zones When Placing Turrets
settingV2.arcTurretPlacementItem.name = Show Different Ammo Ranges for Turrets
settingV2.githubMirror.name = GitHub Mirror Acceleration (WZ Mirror)
settingV2.githubMirror.description = Optimizes global server list and mod browser functions
settingV2.githubAcceleration.enabled.name = Enable GitHub Acceleration
settingV2.githubAcceleration.cache.name = Enable Resource Caching
settingV2.githubAcceleration.cacheExpire.name = Cache Expiration Time
settingV2.githubAcceleration.maxRetries.name = Max Retry Attempts
settingV2.githubAcceleration.proxies.name = Proxy List Configuration
settingV2.replayRecord.name = Multiplayer Recording
settingV2.replayRecord.description = Automatically records gameplay, output in saves folder\nRecording files may be large, remember to clean up
settingV2.maxSchematicSize.name = Max Selection (Schematic) Size
Expand Down
21 changes: 1 addition & 20 deletions src/mindustryX/Hooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static void beforeInit(){
SettingsV2.INSTANCE.init();
DebugUtil.init();//this is safe, and better at beforeInit,
BindingExt.init();
GithubAcceleration.INSTANCE.loadProxies(); // 初始化GitHub加速
Events.on(ClientLoadEvent.class, (e) -> MetricCollector.INSTANCE.onLaunch());
//deprecated Java 8
if(!OS.isAndroid && Strings.parseInt(OS.javaVersion.split("\\.")[0]) < 17){
Expand All @@ -30,11 +31,6 @@ public static void beforeInit(){
ui.showInfo("Java版本过低,不受支持(" + OS.javaVersion + ")。请使用Java 17或更高版本运行MindustryX。\n[grey]该警告不存在设置,请更新Java版本。");
});
}
try{
Http.onBeforeRequest = Hooks::onHttp;
}catch(NoSuchFieldError e){
Log.warn("Failed to set Http.onBeforeRequest " + e.toString());
}
}

/** invoke after loading, just before `Mod::init` */
Expand All @@ -57,21 +53,6 @@ public void init(){
}
}

@SuppressWarnings("unused")//call before arc.util.Http$HttpRequest.block
public static void onHttp(Http.HttpRequest req){
if(VarsX.githubMirror.get()){
try{
String url = req.url;
String host = new URL(url).getHost();
if(host.contains("github.com") || host.contains("raw.githubusercontent.com")){
url = "https://gh.tinylake.top/" + url;
req.url = url;
}
}catch(Exception e){
//ignore
}
}
}

public static @Nullable String onHandleSendMessage(String message, @Nullable Player sender){
if(message == null) return null;
Expand Down
267 changes: 267 additions & 0 deletions src/mindustryX/features/GithubAcceleration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
package mindustryX.features

import arc.Core
import arc.scene.ui.CheckBox
import arc.scene.ui.layout.Table
import arc.util.Http
import arc.util.Log
import arc.util.serialization.Jval
import mindustry.Vars
import mindustry.gen.Icon
import mindustry.ui.Styles
import mindustryX.features.SettingsV2.*
import java.net.URL
import java.util.concurrent.ConcurrentHashMap

/**
* GitHub加速服务 - 单文件实现
* 通过包装 HttpRequest.block 实现重试和缓存机制
*/
object GithubAcceleration {

// ========== 代理配置数据类 ==========
data class ProxyConfig(
var id: Int,
var url: String,
var name: String,
var enabled: Boolean = true,
var assetEnabled: Boolean = true,
var apiEnabled: Boolean = true,
val locked: Boolean = false
) {
constructor() : this(0, "https://", "新代理", true, true, false, false)

companion object {
fun defaults() = listOf(
ProxyConfig(0, "https://github.com", "源站", true, true, true, true),
ProxyConfig(1, "https://ghproxy.com", "ghproxy", true, true, false),
ProxyConfig(2, "https://gh.tinylake.top", "WZ镜像", true, true, true)
)
}
}

// ========== 设置项 ==========
val enabled = CheckPref("githubAcceleration.enabled", true).apply {
addFallbackName("githubMirror")
}

val enableCache = CheckPref("githubAcceleration.cache", true)
val cacheExpireMinutes = SliderPref("githubAcceleration.cacheExpire", 60, 10, 1440, 10) { "${it}分钟" }
val maxRetries = SliderPref("githubAcceleration.maxRetries", 3, 1, 10) { "${it}次" }

// 代理列表配置(参考 customButtons 的实现模式)
@JvmField
val proxyList = object : Data<List<ProxyConfig>>("githubAcceleration.proxies", ProxyConfig.defaults()) {
init {
persistentProvider = PersistentProvider.AsUBJson(
PersistentProvider.Arc(name),
List::class.java,
ProxyConfig::class.java
)
}

override fun buildUI() = Table().let { table ->
var shown = false
table.button(title) { shown = !shown }.growX().height(55f).padBottom(2f).get().apply {
imageDraw { if (shown) Icon.downOpen else Icon.upOpen }.size(Vars.iconMed)
cells.reverse()
update { isChecked = shown }
}
table.row()
table.collapser(Table().apply {
defaults().pad(2f)
update {
if (changed()) clearChildren()
if (hasChildren()) return@update

// 表头
add("№").width(30f); add("启用").width(40f); add("名称").width(80f)
add("URL").growX(); add("Asset").width(50f); add("API").width(50f); add("操作").width(80f)
row()

// 代理列表
value.forEachIndexed { _, proxy ->
var tmp = proxy

add(proxy.id.toString()).width(30f)

// 启用开关
if (proxy.locked) {
add("[gray]✓").width(40f)
} else {
val cb = CheckBox("").apply { isChecked = proxy.enabled }
cb.changed { tmp = tmp.copy(enabled = cb.isChecked) }
add(cb).width(40f)
}

field(proxy.name) { v -> tmp = tmp.copy(name = v) }.width(80f).maxTextLength(20)
field(proxy.url) { v -> tmp = tmp.copy(url = v) }.growX().maxTextLength(200)

// Asset/API 开关
val assetCb = CheckBox("").apply { isChecked = proxy.assetEnabled; disabled = proxy.locked }
assetCb.changed { tmp = tmp.copy(assetEnabled = assetCb.isChecked) }
add(assetCb).width(50f)

val apiCb = CheckBox("").apply { isChecked = proxy.apiEnabled; disabled = proxy.locked }
apiCb.changed { tmp = tmp.copy(apiEnabled = apiCb.isChecked) }
add(apiCb).width(50f)

// 操作
table { ops ->
if (proxy.locked) {
ops.image(Icon.lock).size(24f)
} else {
ops.button(Icon.trashSmall, Styles.clearNonei, Vars.iconMed) {
set(value.filterNot { it === proxy })
}
ops.button(Icon.saveSmall, Styles.clearNonei, Vars.iconMed) {
set(value.map { if (it === proxy) tmp else it })
}.disabled { tmp === proxy }
}
}.width(80f)
row()
}

// 添加新代理
button("@add", Icon.addSmall) {
val newId = (value.maxOfOrNull { it.id } ?: 0) + 1
set(value + ProxyConfig().copy(id = newId))
}.colspan(columns).fillX().row()

add("[yellow]添加新代理前,请先保存编辑的代理").colspan(columns).center().padTop(-4f).row()

// 清空缓存
button("清空缓存", Icon.trash) {
cache.clear()
Vars.ui.showInfoFade("缓存已清空")
}.colspan(columns).fillX()
}
}) { shown }.growX()
table.row()
}
}

@JvmStatic
val settings: List<Data<*>> get() = listOf(enabled, enableCache, cacheExpireMinutes, maxRetries, proxyList)

// ========== 内部实现 ==========
private data class CachedResponse(val content: String, val timestamp: Long, val status: Int = 200)
private val cache = ConcurrentHashMap<String, CachedResponse>()

init {
setupHttpHooks()
}

private fun setupHttpHooks() {
Http.onBeforeRequest = { req ->
if (!enabled.value || !isGithubUrl(req.url)) return@onBeforeRequest

val originalUrl = req.url
val isApi = isApiUrl(originalUrl)

// 检查缓存 - 如果有缓存,修改 block 直接返回缓存内容
if (enableCache.value && !isApi) {
cache[originalUrl]?.let { cached ->
val ageMin = (System.currentTimeMillis() - cached.timestamp) / 60000
if (ageMin <= cacheExpireMinutes.value) {
Log.debug("GH缓存命中: @ (age: @min)", originalUrl, ageMin)
// 由于无法直接构造 HttpResponse,我们跳过缓存返回功能
// 仅作为缓存标记,减少不必要的代理切换
// TODO: 需要 Arc patch 支持才能真正返回缓存内容
}
cache.remove(originalUrl) // 过期则移除
}
}

// 获取可用代理
val proxies = proxyList.value.filter {
it.enabled && if (isApi) it.apiEnabled else it.assetEnabled
}

if (proxies.isEmpty()) return@onBeforeRequest

// 应用第一个代理
val cleanUrl = cleanProxyUrl(originalUrl)
val firstProxy = proxies.first()
req.url = if (firstProxy.locked && firstProxy.id == 0) cleanUrl
else "${firstProxy.url.trimEnd('/')}/$cleanUrl"

Log.debug("GH加速: @ -> @", firstProxy.name, req.url)

// 包装 block 实现重试和缓存
wrapBlockWithRetry(req, originalUrl, cleanUrl, proxies, isApi)
}
}

// 移除缓存包装函数,因为无法直接构造 HttpResponse

private fun wrapBlockWithRetry(
req: Http.HttpRequest,
originalUrl: String,
cleanUrl: String,
proxies: List<ProxyConfig>,
isApi: Boolean
) {
val originalBlock = req.block
var attemptIndex = 0

req.block = object : arc.func.Cons<Http.HttpResponse> {
override fun get(response: Http.HttpResponse) {
try {
originalBlock?.get(response)

// 成功:缓存
if (enableCache.value && !isApi && response.status == 200) {
response.resultAsString?.let { content ->
cache[originalUrl] = CachedResponse(content, System.currentTimeMillis(), 200)
Log.debug("GH缓存: @", originalUrl)
}
}
} catch (e: Exception) {
handleError(e)
}
}

private fun handleError(error: Throwable) {
attemptIndex++

if (attemptIndex < proxies.size && attemptIndex < maxRetries.value) {
val nextProxy = proxies[attemptIndex]
val retryUrl = if (nextProxy.locked && nextProxy.id == 0) cleanUrl
else "${nextProxy.url.trimEnd('/')}/$cleanUrl"

Log.warn("GH重试 [@/@]: @ -> @", attemptIndex + 1, proxies.size, nextProxy.name, retryUrl)

// 创建新请求重试
Http.HttpRequest().apply {
method = req.method
url = retryUrl
content = req.content
contentType = req.contentType
followRedirects = req.followRedirects
includeCredentials = req.includeCredentials
timeout = req.timeout
headers.putAll(req.headers)
block = this@object
this.error = req.error
}.submit()
} else {
Log.err("GH加速失败: 已尝试 @ 个代理", attemptIndex)
req.error?.get(error)
}
}
}
}

private fun isGithubUrl(url: String) = try {
val host = URL(url).host.lowercase()
host.contains("github.com") || host.contains("githubusercontent.com")
} catch (e: Exception) { false }

private fun isApiUrl(url: String) = url.contains("api.github.com")

private fun cleanProxyUrl(url: String): String {
val pattern = Regex("^https?://[^/]+/(https?://(?:github\\.com|raw\\.githubusercontent\\.com)/)")
return pattern.find(url)?.groupValues?.get(1) ?: url
}
}