You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
7.8 KiB
205 lines
7.8 KiB
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")
|
|
|