import shutil import os from collections import OrderedDict from logging import getLogger from typing import Dict from git import Repo from pyenliven import MODS_STOPGAP_DIR, echo0 from pyenliven.metadata import ( BASE_ENLIVEN_CONF_SETTINGS, gamespec, update_conf, ) logger = getLogger(__name__) class GameBuilder: """Manage creation of a game from scratch. Attributes: more_conf (OrderedDict): minetest.conf settings collected from entry(ies) such as from install_mod calls. """ def __init__(self, minetest_game_path: str, minetest_version: str = "5", offline: bool = False, delete: bool = False, pull: bool = True): self.source_game = os.path.realpath(minetest_game_path) self.target_parent = os.path.dirname(self.source_game) self.target_game = os.path.join(self.target_parent, "ENLIVEN") self.pull = pull if os.path.exists(self.target_game): if delete: print(f"rm -rf {repr(self.target_game)}") shutil.rmtree(self.target_game) self.mods_target = os.path.join(self.target_game, "mods") self.minetest_version = minetest_version # "5" or "0.4" self.offline = offline self.more_conf = OrderedDict() self.meta = OrderedDict() self.meta['mods'] = OrderedDict() if minetest_version not in ("5", "0.4"): raise ValueError("minetest_version must be '5' or '0.4'") if not os.path.isdir(self.source_game): raise FileNotFoundError(f"minetest_game not found: {self.source_game}") if os.path.realpath(self.source_game) == os.path.realpath(self.target_game): raise ValueError("source game and target game are both" f" {repr(os.path.realpath(self.source_game))}") echo0(f"Building ENLIVEN → {self.target_game}") echo0(f" from base: {self.source_game}") echo0(f" target Minetest compatibility: {self.minetest_version}") echo0(f" offline mode: {self.offline}") def prepare_target(self): """Copy minetest_game → ENLIVEN if needed""" if os.path.exists(self.target_game): raise FileExistsError(f"Target already exists: {self.target_game}") # return echo0("Copying minetest_game → ENLIVEN ...") shutil.copytree(self.source_game, self.target_game) def install_mod(self, entry: Dict[str, any]): name = entry.get('name') repo = entry.get('repo') branch = entry.get('branch') stopgap_only = entry.get('stopgap_only', False) settings = entry.get('settings') if settings: for k, v in settings.items(): self.more_conf[k] = v if not name: raise ValueError(f"Missing 'name' in {entry}") if name in self.meta['mods']: raise KeyError(f"Already installed a mod named {name}") dest = os.path.join(self.mods_target, name) # 1. Prefer stopgap if exists stopgap_src = os.path.join(MODS_STOPGAP_DIR, name) if os.path.isdir(stopgap_src): echo0(f" [stopgap] {name}") if os.path.exists(dest): shutil.rmtree(dest) shutil.copytree(stopgap_src, dest) self.meta['mods'][name] = entry return # 2. Git clone if we have repo URL(s) if not repo: raise ValueError(f"Missing 'repo' for {entry}") urls = [repo] if isinstance(repo, str) else repo url = urls[-1] # prefer last one del urls distributor = entry.get('distributor') if not distributor: distributor = url.split("/")[-2] repo_name = url.split("/")[-1].replace(".git", "") git_local_path = os.path.expanduser(f"~/{repo_name}") if os.path.isdir(git_local_path): # Use the development copy on the computer logger.warning(f"Using local git repo without update: {git_local_path}") if os.path.exists(dest): raise FileExistsError(f"Remove {dest} first.") # shutil.rmtree(dest) shutil.copytree(git_local_path, dest) self.meta['mods'][name] = entry return downloads_path = os.path.expanduser(f"~/Downloads/git/{distributor}/{repo_name}") if os.path.isdir(downloads_path): if not self.offline: if self.pull: echo0(f" pulling {downloads_path}") git_repo = Repo(downloads_path) git_repo.remotes.origin.pull() else: echo0(f" using existing {downloads_path}") if os.path.exists(dest): raise FileExistsError(f"Remove {dest} first.") # shutil.rmtree(dest) shutil.copytree(downloads_path, dest) self.meta['mods'][name] = entry return else: if self.offline: raise FileNotFoundError(f"Mod {name} not found in offline mode: {downloads_path}") else: echo0(f" cloning {url} → {downloads_path}") Repo.clone_from(url, downloads_path) if os.path.exists(dest): raise FileExistsError(f"Remove {dest} first.") # shutil.rmtree(dest) shutil.copytree(downloads_path, dest) # dest_git = os.path.join(dest, ".git") # if os.path.isdir(dest_git): # shutil.rmtree(dest_git) self.meta['mods'][name] = entry return def remove_mod(self, name: str): path = os.path.join(self.mods_target, name) if os.path.isdir(path): echo0(f" removing {name}") shutil.rmtree(path) def apply_remove_list(self): for m in gamespec.get('remove_mods', []): self.remove_mod(m) def install_all_mods(self): for entry in gamespec.get('add_mods', []): self.install_mod(entry) def write_game_conf(self): path = os.path.join(self.target_game, "game.conf") with open(path, "w", encoding="utf-8") as f: f.write("name = ENLIVEN\n") f.write("description = For building immersive worlds using ramping, consistent style, and emphasizing world interaction over menus\n") def update_conf(self, path: str): """Append settings only if not already present (line-based, stripped comparison)""" os.makedirs(os.path.dirname(path), exist_ok=True) # Collect base settings new_settings = BASE_ENLIVEN_CONF_SETTINGS.copy() new_settings.update(self.more_conf) # Add version-specific player animation setting if 'playeranim' in self.meta['mods']: # TODO: Make version keys and values in gamespec if self.minetest_version == "5": new_settings['playeranim.model_version'] = "MTG_4_Nov_2017" else: # "0.4" new_settings['playeranim.model_version'] = "MTG_4_Jun_2017" if not new_settings: return update_conf(path, new_settings) # desired_set = {line.strip() for line in desired_lines if line.strip()} def build(self, conf_path: str = None): self.prepare_target() self.apply_remove_list() self.install_all_mods() self.write_game_conf() # Default conf path if not provided if not conf_path: conf_path = os.path.join(self.source_game, "minetest.conf.enliven") self.update_conf(conf_path) echo0("\nBuild finished.") echo0(f"Game location: {self.target_game}") echo0(f"Config : {conf_path}") echo0("Next steps:") echo0(" • review & edit the minetest.conf file") echo0(" • test in Minetest")