Server BugFixes + убрал генерацию sevrer команды т.к это уже в клиенте лол
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
# main.py
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Request
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
@@ -11,7 +10,6 @@ from cachetools import TTLCache
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import asyncio
|
|
||||||
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
||||||
|
|
||||||
# Import local modules
|
# Import local modules
|
||||||
@@ -25,10 +23,6 @@ from log_manager import init_logging, get_logger
|
|||||||
from models import MinecraftVersion
|
from models import MinecraftVersion
|
||||||
from pack_manager import get_minecraft_versions, download_minecraft_version
|
from pack_manager import get_minecraft_versions, download_minecraft_version
|
||||||
|
|
||||||
import minecraft_launcher_lib.command as mcl_command
|
|
||||||
import minecraft_launcher_lib.utils as mcl_utils
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
# Cache for manifests - expires after 5 minutes
|
# Cache for manifests - expires after 5 minutes
|
||||||
@@ -301,77 +295,6 @@ async def get_pack_manifest(pack_name: str, request: Request):
|
|||||||
headers={"X-Pack-Version": str(meta.version), "X-Cached": "false"}
|
headers={"X-Pack-Version": str(meta.version), "X-Cached": "false"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.post("/pack/{pack_name}/launch")
|
|
||||||
async def get_launch_command(
|
|
||||||
pack_name: str,
|
|
||||||
request: Request,
|
|
||||||
options: dict # клиент присылает: {"username": "...", "uuid": "...", "token": "...", "gameDir": "...", "javaPath": "...", "maxMemory": 4096}
|
|
||||||
):
|
|
||||||
client_ip = request.client.host if request.client else "unknown"
|
|
||||||
logger.info("Launch command requested", pack=pack_name, client_ip=client_ip)
|
|
||||||
|
|
||||||
try:
|
|
||||||
meta = get_cached_manifest(pack_name) or await scan_pack(pack_name)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(404, "Pack not found") from e
|
|
||||||
|
|
||||||
# Базовые опции для minecraft-launcher-lib
|
|
||||||
launch_options = {
|
|
||||||
"username": options.get("username", "Player"),
|
|
||||||
"uuid": options.get("uuid", "00000000-0000-0000-0000-000000000000"),
|
|
||||||
"token": options.get("token", "token"),
|
|
||||||
"gameDirectory": options.get("gameDir", str(Path("."))), # относительный
|
|
||||||
"executablePath": options.get("javaPath", "java"), # или полный путь
|
|
||||||
"maxMemory": options.get("maxMemory", 4096),
|
|
||||||
"customResolution": options.get("customResolution", False),
|
|
||||||
# можно добавить width/height и т.д.
|
|
||||||
}
|
|
||||||
|
|
||||||
# minecraft_directory — это корень пака у клиента (куда скачаны файлы)
|
|
||||||
minecraft_dir = launch_options["gameDirectory"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Генерируем команду (библиотека сама обработает Fabric/Forge через version.json)
|
|
||||||
command_list = mcl_command.get_minecraft_command(
|
|
||||||
version=meta.minecraft_version,
|
|
||||||
minecraft_directory=minecraft_dir,
|
|
||||||
options=launch_options
|
|
||||||
)
|
|
||||||
|
|
||||||
# Превращаем абсолютные пути в относительные (очень важно для портативности)
|
|
||||||
relative_command = []
|
|
||||||
game_dir_path = Path(minecraft_dir).resolve()
|
|
||||||
|
|
||||||
for arg in command_list:
|
|
||||||
try:
|
|
||||||
arg_path = Path(arg)
|
|
||||||
if arg_path.is_absolute() and game_dir_path in arg_path.parents:
|
|
||||||
rel = arg_path.relative_to(game_dir_path).as_posix()
|
|
||||||
relative_command.append(f"./{rel}" if os.name != "nt" else rel.replace("/", "\\"))
|
|
||||||
else:
|
|
||||||
relative_command.append(arg)
|
|
||||||
except Exception:
|
|
||||||
relative_command.append(arg)
|
|
||||||
|
|
||||||
# Дополнительно: если в PackMeta есть свои jvmArgs/gameArgs — мерджим
|
|
||||||
if hasattr(meta, 'launch') and meta.launch:
|
|
||||||
# Добавляем кастомные jvmArgs в начало и т.д.
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
|
||||||
"version": meta.version,
|
|
||||||
"minecraftVersion": meta.minecraft_version,
|
|
||||||
"loaderType": meta.loader_type,
|
|
||||||
"command": relative_command, # список строк — готов к subprocess
|
|
||||||
"workingDirectory": ".", # относительный
|
|
||||||
"mainClass": meta.launch.mainClass if hasattr(meta, 'launch') else None,
|
|
||||||
"classpath": meta.launch.classpath if hasattr(meta, 'launch') else []
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to generate launch command", pack=pack_name, error=str(e), exc_info=True)
|
|
||||||
raise HTTPException(500, f"Failed to generate launch command: {str(e)}")
|
|
||||||
|
|
||||||
@app.get("/pack/{pack_name}/file/{file_path:path}")
|
@app.get("/pack/{pack_name}/file/{file_path:path}")
|
||||||
async def get_pack_file(pack_name: str, file_path: str, request: Request):
|
async def get_pack_file(pack_name: str, file_path: str, request: Request):
|
||||||
full_path = PACKS_DIR / pack_name / file_path
|
full_path = PACKS_DIR / pack_name / file_path
|
||||||
|
|||||||
@@ -12,20 +12,6 @@ class FileEntry(BaseModel):
|
|||||||
added_at: datetime
|
added_at: datetime
|
||||||
modified_at: datetime
|
modified_at: datetime
|
||||||
|
|
||||||
|
|
||||||
class LaunchConfig(BaseModel):
|
|
||||||
mainClass: str
|
|
||||||
classpath: List[str] = Field(default_factory=list) # относительные пути от gameDir
|
|
||||||
jvmArgs: List[str] = Field(default_factory=list)
|
|
||||||
gameArgs: List[str] = Field(default_factory=list)
|
|
||||||
nativesPath: Optional[str] = None # например: "natives"
|
|
||||||
assetIndex: str = "1.20" # или актуальная версия
|
|
||||||
minecraftVersion: str
|
|
||||||
loaderType: str # "vanilla" | "fabric" | "forge" | "neoforge" | "quilt"
|
|
||||||
loaderVersion: Optional[str] = None
|
|
||||||
gameDirectory: str = "." # "." = корень инсталляции пака (рекомендую)
|
|
||||||
|
|
||||||
|
|
||||||
class PackMeta(BaseModel):
|
class PackMeta(BaseModel):
|
||||||
pack_name: str
|
pack_name: str
|
||||||
version: int = 1
|
version: int = 1
|
||||||
@@ -39,22 +25,16 @@ class PackMeta(BaseModel):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Основные поля (один раз!)
|
|
||||||
minecraft_version: str
|
minecraft_version: str
|
||||||
loader_type: str
|
loader_type: str
|
||||||
loader_version: Optional[str] = None
|
loader_version: Optional[str] = None
|
||||||
|
|
||||||
# Конфигурация запуска (обязательна)
|
|
||||||
launch: LaunchConfig
|
|
||||||
|
|
||||||
|
|
||||||
class MinecraftVersion(BaseModel):
|
class MinecraftVersion(BaseModel):
|
||||||
version: str
|
version: str
|
||||||
type: str # release, snapshot, old_alpha, old_beta
|
type: str # release, snapshot, old_alpha, old_beta
|
||||||
release_time: datetime
|
release_time: datetime
|
||||||
url: Optional[str] = None
|
url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ModLoader(BaseModel):
|
class ModLoader(BaseModel):
|
||||||
type: str
|
type: str
|
||||||
version: str
|
version: str
|
||||||
|
|||||||
+29
-17
@@ -1,4 +1,3 @@
|
|||||||
# pack_manager.py (updated)
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -22,7 +21,6 @@ DATA_DIR.mkdir(exist_ok=True)
|
|||||||
MINECRAFT_VERSION_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
|
MINECRAFT_VERSION_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
|
||||||
FABRIC_META_URL = "https://meta.fabricmc.net/v2/versions"
|
FABRIC_META_URL = "https://meta.fabricmc.net/v2/versions"
|
||||||
FORGE_META_URL = "https://files.minecraftforge.net/net/minecraftforge/forge/"
|
FORGE_META_URL = "https://files.minecraftforge.net/net/minecraftforge/forge/"
|
||||||
MinecraftVersion=[]
|
|
||||||
|
|
||||||
# Cache for loaded manifests
|
# Cache for loaded manifests
|
||||||
_manifest_cache: Dict[str, PackMeta] = {}
|
_manifest_cache: Dict[str, PackMeta] = {}
|
||||||
@@ -41,8 +39,9 @@ async def calculate_sha256(file_path: Path) -> str:
|
|||||||
hash_sha.update(chunk)
|
hash_sha.update(chunk)
|
||||||
return hash_sha.hexdigest()
|
return hash_sha.hexdigest()
|
||||||
|
|
||||||
async def get_minecraft_versions() -> List[MinecraftVersion]:
|
async def get_minecraft_versions():
|
||||||
"""Fetch available Minecraft versions from Mojang"""
|
"""Fetch available Minecraft versions from Mojang"""
|
||||||
|
from models import MinecraftVersion
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(MINECRAFT_VERSION_MANIFEST_URL) as response:
|
async with session.get(MINECRAFT_VERSION_MANIFEST_URL) as response:
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
@@ -72,21 +71,17 @@ async def get_forge_versions(minecraft_version: str) -> List[str]:
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(FORGE_META_URL) as response:
|
async with session.get(FORGE_META_URL) as response:
|
||||||
# Forge API is more complex, simplified for now
|
# Forge API is more complex, simplified for now
|
||||||
# You might want to use a proper forge API
|
return []
|
||||||
return [] # Placeholder
|
|
||||||
|
|
||||||
async def download_minecraft_version(version: str, target_path: Path) -> bool:
|
async def download_minecraft_version(version: str, target_path: Path) -> bool:
|
||||||
"""Download Minecraft version JSON and client jar"""
|
"""Download Minecraft version JSON and client jar"""
|
||||||
# Get version manifest
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(f"https://piston-meta.mojang.com/mc/game/{version}/{version}.json") as response:
|
async with session.get(f"https://piston-meta.mojang.com/mc/game/{version}/{version}.json") as response:
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
version_data = await response.json()
|
version_data = await response.json()
|
||||||
# Save version JSON
|
|
||||||
async with aiofiles.open(target_path / f"{version}.json", 'w') as f:
|
async with aiofiles.open(target_path / f"{version}.json", 'w') as f:
|
||||||
await f.write(json.dumps(version_data, indent=2))
|
await f.write(json.dumps(version_data, indent=2))
|
||||||
|
|
||||||
# Download client jar
|
|
||||||
downloads = version_data.get("downloads", {})
|
downloads = version_data.get("downloads", {})
|
||||||
client_info = downloads.get("client", {})
|
client_info = downloads.get("client", {})
|
||||||
if client_info:
|
if client_info:
|
||||||
@@ -109,20 +104,22 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
meta_path = get_meta_path(pack_name)
|
meta_path = get_meta_path(pack_name)
|
||||||
current_meta: Optional[PackMeta] = None
|
current_meta: Optional[PackMeta] = None
|
||||||
|
|
||||||
# Check if we have cached version and force_rescan is False
|
|
||||||
if not force_rescan and pack_name in _manifest_cache:
|
if not force_rescan and pack_name in _manifest_cache:
|
||||||
return _manifest_cache[pack_name]
|
return _manifest_cache[pack_name]
|
||||||
|
|
||||||
if meta_path.exists():
|
if meta_path.exists():
|
||||||
async with aiofiles.open(meta_path, 'r', encoding='utf-8') as f:
|
async with aiofiles.open(meta_path, 'r', encoding='utf-8') as f:
|
||||||
data = json.loads(await f.read())
|
data = json.loads(await f.read())
|
||||||
current_meta = PackMeta.model_validate(data)
|
try:
|
||||||
|
current_meta = PackMeta.model_validate(data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to validate existing meta for pack {pack_name}", error=str(e))
|
||||||
|
current_meta = None
|
||||||
|
|
||||||
new_files: Dict[str, FileEntry] = {}
|
new_files: Dict[str, FileEntry] = {}
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
for root, dirs, files in os.walk(pack_path):
|
for root, dirs, files in os.walk(pack_path):
|
||||||
# Filter ignored directories
|
|
||||||
ignored = current_meta.ignored_dirs if current_meta else []
|
ignored = current_meta.ignored_dirs if current_meta else []
|
||||||
dirs[:] = [d for d in dirs if d not in ignored]
|
dirs[:] = [d for d in dirs if d not in ignored]
|
||||||
|
|
||||||
@@ -130,7 +127,6 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
file_path = Path(root) / file
|
file_path = Path(root) / file
|
||||||
rel_path = file_path.relative_to(pack_path).as_posix()
|
rel_path = file_path.relative_to(pack_path).as_posix()
|
||||||
|
|
||||||
# Skip files in ignored directories
|
|
||||||
if any(ignored_dir in rel_path.split('/') for ignored_dir in ignored):
|
if any(ignored_dir in rel_path.split('/') for ignored_dir in ignored):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -151,27 +147,43 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
|||||||
current_meta.files[rel_path].hash != file_hash):
|
current_meta.files[rel_path].hash != file_hash):
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
# Update manifest if changes detected or new pack
|
|
||||||
if not current_meta or changed or len(new_files) != len(current_meta.files if current_meta else 0):
|
if not current_meta or changed or len(new_files) != len(current_meta.files if current_meta else 0):
|
||||||
version = (current_meta.version + 1) if current_meta else 1
|
version = (current_meta.version + 1) if current_meta else 1
|
||||||
|
|
||||||
|
pack_config_path = pack_path / "instance.json"
|
||||||
|
minecraft_version = "1.20.4"
|
||||||
|
loader_type = "vanilla"
|
||||||
|
loader_version = None
|
||||||
|
|
||||||
|
if pack_config_path.exists():
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(pack_config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.loads(await f.read())
|
||||||
|
minecraft_version = config.get("minecraftVersion", minecraft_version)
|
||||||
|
loader_type = config.get("loaderType", loader_type)
|
||||||
|
loader_version = config.get("loaderVersion")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load instance.json for {pack_name}", error=str(e))
|
||||||
|
|
||||||
new_meta = PackMeta(
|
new_meta = PackMeta(
|
||||||
pack_name=pack_name,
|
pack_name=pack_name,
|
||||||
version=version,
|
version=version,
|
||||||
files=new_files,
|
files=new_files,
|
||||||
updated_at=datetime.utcnow(),
|
updated_at=datetime.utcnow(),
|
||||||
ignored_dirs=current_meta.ignored_dirs if current_meta else []
|
ignored_dirs=current_meta.ignored_dirs if current_meta else [],
|
||||||
|
minecraft_version=minecraft_version,
|
||||||
|
loader_type=loader_type,
|
||||||
|
loader_version=loader_version
|
||||||
)
|
)
|
||||||
|
|
||||||
async with aiofiles.open(meta_path, 'w', encoding='utf-8') as f:
|
async with aiofiles.open(meta_path, 'w', encoding='utf-8') as f:
|
||||||
await f.write(new_meta.model_dump_json(indent=2))
|
await f.write(new_meta.model_dump_json(indent=2))
|
||||||
|
|
||||||
# Update memory cache
|
|
||||||
_manifest_cache[pack_name] = new_meta
|
_manifest_cache[pack_name] = new_meta
|
||||||
|
|
||||||
logger.info("Pack updated", pack=pack_name, new_version=version, files_count=len(new_files))
|
logger.info(f"Pack updated: {pack_name} v{version}, {len(new_files)} files")
|
||||||
return new_meta
|
return new_meta
|
||||||
|
|
||||||
# Update cache with existing manifest
|
|
||||||
if current_meta:
|
if current_meta:
|
||||||
_manifest_cache[pack_name] = current_meta
|
_manifest_cache[pack_name] = current_meta
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user