Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ scripts/build_installer_nuitka.bat
## 資料儲存位置

- 使用者設定檔:`%LOCALAPPDATA%\Programs\MinecraftServerManager\user_settings.json`
- 日誌檔案:`%LOCALAPPDATA%\Programs\MinecraftServerManager\log\`(自動管理,超過 10MB 時會刪除相當於 8MB 的舊日誌)
- 伺服器資料夾:由使用者選擇「主資料夾」後,程式會在該資料夾內建立 `servers` 子資料夾並存放所有伺服器資料。

## 貢獻與回饋
Expand Down
6 changes: 5 additions & 1 deletion docs/TECHNICAL_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
### 3. 工具層 (Utils Layer) - src/utils/
提供跨模組共用的通用功能與輔助函式。
- **JavaUtils / JavaDownloader**: Java 環境的偵測、驗證與自動下載。
- **LogUtils**: 統一的日誌記錄系統,支援多級別日誌輸出
- **Logger (基於 loguru)**: 統一的日誌記錄系統,支援多級別日誌輸出與自動日誌管理
- **SettingsManager**: 應用程式設定的持久化存儲與讀取。
- **UIUtils**: 通用的 UI 輔助函式,如對話框顯示、字體管理等。

Expand Down Expand Up @@ -107,6 +107,9 @@ MinecraftServerManger/
## 使用者資料與伺服器資料路徑

- 使用者設定檔固定存放於:`%LOCALAPPDATA%\Programs\MinecraftServerManager\user_settings.json`
- 日誌檔案存放於:`%LOCALAPPDATA%\Programs\MinecraftServerManager\log\`
- 日誌檔案命名格式:`YYYY-MM-DD-HH-mm.log`
- 自動清理機制:當日誌資料夾超過 10MB 時,會自動刪除相當於 8MB 的舊日誌
- `user_settings.json` 會記錄「使用者選擇的伺服器主資料夾」(base dir),實際伺服器資料會放在該資料夾內的 `servers` 子資料夾。

## 技術堆疊 (Tech Stack)
Expand All @@ -121,6 +124,7 @@ MinecraftServerManger/
- **psutil**: 跨平台系統監控,用於獲取 CPU 與記憶體使用率。
- **lxml**: 高效能 XML 解析,用於處理 Maven Metadata。
- **toml**: 解析 TOML 設定檔 (如 Fabric/Forge 配置)。
- **loguru**: 現代化的日誌記錄函式庫,提供彩色輸出、自動日誌輪轉與執行緒安全等功能。

## 安全性與合規性

Expand Down
4 changes: 4 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@

使用者設定會儲存在:`%LOCALAPPDATA%\Programs\MinecraftServerManager\user_settings.json`

日誌檔案會自動記錄於:`%LOCALAPPDATA%\Programs\MinecraftServerManager\log\`
- 每次啟動程式會建立新的日誌檔案(格式:年-月-日-時-分.log)
- 程式會自動清理舊日誌:當日誌資料夾超過 10MB 時,會刪除相當於 8MB 的舊日誌

### 查看目前安裝的套件(開發者/進階)
在專案根目錄執行:

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies = [
"nuitka>=2.8.9",
"zstandard>=0.15",
"uv>=0.6.0",
"loguru>=0.7.3",
]

[project.scripts]
Expand Down
59 changes: 25 additions & 34 deletions src/core/loader_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
# ====== 專案內部模組 ======
from src.models import LoaderVersion
from src.utils import java_utils
from src.utils import HTTPUtils, LogUtils, UIUtils, ensure_dir, get_cache_dir
from src.utils import HTTPUtils, UIUtils, ensure_dir, get_cache_dir
from src.utils.logger import get_logger

logger = get_logger().bind(component="LoaderManager")

class LoaderManager:
"""
Expand Down Expand Up @@ -57,14 +60,14 @@ def clear_cache_file(self):
# 清除記憶體快取
self._version_cache.clear()
except PermissionError as e:
LogUtils.error_exc(f"清除快取檔案失敗: {e}", "LoaderManager", e)
logger.exception(f"清除快取檔案失敗: {e}")
UIUtils.show_error(
"清除快取檔案失敗",
f"無法刪除快取檔案: {cache_file}\n權限不足\n{e}",
topmost=True,
)
except Exception as e:
LogUtils.error_exc(f"清除快取檔案失敗: {e}", "LoaderManager", e)
logger.exception(f"清除快取檔案失敗: {e}")
UIUtils.show_error(
"清除快取檔案失敗", f"無法刪除快取檔案: {cache_file}\n{e}", topmost=True
)
Expand Down Expand Up @@ -185,29 +188,29 @@ def preload_loader_versions(self):
self._preload_forge_versions()

def _preload_fabric_versions(self):
LogUtils.debug("預先抓取 Fabric 載入器版本...", "LoaderManager")
logger.debug("預先抓取 Fabric 載入器版本...", "LoaderManager")
fabric_url = "https://meta.fabricmc.net/v2/versions/loader"
try:
data = HTTPUtils.get_json(fabric_url, timeout=15)
if data:
with open(self.fabric_cache_file, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
LogUtils.error_exc(f"載入 Fabric 版本失敗: {e}", "LoaderManager", e)
logger.exception(f"載入 Fabric 版本失敗: {e}")
UIUtils.show_error(
"載入 Fabric 版本失敗",
f"無法從 API 獲取 Fabric 版本:{e}",
topmost=True,
)

def _preload_forge_versions(self):
LogUtils.debug("預先抓取 Forge 載入器版本...", "LoaderManager")
logger.debug("預先抓取 Forge 載入器版本...", "LoaderManager")
try:
forge_url = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml"
response = HTTPUtils.get_content(forge_url, timeout=15)

if response and response.status_code == 200:
LogUtils.debug("成功獲取 Forge XML 數據", "LoaderManager")
logger.debug("成功獲取 Forge XML 數據", "LoaderManager")
root = etree.fromstring(response.content)
versions = []

Expand Down Expand Up @@ -244,7 +247,7 @@ def _preload_forge_versions(self):
version_dict[mc_version] = []
version_dict[mc_version].append(version)
except Exception as e:
LogUtils.debug(f"解析 Forge 版本字串失敗 '{version}': {e}", "LoaderManager")
logger.debug(f"解析 Forge 版本字串失敗 '{version}': {e}", "LoaderManager")
continue

# 對每個 MC 版本的 Forge 版本進行排序(最新在前)
Expand All @@ -259,7 +262,7 @@ def _preload_forge_versions(self):
return

except Exception as e:
LogUtils.error_exc(f"Maven metadata API 方法失敗: {e}", "LoaderManager", e)
logger.exception(f"Maven metadata API 方法失敗: {e}")
UIUtils.show_error(
"載入 Forge 版本失敗",
f"無法從 Maven metadata API 獲取 Forge 版本:{e}",
Expand Down Expand Up @@ -316,9 +319,7 @@ def get_compatible_loader_versions(
self._version_cache[cache_key] = result
return result
except Exception as e:
LogUtils.error_exc(
f"獲取 Fabric 版本時發生錯誤: {e}", "LoaderManager", e
)
logger.exception(f"獲取 Fabric 版本時發生錯誤: {e}")
return []
# Forge
elif loader_type.lower() == "forge":
Expand All @@ -341,9 +342,7 @@ def get_compatible_loader_versions(
self._version_cache[cache_key] = result
return result
except Exception as e:
LogUtils.error_exc(
f"獲取 Forge 版本時發生錯誤: {e}", "LoaderManager", e
)
logger.exception(f"獲取 Forge 版本時發生錯誤: {e}")
return []

def _is_fabric_compatible_version(self, mc_version: str) -> bool:
Expand Down Expand Up @@ -377,7 +376,7 @@ def _is_fabric_compatible_version(self, mc_version: str) -> bool:
else:
return False
except Exception as e:
LogUtils.error_exc(f"檢查 Fabric 相容性時發生錯誤: {e}", "LoaderManager", e)
logger.exception(f"檢查 Fabric 相容性時發生錯誤: {e}")
return False

def _parse_mc_version(self, version_str: str) -> list:
Expand All @@ -396,7 +395,7 @@ def _parse_mc_version(self, version_str: str) -> list:
matches = re.findall(r"\d+", version_str)
return [int(x) for x in matches] if matches else []
except Exception as e:
LogUtils.error_exc(f"解析 MC 版本時發生錯誤: {e}", "LoaderManager", e)
logger.exception(f"解析 MC 版本時發生錯誤: {e}")
return []
# ---------------------- 私有輔助方法 ----------------------
# === 下載 × 執行安裝器 ===
Expand Down Expand Up @@ -511,16 +510,14 @@ def _download_and_run_installer(
progress_callback(install_start, f"處理中: {line[:40]}...")

if process.returncode != 0:
LogUtils.error(
f"安裝器執行失敗 (Code {process.returncode})", "LoaderManager"
)
logger.error(f"安裝器執行失敗 (Code {process.returncode})")
return self._fail(
progress_callback,
f"安裝器執行失敗 (Code {process.returncode})",
debug=f"[DEBUG] cmd: {' '.join(cmd)}",
)
except Exception as e:
LogUtils.error_exc(f"執行安裝器時發生錯誤: {e}", "LoaderManager", e)
logger.exception(f"執行安裝器時發生錯誤: {e}")
return self._fail(
progress_callback,
f"執行安裝器時發生錯誤: {e}",
Expand Down Expand Up @@ -551,7 +548,7 @@ def _download_and_run_installer(
if java_line and "nogui" not in java_line.lower():
java_line += " nogui"
except Exception as e:
LogUtils.warning(f"無法讀取 run.bat: {e}")
logger.warning(f"無法讀取 run.bat: {e}")
UIUtils.show_warning(
"讀取失敗", f"無法讀取 run.bat:{e}", parent=parent_window
)
Expand All @@ -576,9 +573,7 @@ def _download_and_run_installer(
with start_server_path.open("w", encoding="utf-8") as f:
f.writelines(new_lines)
except Exception as e:
LogUtils.error_exc(
f"修改 start_server.bat 失敗: {e}", "LoaderManager", e
)
logger.exception(f"修改 start_server.bat 失敗: {e}")
UIUtils.show_warning(
"修改失敗",
f"無法修改 start_server.bat:{e}",
Expand All @@ -598,17 +593,15 @@ def _download_and_run_installer(
with suppress(FileNotFoundError):
file_path.unlink()
except Exception as e:
LogUtils.error_exc(
f"清理安裝檔失敗: {installer_path}: {e}", "LoaderManager", e
)
logger.exception(f"清理安裝檔失敗: {installer_path}: {e}")
UIUtils.show_warning(
"清理失敗",
f"安裝完成,但無法清理安裝器檔案:{installer_path}\n可手動刪除。",
parent=parent_window,
)

except Exception as e:
LogUtils.error_exc(f"安裝過程中發生錯誤: {e}", "LoaderManager", e)
logger.exception(f"安裝過程中發生錯誤: {e}")
UIUtils.show_error(
"安裝失敗", f"安裝過程中發生錯誤:{e}", parent=parent_window
)
Expand Down Expand Up @@ -714,7 +707,7 @@ def _download_file_with_progress(
)
return True
except Exception as e:
LogUtils.error_exc(f"下載失敗: {e}", "LoaderManager", e)
logger.exception(f"下載失敗: {e}")
return self._fail(
progress_callback,
f"下載失敗: {e}",
Expand Down Expand Up @@ -747,9 +740,7 @@ def _get_minecraft_server_url(self, mc_version: str) -> Optional[str]:
return None
return ver_data["downloads"]["server"]["url"]
except Exception as e:
LogUtils.error_exc(
f"獲取 Minecraft 伺服器 URL 失敗: {e}", "LoaderManager", e
)
logger.exception(f"獲取 Minecraft 伺服器 URL 失敗: {e}")
return None

def _standardize_loader_type(self, lt: str, loader_version: str) -> str:
Expand Down Expand Up @@ -788,7 +779,7 @@ def _fail(self, progress_callback, user_msg: str, debug: str = "") -> bool:
是否成功
"""
if debug:
LogUtils.debug(debug)
logger.debug(debug)
if progress_callback:
progress_callback(100, user_msg)
return False
37 changes: 16 additions & 21 deletions src/core/mod_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import toml
import zipfile
# ====== 專案內部模組 ======
from ..utils import HTTPUtils, LogUtils, UIUtils
from ..utils import HTTPUtils, UIUtils
from ..utils.logger import get_logger
from ..version_info import APP_VERSION, GITHUB_OWNER, GITHUB_REPO

logger = get_logger().bind(component="ModManager")

# ====== 模組狀態與平台定義 ======
class ModStatus(Enum):
"""
Expand Down Expand Up @@ -186,7 +189,7 @@ def create_mod_info_from_file(self, file_path: Path) -> Optional[LocalModInfo]:
file_size=file_path.stat().st_size,
)
except Exception as e:
LogUtils.error(f"解析模組檔案失敗 {file_path}: {e}", "ModManager")
logger.error(f"解析模組檔案失敗 {file_path}: {e}", "ModManager")
return None

def _parse_file_info(self, file_path: Path) -> tuple[str, bool, str]:
Expand Down Expand Up @@ -229,7 +232,7 @@ def _get_manifest_version(self, jar) -> Optional[str]:
if v and v != "${projectversion}":
return v
except Exception as e:
LogUtils.error_exc(f"讀取 MANIFEST.MF 版本資訊失敗: {e}", "ModManager", e)
logger.exception(f"讀取 MANIFEST.MF 版本資訊失敗: {e}")
return None

def _extract_metadata_from_jar(self, file_path: Path, mod_data: dict) -> None:
Expand All @@ -256,9 +259,7 @@ def _extract_metadata_from_jar(self, file_path: Path, mod_data: dict) -> None:
extractor(jar, mod_data)
break # 找到第一個匹配的元資料檔案即停止
except Exception as e:
LogUtils.error_exc(
f"從 JAR 提取元資料失敗 {file_path}: {e}", "ModManager", e
)
logger.exception(f"從 JAR 提取元資料失敗 {file_path}: {e}")

def _extract_fabric_metadata(self, jar, mod_data: dict) -> None:
"""
Expand All @@ -285,7 +286,7 @@ def _extract_fabric_metadata(self, jar, mod_data: dict) -> None:
mc_version = depends.get("minecraft", mod_data["mc_version"])
mod_data["mc_version"] = self._normalize_mc_version(mc_version)
except (TypeError, Exception) as e:
LogUtils.error(f"無法從 JAR 檔案提取 Fabric 元資料: {e}", "ModManager")
logger.error(f"無法從 JAR 檔案提取 Fabric 元資料: {e}", "ModManager")

def _extract_forge_metadata(self, jar, mod_data: dict) -> None:
"""
Expand Down Expand Up @@ -331,7 +332,7 @@ def _extract_forge_metadata(self, jar, mod_data: dict) -> None:
)
break
except Exception as e:
LogUtils.error_exc(f"解析 Forge 元資料失敗: {e}", "ModManager", e)
logger.exception(f"解析 Forge 元資料失敗: {e}")

def _extract_legacy_forge_metadata(self, jar, mod_data: dict) -> None:
"""
Expand Down Expand Up @@ -364,9 +365,7 @@ def _extract_legacy_forge_metadata(self, jar, mod_data: dict) -> None:
mod_data["mc_version"] = info.get("mcversion", mod_data["mc_version"])
mod_data["loader_type"] = "Forge"
except Exception as e:
LogUtils.error_exc(
f"解析 legacy Forge mcmod.info 失敗: {e}", "ModManager", e
)
logger.exception(f"解析 legacy Forge mcmod.info 失敗: {e}")

def _resolve_version(self, jar, version: str) -> str:
"""
Expand Down Expand Up @@ -621,9 +620,7 @@ def _detect_platform_info(
if platform_id:
platform = ModPlatform.MODRINTH
except Exception as e:
LogUtils.error_exc(
f"從 JAR 偵測平台 ID 失敗 {file_path}: {e}", "ModManager", e
)
logger.exception(f"從 JAR 偵測平台 ID 失敗 {file_path}: {e}")

# Fallback: search on Modrinth API
if platform == ModPlatform.LOCAL or not platform_id:
Expand All @@ -646,9 +643,7 @@ def _extract_platform_id_from_fabric(self, jar) -> str:
meta = json.load(f)
return meta.get("id", "")
except Exception as e:
LogUtils.error_exc(
f"解析 fabric.mod.json 取得平台 ID 失敗: {e}", "ModManager", e
)
logger.exception(f"解析 fabric.mod.json 取得平台 ID 失敗: {e}")
return ""

def _extract_platform_id_from_forge(self, jar) -> str:
Expand All @@ -674,7 +669,7 @@ def _extract_platform_id_from_forge(self, jar) -> str:
if m:
return m.group(2)
except Exception as e:
LogUtils.error_exc(f"解析 mods.toml 取得平台 ID 失敗: {e}", "ModManager", e)
logger.exception(f"解析 mods.toml 取得平台 ID 失敗: {e}")
return ""

def _search_on_modrinth(
Expand Down Expand Up @@ -712,7 +707,7 @@ def _search_on_modrinth(
hit = data["hits"][0]
return ModPlatform.MODRINTH, hit.get("slug", "")
except Exception as e:
LogUtils.error_exc(f"Modrinth 搜尋失敗: {e}", "ModManager", e)
logger.exception(f"Modrinth 搜尋失敗: {e}")

return ModPlatform.LOCAL, ""

Expand Down Expand Up @@ -804,7 +799,7 @@ def enable_mod(self, mod_id: str) -> bool:
return True
return False
except Exception as e:
LogUtils.error(f"啟用模組失敗: {e}", "ModManager")
logger.error(f"啟用模組失敗: {e}", "ModManager")
if threading.current_thread() is threading.main_thread():
UIUtils.show_error("啟用失敗", f"啟用模組失敗: {e}")
return False
Expand Down Expand Up @@ -856,7 +851,7 @@ def disable_mod(self, mod_id: str) -> bool:
return True
return False
except Exception as e:
LogUtils.error(f"停用模組失敗: {e}", "ModManager")
logger.error(f"停用模組失敗: {e}", "ModManager")
if threading.current_thread() is threading.main_thread():
UIUtils.show_error("停用失敗", f"停用模組失敗: {e}")
return False
Expand Down
Loading