feat: fix mods tab, add client/server split, and scaffold server dirs

Mods tab bug fixes:
- mod_manager: fix wrong kwargs in set_enabled_mods, fix scan dir to use
  mods/ subdir instead of server root, migrate old string-list format to
  dict format on read
- service: replace dead server_mods SQL JOIN with get_enabled_mods()
  call through the mod_manager capability; pass is_server_mod to
  build_mod_args
- mods_router: accept list[EnabledModEntry] objects (name + is_server_mod)
  instead of bare strings

Client/server mod split:
- Mods now stored as list[{"name": str, "is_server_mod": bool}]; old
  string-list format auto-migrated on read
- is_server_mod=true routes to -serverMod= arg; false to -mod= arg
- ModList UI: amber Client/Server badge in selected pane; toggle button
  in split-pane selector

Directory scaffold:
- process_config: adds "mods" to dir layout; provides get_dir_readme()
  with per-directory README.txt content
- file_utils: ensure_server_dirs() gains readme_provider kwarg; writes
  README.txt idempotently if absent
- service.create_server: passes readme_provider via hasattr probe
- main.py startup: backfills all existing servers with correct subdirs
  and README files (idempotent)

Docs: API.md and FRONTEND.md updated for new mod schema and types
Test __init__.py files added for pytest discovery
This commit is contained in:
Tran G. (Revernomad) Khoa
2026-04-20 10:54:56 +07:00
parent fa95587567
commit d45345a094
12 changed files with 209 additions and 67 deletions

View File

@@ -24,8 +24,13 @@ def _ok(data):
return {"success": True, "data": data, "error": None}
class EnabledModEntry(BaseModel):
name: str
is_server_mod: bool = False
class SetEnabledModsRequest(BaseModel):
mods: list[str]
mods: list[EnabledModEntry]
def _get_mod_manager(server_id: int, game_type: str):
@@ -52,12 +57,15 @@ def list_mods(
config_repo = ConfigRepository(db)
try:
available = mgr.list_available_mods()
enabled = set(mgr.get_enabled_mods(config_repo))
enabled_mods = mgr.get_enabled_mods(config_repo)
except AdapterError as exc:
raise HTTPException(status_code=500, detail={"code": "ADAPTER_ERROR", "message": str(exc)})
enabled_map = {m["name"]: m for m in enabled_mods}
for mod in available:
mod["enabled"] = mod["name"] in enabled
entry = enabled_map.get(mod["name"])
mod["enabled"] = entry is not None
mod["is_server_mod"] = entry["is_server_mod"] if entry else False
return _ok({
"server_id": server_id,
@@ -83,7 +91,7 @@ def set_enabled_mods(
config_repo = ConfigRepository(db)
try:
mgr.set_enabled_mods(body.mods, config_repo)
mgr.set_enabled_mods([m.model_dump() for m in body.mods], config_repo)
except AdapterError as exc:
raise HTTPException(status_code=400, detail={"code": "ADAPTER_ERROR", "message": str(exc)})
except ValueError as exc:
@@ -97,5 +105,5 @@ def set_enabled_mods(
return _ok({
"message": "Enabled mods updated. Restart the server for changes to take effect.",
"enabled_mods": body.mods,
"enabled_mods": [m.model_dump() for m in body.mods],
})

View File

@@ -126,9 +126,10 @@ class ServerService:
max_restarts=max_restarts,
)
# Create directory layout
# Create directory layout with per-directory README files
layout = process_config.get_server_dir_layout()
ensure_server_dirs(server_id, layout)
readme_fn = getattr(process_config, "get_dir_readme", None)
ensure_server_dirs(server_id, layout, readme_provider=readme_fn)
# Seed default config sections
config_gen = adapter.get_config_generator()
@@ -242,17 +243,17 @@ class ServerService:
# Get mod args if adapter supports mods
mod_args: list[str] = []
if adapter.has_capability("mod_manager"):
from sqlalchemy import text
mods = self._db.execute(
text("""
SELECT m.folder_path, sm.is_server_mod, sm.sort_order
FROM server_mods sm JOIN mods m ON m.id = sm.mod_id
WHERE sm.server_id = :sid ORDER BY sm.sort_order
"""),
{"sid": server_id},
).fetchall()
mod_list = [dict(r._mapping) for r in mods]
mod_args = adapter.get_mod_manager().build_mod_args(mod_list)
mod_mgr = adapter.get_mod_manager(server_id)
enabled_mods = mod_mgr.get_enabled_mods(self._config_repo)
server_dir = get_server_dir(server_id)
mod_list = [
{
"folder_path": str(server_dir / "mods" / m["name"]),
"game_data": {"is_server_mod": m.get("is_server_mod", False)},
}
for m in enabled_mods
]
mod_args = mod_mgr.build_mod_args(mod_list)
# Write config files (atomic)
server_dir = get_server_dir(server_id)