#!/usr/bin/env python3 ''' ENLIVEN subgame builder - creates ENLIVEN based on minetest_game Merges mods & settings from old bash installer script ''' from __future__ import print_function import sys import os import argparse # import configparser import shutil from typing import List, Dict, Union # Assuming these exist in your pyenliven module from pyenliven import ( echo0, # getSGPath, # not used here anymore # profile, MODS_STOPGAP_DIR, ) # ────────────────────────────────────────────────────────────── # M O D L I S T F R O M B A S H S C R I P T # ────────────────────────────────────────────────────────────── # Format: # 'name': folder name expected in mods/ or mods_stopgap/ # 'repo': git URL (str or list[str] — first = highest priority) # 'branch': optional branch name # 'stopgap_only': True → only use from MODS_STOPGAP_DIR, ignore repo gamespec: Dict[str, any] = {} gamespec['remove_mods'] = [ "facade", # no recipes "placecraft", # interferes with eating "more_chests", # https://github.com/poikilos/EnlivenMinetest/issues/446 "emeralds", # https://github.com/poikilos/EnlivenMinetest/issues/497 "give_initial_stuff", "xban2", "dynamic_liquid", "stamina", "hbarmor", "hbhunger", "hudbars", "hbsprint", "dungeon_loot", # replaced by treasurer + trm_* "helicopter", # known crash issues in older versions # add others from issue #310 if desired ] gamespec['add_mods']: List[Dict[str, any]] = [ # ── Utility / Admin ──────────────────────────────────────── {'name': 'invhack', 'repo': "https://github.com/salahzar/minetest-invhack.git"}, {'name': 'worldedit', 'repo': "https://github.com/Uberi/MineTest-WorldEdit.git"}, {'name': 'metatools', 'repo': "https://github.com/poikilos/metatools.git"}, {'name': 'protector', 'repo': "https://notabug.org/TenPlus1/protector.git"}, {'name': 'advancedban', 'repo': "https://github.com/srifqi/advancedban"}, {'name': 'areas', 'repo': "https://github.com/ShadowNinja/areas.git"}, {'name': 'whitelist', 'repo': "https://github.com/ShadowNinja/whitelist.git"}, {'name': 'vote', 'repo': "https://github.com/minetest-mods/vote.git"}, # ── Mobs & Worldgen ──────────────────────────────────────── {'name': 'worldedge', 'repo': "https://github.com/minetest-mods/worldedge.git"}, {'name': 'mobs_redo', 'repo': "https://notabug.org/TenPlus1/mobs_redo.git"}, {'name': 'mobs_monster', 'repo': "https://notabug.org/TenPlus1/mobs_monster.git"}, {'name': 'mobs_animal', 'repo': "https://notabug.org/TenPlus1/mobs_animal.git"}, {'name': 'mob_horse', 'repo': "https://notabug.org/tenplus1/mob_horse.git"}, {'name': 'mobs_sky', 'repo': "https://github.com/poikilos/mobs_sky.git"}, {'name': 'spawners', 'repo': "https://bitbucket.org/minetest_gamers/spawners.git"}, {'name': 'tsm_pyramids', 'repo': "https://github.com/poikilos/tsm_pyramids.git"}, {'name': 'tsm_chests_dungeon', 'repo': "http://repo.or.cz/minetest_tsm_chests_dungeon.git"}, {'name': 'treasurer', 'repo': "http://repo.or.cz/minetest_treasurer.git"}, {'name': 'trm_pyramids'}, # special – files copied directly in bash → handle manually or stopgap {'name': 'moreblocks', 'repo': "https://github.com/minetest-mods/moreblocks.git"}, {'name': 'plantlife_modpack','repo': "https://github.com/mt-mods/plantlife_modpack.git"}, {'name': 'bushes_soil', 'repo': "https://github.com/poikilos/bushes_soil.git"}, {'name': 'lapis', 'repo': "https://github.com/Napiophelios/LapisLazuli.git"}, {'name': 'biome_lib', 'repo': "https://github.com/mt-mods/biome_lib.git"}, {'name': 'moretrees', 'repo': "https://github.com/mt-mods/moretrees.git"}, {'name': 'mesecons', 'repo': "https://github.com/minetest-mods/mesecons"}, {'name': 'pipeworks', 'repo': "https://github.com/mt-mods/pipeworks.git"}, {'name': 'technic', 'repo': "https://github.com/minetest-mods/technic.git"}, {'name': 'technic_armor', 'repo': "https://github.com/stujones11/technic_armor.git"}, {'name': 'mapgen_helper', 'repo': "https://github.com/minetest-mods/mapgen_helper.git"}, {'name': 'subterrane', 'repo': "https://github.com/minetest-mods/subterrane.git"}, {'name': 'caverealms', 'repo': "https://github.com/FaceDeer/minetest-caverealms.git"}, {'name': 'moreores', 'repo': "https://github.com/minetest-mods/moreores.git"}, {'name': 'tsm_mines', 'repo': "http://repo.or.cz/tsm_mines.git"}, {'name': 'tsm_railcorridors','repo': "http://repo.or.cz/RailCorridors/tsm_railcorridors.git"}, {'name': 'birthstones', 'repo': "https://github.com/poikilos/birthstones.git"}, {'name': 'bakedclay', 'repo': "https://notabug.org/tenplus1/bakedclay.git"}, {'name': 'quartz', 'repo': "https://github.com/minetest-mods/quartz"}, {'name': 'magma_conduits', 'repo': "https://github.com/FaceDeer/magma_conduits.git"}, {'name': 'boost_cart', 'repo': "https://github.com/SmallJoker/boost_cart.git"}, # ── Gameplay / Items ─────────────────────────────────────── {'name': 'throwing', 'repo': "https://github.com/minetest-mods/throwing.git"}, {'name': 'throwing_arrows', 'repo': "https://github.com/minetest-mods/throwing_arrows.git"}, {'name': 'fishing', 'repo': "https://github.com/MinetestForFun/fishing.git", 'issues': ["Make sure fishing rods recipe works"]}, {'name': 'compassgps', 'repo': "https://github.com/poikilos/compassgps.git"}, {'name': 'sounding_line', 'repo': "https://github.com/minetest-mods/sounding_line.git"}, {'name': 'mywalls', 'repo': "https://github.com/minetest-mods/mywalls.git"}, {'name': 'mymasonhammer', 'repo': "https://github.com/minetest-mods/mymasonhammer.git"}, {'name': 'ts_furniture', 'repo': "https://github.com/minetest-mods/ts_furniture.git"}, {'name': '3d_armor', 'repo': "https://github.com/stujones11/minetest-3d_armor.git"}, {'name': 'basic_materials','repo': "https://github.com/mt-mods/basic_materials.git"}, {'name': 'homedecor_modpack','repo': "https://github.com/mt-mods/homedecor_modpack.git"}, {'name': 'homedecor_ua', 'repo': "https://github.com/poikilos/homedecor_ua.git"}, {'name': 'unifieddyes', 'repo': "https://github.com/mt-mods/unifieddyes.git"}, {'name': 'travelnet', 'repo': "https://github.com/Sokomine/travelnet.git"}, {'name': 'anvil', 'repo': "https://github.com/minetest-mods/anvil.git"}, {'name': 'sling', 'repo': "https://github.com/minetest-mods/sling.git"}, {'name': 'signs_lib', 'repo': "https://github.com/mt-mods/signs_lib.git"}, {'name': 'slimenodes', 'repo': "https://github.com/poikilos/slimenodes.git"}, {'name': 'ropes', 'repo': "https://github.com/minetest-mods/ropes.git"}, {'name': 'digilines', 'repo': "https://github.com/minetest-mods/digilines.git"}, {'name': 'trmp_minetest_game','repo': "https://github.com/poikilos/trmp_minetest_game.git"}, {'name': 'awards', 'repo': "https://gitlab.com/rubenwardy/awards.git"}, {'name': 'awards_board', 'repo': "https://framagit.org/xisd-minetest/awards_board.git"}, {'name': 'item_drop', 'repo': "https://github.com/minetest-mods/item_drop.git"}, {'name': 'sponge', 'repo': "https://github.com/BenjieFiftysix/sponge"}, # ── Player UX ────────────────────────────────────────────── {'name': 'money', 'repo': "https://notabug.org/TenPlus1/money"}, {'name': 'lightning', 'repo': "https://github.com/minetest-mods/lightning.git"}, {'name': 'unified_inventory','repo': [ "https://github.com/minetest-mods/unified_inventory.git", # official "https://github.com/MinetestForFun/unified_inventory" # old bash preference ]}, {'name': 'player_monoids', 'repo': "https://github.com/minetest-mods/player_monoids.git"}, {'name': 'sprint', 'repo': "https://github.com/GunshipPenguin/sprint.git"}, {'name': 'hunger_ng', 'repo': "https://gitlab.com/4w/hunger_ng.git"}, {'name': 'playereffects', 'repo': "https://github.com/sys4-fr/playereffects"}, {'name': 'ambience', 'repo': "https://notabug.org/tenplus1/ambience.git"}, {'name': 'playeranim', 'repo': "https://github.com/minetest-mods/playeranim.git"}, {'name': 'skinsdb', 'repo': "https://github.com/minetest-mods/skinsdb.git"}, {'name': 'woodcutting', 'repo': "https://github.com/minetest-mods/woodcutting.git"}, # ── Legacy / Special ─────────────────────────────────────── #{'name': 'animal_materials_legacy'}, #{'name': 'elk_legacy'}, #{'name': 'glooptest_missing'}, #{'name': 'nftools_legacy'}, ] ''' mt_conf_by_mod settings should be placed in minetest.conf such as /opt/minebest/mtworlds/center/ENLIVEN/minetest.conf but for now just use - patches/subgame/minetest.conf To define the game. If minebest is present, combine minetest.conf and minetest.server-example.conf but maybe make an alternate version with stuff that isn't in world.conf. For other conf settings: - patches/subgame/minetest.server-example.conf goes in the server only. - Place the result in the game directory such as will result in /opt/minebest/mtworlds/center/ENLIVEN/minetest.conf - patches/subgame/minetest.client-example.conf goes in clients only. ''' BASE_ENLIVEN_CONF_SETTINGS = [ # General / map "enable_lapis_mod_columns = true", "map_generation_limit = 5000", # Protector "protector_radius = 7", "protector_flip = true", "protector_pvp = true", "protector_pvp_spawn = 10", "protector_drop = false", "protector_hurt = 3", # Other gameplay "world_edge = 5000", "default_privs = interact,shout,home", "max_users = 50", "motd = \"Actions and chat messages are logged. Use inventory to see recipes (use web for live map if available).\"", "disallow_empty_passwords = true", "secure.trusted_mods = advanced_npc", "server_dedicated = false", "bones_position_message = true", # Sprint (GunshipPenguin sprint settings) "sprint_speed = 2.25", "sprint_jump = 1.25", "sprint_stamina_drain = .5", ] # Per-mod overrides / extras mt_conf_by_mod = { 'item_drop': { 'item_drop.pickup_radius': "1.425", }, 'throwing_arrows': { 'throwing.enable_arrow': "true", }, } why = {} why["https://github.com/MinetestForFun/unified_inventory"] = ''' This fork makes a "nicer interface". The fork hasn't been tested yet. ''' # deprecates https://github.com/poikilos/vines.git fork of Facedeer's: why["https://github.com/FaceDeer/vines.git"] = ''' > I've finally done it, I've split this mod in twain. The new > stand-alone ropes mod has no dependency on biome_lib and no vine > content, though its crafting recipes remain compatible with the vines > produced by this mod. > > My fork of this vines mod has had the rope-related content removed > from it, leaving it as just a vines mod. Note that I haven't tested > it extensively - I have to admit, I've mainly been in this for the > ropes. :) I'll do what I can to maintain it, though, if anyone has > bug reports or requests. > > I've added a node upgrade function to the new ropes mod that will > convert the ropes from both my fork of the vines mod and the original > version of the vines mod by bas080 to the new ropes mod's ropes. So > if you wish to upgrade an existing world it should work. - FaceDeer on [[Mod] Vines and Rope [2.3] [vines]] (https://forums.minetest.org/viewtopic.php?f=11&t=2344&start=50 &sid=bf15c996963e891cd3f2460c2525044a) Note that vines requires: default biome_lib moretrees? doc? intllib? mobs? creatures? ''' gamespec['disable_mobs'] = [ "old_lady", ] """ known_issues = {} known_issues['fishing'] = "Make sure fishing rods recipe works" server_only_mods = [ 'ircpack', 'chat3', # debatable – was removed in some places ] # ────────────────────────────────────────────────────────────── class GameBuilder: def __init__(self, minetest_game_path: str, minetest_version: str = "5"): 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.mods_target = os.path.join(self.target_game, "mods") self.minetest_version = minetest_version # "5" or "0.4" 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}") echo0(f"Building ENLIVEN → {self.target_game}") echo0(f" from base: {self.source_game}") echo0(f" target Minetest compatibility: {self.minetest_version}") def prepare_target(self): """Copy minetest_game → ENLIVEN if needed""" if os.path.exists(self.target_game): echo0(f"Target already exists: {self.target_game}") echo0("→ delete it manually if you want fresh copy") 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) if not name: echo0("Skipping entry without 'name'") return 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) return # 2. Git clone if we have repo URL(s) if repo and not stopgap_only: urls = [repo] if isinstance(repo, str) else repo cloned = False for url in urls: echo0(f" trying → {url}") # Here should be real git clone logic. # For now just placeholder message. # You can use subprocess.run(["git", "clone", ...]) echo0(f" [TODO git clone] {url} → {dest}") cloned = True # pretend success break if cloned: return echo0(f"→ WARNING: no source found for {name}") def remove_mod(self, modname: str): path = os.path.join(self.mods_target, modname) if os.path.isdir(path): echo0(f" removing {modname}") 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 = Enhanced minetest_game experience\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 desired_lines = list(BASE_ENLIVEN_CONF_SETTINGS) # Add version-specific player animation setting if self.minetest_version == "5": desired_lines.append("playeranim.model_version = MTG_4_Nov_2017") else: # "0.4" desired_lines.append("playeranim.model_version = MTG_4_Jun_2017") # Add per-mod settings for modname, settings in mt_conf_by_mod.items(): desired_lines.append(f"# settings from {modname}") for k, v in settings.items(): desired_lines.append(f"{k} = {v}") desired_lines.append("") if not desired_lines: return # Normalize for comparison (strip, remove empty) desired_set = {line.strip() for line in desired_lines if line.strip()} # Read existing content existing_lines = [] if os.path.isfile(path): with open(path, "r", encoding="utf-8") as f: existing_lines = [line.rstrip("\n") for line in f] existing_set = {line.strip() for line in existing_lines if line.strip()} else: existing_set = set() # Find lines to add to_add = [line for line in desired_lines if line.strip() and line.strip() not in existing_set] if not to_add: echo0(f"minetest.conf already contains all desired ENLIVEN settings ({path})") return # Write mode mode = "a" if os.path.isfile(path) else "w" with open(path, mode, encoding="utf-8") as f: if mode == "w": f.write(f"# ENLIVEN subgame recommended settings (Minetest {self.minetest_version} compatibility)\n\n") for line in to_add: f.write(line + "\n") echo0(f"Updated {path} — added {len(to_add)} new line(s)") def build(self, conf_path: str = None): self.prepare_target() self.apply_remove_list() self.install_all_mods() self.write_game_conf() 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") def main(): parser = argparse.ArgumentParser(description="Build ENLIVEN subgame from minetest_game") parser.add_argument("minetest_game_path", help="Path to minetest_game directory") parser.add_argument("--conf", "-c", dest="conf_path", help="Path to write/append ENLIVEN minetest.conf settings " "(default: minetest_game_path/minetest.conf.enliven)") parser.add_argument("--minetest-version", "-v", dest="minetest_version", choices=["0.4", "5"], default="5", help="Target Minetest version compatibility: '5' (default) or '0.4'") args = parser.parse_args() try: builder = GameBuilder( args.minetest_game_path, minetest_version=args.minetest_version ) builder.build(conf_path=args.conf_path) return 0 except Exception as exc: echo0(f"ERROR: {exc}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())