This is an experimental copy for testing Poikilos' issue mirroring system. Note that Gitea's migration tool can import issues, but the "Issues" checkbox is disabled when "This repository will be a mirror" is enabled (it is for this repo).
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

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")