6 changed files with 713 additions and 399 deletions
@ -0,0 +1,205 @@ |
|||
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") |
|||
@ -0,0 +1,354 @@ |
|||
import shutil |
|||
import os |
|||
|
|||
from collections import OrderedDict |
|||
from decimal import Decimal |
|||
from typing import Dict, List |
|||
|
|||
|
|||
gamespec: OrderedDict[str, any] = OrderedDict() |
|||
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", # or 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 ──────────────────────────────────────── |
|||
# ^ See also inventory_admin mod which is purely command-based |
|||
{'name': "advancedban", 'repo': "https://github.com/srifqi/advancedban"}, |
|||
{'name': "areas", 'repo': "https://github.com/ShadowNinja/areas.git"}, |
|||
{'name': "invhack", 'repo': "https://github.com/salahzar/minetest-invhack.git", |
|||
'privs': {'moderator': ['invhack']}, |
|||
'items': {'moderator': ['invhack:tool']}}, |
|||
{'name': "metatools", 'repo': "https://github.com/poikilos/metatools.git"}, |
|||
{'name': "modlist", 'repo': "https://github.com/SkyBuilder1717/modlist.git"}, |
|||
# {'name': "protector", 'repo': ["https://notabug.org/TenPlus1/protector.git", |
|||
# "https://codeberg.org/tenplus1/protector.git"]}, |
|||
{'name': "protect_block_area", 'repo': "https://github.com/C-C-Minetest-Server/protect_block_area.git"}, |
|||
{'name': "vote", 'repo': "https://github.com/minetest-mods/vote.git"}, |
|||
{'name': "whitelist", 'repo': "https://github.com/ShadowNinja/whitelist.git"}, |
|||
{'name': "worldedit", 'repo': "https://github.com/Uberi/MineTest-WorldEdit.git"}, |
|||
|
|||
# Mobs |
|||
#{'name': "mobs_redo", 'repo': ["https://notabug.org/TenPlus1/mobs_redo.git", |
|||
# "https://codeberg.org/tenplus1/mobs_redo.git"]}, |
|||
#{'name': "mobs_monster", 'repo': ["https://notabug.org/TenPlus1/mobs_monster.git", |
|||
# "https://codeberg.org/tenplus1/mobs_monster.git"]}, |
|||
#{'name': "mobs_animal", 'repo': ["https://notabug.org/TenPlus1/mobs_animal.git", |
|||
# "https://codeberg.org/tenplus1/mobs_animal.git"]}, |
|||
#{'name': "mob_horse", 'repo': ["https://notabug.org/tenplus1/mob_horse.git", |
|||
# "https://codeberg.org/tenplus1/mob_horse.git"]}, |
|||
#{'name': "mobs_sky", 'repo': "https://github.com/poikilos/mobs_sky.git"}, |
|||
#{'name': "mobs_water", 'repo': "https://github.com/blert2112/mobs_water.git"}, |
|||
#{'name': "dmobs", 'repo': ["https://github.com/minetest-mobs-mods/dmobs.git", |
|||
# "https://codeberg.org/tenplus1/dmobs"]}, |
|||
# TODO: ^ Make open_ai mobs api bridge for these |
|||
{'name': "open_ai", 'repo': "https://github.com/Poikilos/open_ai.git"}, |
|||
{'name': "spawners", 'repo': "https://bitbucket.org/minetest_gamers/spawners.git"}, |
|||
|
|||
# Worldgen |
|||
{'name': "bakedclay", 'repo': ["https://notabug.org/tenplus1/bakedclay.git", |
|||
"https://codeberg.org/tenplus1/bakedclay.git"]}, |
|||
{'name': "biome_lib", 'repo': "https://github.com/mt-mods/biome_lib.git"}, |
|||
# {'name': "birthstones", 'repo': "https://github.com/poikilos/birthstones.git"}, # commented to reduce inventory overload |
|||
{'name': "bushes_soil", 'repo': "https://github.com/poikilos/bushes_soil.git"}, |
|||
{'name': "caverealms", 'repo': "https://github.com/FaceDeer/minetest-caverealms.git"}, |
|||
{'name': "lapis", 'repo': "https://github.com/Napiophelios/LapisLazuli.git", |
|||
'settings': OrderedDict(enable_lapis_mod_columns=True), |
|||
'why': "Unlike, minetest-mods/lapis, LapisLazuli has more things you can make."}, |
|||
{'name': "mapgen_helper", 'repo': "https://github.com/minetest-mods/mapgen_helper.git"}, |
|||
{'name': "mesecons", 'repo': "https://github.com/minetest-mods/mesecons"}, |
|||
{'name': "moreblocks", 'repo': "https://github.com/minetest-mods/moreblocks.git"}, |
|||
{'name': "moreores", 'repo': "https://github.com/minetest-mods/moreores.git"}, |
|||
{'name': "moretrees", 'repo': "https://github.com/mt-mods/moretrees.git"}, |
|||
{'name': "plantlife_modpack", 'repo': "https://github.com/mt-mods/plantlife_modpack.git"}, |
|||
{'name': "pipeworks", 'repo': "https://github.com/mt-mods/pipeworks.git"}, |
|||
{'name': "subterrane", 'repo': "https://github.com/minetest-mods/subterrane.git"}, |
|||
{'name': "technic", 'repo': "https://github.com/minetest-mods/technic.git"}, |
|||
{'name': "technic_armor", 'repo': "https://github.com/stujones11/technic_armor.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"}, # see "loot" mod instead |
|||
# {'name': "treasurer", 'repo': "http://repo.or.cz/minetest_treasurer.git"}, |
|||
# {'name': "trm_pyramids"}, # special – files copied directly in bash → handle manually or stopgap |
|||
# {'name': "tsm_mines", 'repo': "http://repo.or.cz/tsm_mines.git"}, fork of BlockMen’s [Mines](https://forum.minetest.net/viewtopic.php?f=11&t=6307) replaced by tsm_railcorridors |
|||
# {'name': "tsm_railcorridors", 'repo': ["http://repo.or.cz/RailCorridors/tsm_railcorridors.git", |
|||
# "https://codeberg.org/Wuzzy/minetest_tsm_railcorridors.git"]}, |
|||
# TODO: ^ Make loot version of these |
|||
{'name': "loot", 'repo': "https://github.com/minetest-mods/loot.git", |
|||
'why': "Defines loot API and adds loot to dungeons", |
|||
'settings': OrderedDict(loot_dungeons=True)}, |
|||
# {'name': 'quartz', 'repo': "https://github.com/minetest-mods/quartz"}, # commented to reduce inventory overload |
|||
{'name': "magma_conduits", 'repo': "https://github.com/FaceDeer/magma_conduits.git"}, |
|||
{'name': "worldedge", 'repo': "https://github.com/minetest-mods/worldedge.git"}, |
|||
|
|||
# ── Gameplay / Items ─────────────────────────────────────── |
|||
{'name': "3d_armor", 'repo': "https://github.com/stujones11/minetest-3d_armor.git"}, |
|||
{'name': "anvil", 'repo': "https://github.com/minetest-mods/anvil.git"}, |
|||
{'name': "awards", 'repo': "https://gitlab.com/rubenwardy/awards.git"}, |
|||
{'name': "awards_board", 'repo': "https://framagit.org/xisd-minetest/awards_board.git"}, |
|||
{'name': "basic_materials",'repo': "https://github.com/mt-mods/basic_materials.git"}, |
|||
{'name': "boost_cart", 'repo': "https://github.com/SmallJoker/boost_cart.git"}, |
|||
{'name': "compassgps", 'repo': "https://github.com/poikilos/compassgps.git"}, |
|||
{'name': "digilines", 'repo': "https://github.com/minetest-mods/digilines.git"}, |
|||
{'name': "fishing", 'repo': "https://github.com/MinetestForFun/fishing.git", |
|||
'issues': ["Make sure fishing rods recipe works"], |
|||
'version-note': "Minetestforfun's (NOT wulfsdad's) fishing <https://forum.minetest.net/viewtopic.php?f=11&t=13659>"}, |
|||
{'name': "homedecor_modpack", 'repo': "https://github.com/mt-mods/homedecor_modpack.git"}, |
|||
{'name': "homedecor_ua", 'repo': "https://github.com/poikilos/homedecor_ua.git"}, |
|||
{'name': "item_drop", 'repo': "https://github.com/minetest-mods/item_drop.git", |
|||
'settings': {'item_drop.pickup_radius': "1.425"}}, |
|||
# {'name': 'mymasonhammer', 'repo': "https://github.com/minetest-mods/mymasonhammer.git", |
|||
# 'what': "A hammer that cuts stairs and ladders in blocks"}, |
|||
{'name': "mywalls", 'repo': "https://github.com/minetest-mods/mywalls.git", |
|||
'what': "Adds more wall types for walls mod from minetest_game."}, |
|||
{'name': "ropes", 'repo': "https://github.com/minetest-mods/ropes.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': "sounding_line", 'repo': "https://github.com/minetest-mods/sounding_line.git"}, |
|||
{'name': "stamina", 'repo': "https://codeberg.org/tenplus1/stamina.git", |
|||
'comment': "Patches item_eat to affect saturation instead of health.", |
|||
'help-dev': "Changes ItemStack before register_on_item_eat callbacks, but they can use 6th param for original ItemStack", |
|||
'privs': {'invincible':['no_hunger']}}, |
|||
{'name': "sponge", 'repo': "https://github.com/BenjieFiftysix/sponge"}, |
|||
# TODO: ^ Test, make sure is in mapgen. Add underwater/sealife mod & integrate |
|||
{'name': "throwing", 'repo': "https://github.com/minetest-mods/throwing.git"}, |
|||
{'name': "throwing_arrows", 'repo': "https://github.com/minetest-mods/throwing_arrows.git", |
|||
'settings': {'throwing.enable_arrow': "true"}}, |
|||
{'name': "travelnet", 'repo': "https://github.com/Sokomine/travelnet.git"}, |
|||
{'name': "ts_furniture", 'repo': "https://github.com/minetest-mods/ts_furniture.git"}, |
|||
# {'name': 'trmp_minetest_game','repo': "https://github.com/poikilos/trmp_minetest_game.git"}, |
|||
# TODO: ^ Make a version for loot |
|||
# {'name': 'unifieddyes', 'repo': "https://github.com/mt-mods/unifieddyes.git"}, |
|||
# TODO: ^ add Poikilos/dyed mod |
|||
|
|||
# ── Player UX ────────────────────────────────────────────── |
|||
{'name': "ambience", 'repo': ["https://notabug.org/tenplus1/ambience.git", |
|||
"https://codeberg.org/tenplus1/ambience.git"]}, |
|||
{'name': "hunger_ng", 'repo': "https://gitlab.com/4w/hunger_ng.git"}, |
|||
{'name': "lightning", 'repo': "https://github.com/minetest-mods/lightning.git"}, |
|||
# {'name': "money", 'repo': ["https://notabug.org/TenPlus1/money", |
|||
# "https://codeberg.org/tenplus1/money.git"], |
|||
# 'why-not': "This fork removes everything except barter stations"}, |
|||
{'name': "player_monoids", 'repo': "https://github.com/minetest-mods/player_monoids.git"}, |
|||
{'name': "playeranim", 'repo': "https://github.com/minetest-mods/playeranim.git", |
|||
'what': "Makes the head, and the right arm when you're mining, face the way you're facing"}, |
|||
{'name': "playereffects", 'repo': "https://github.com/sys4-fr/playereffects"}, |
|||
# {'name': 'skinsdb', 'repo': "https://github.com/minetest-mods/skinsdb.git"}, |
|||
# {'name': "sprint", 'repo': "https://github.com/GunshipPenguin/sprint.git"}, |
|||
# TODO: ^ Make sure stamina mod fully implements sprint |
|||
# {'name': 'unified_inventory','repo': [ |
|||
# "https://github.com/minetest-mods/unified_inventory.git", |
|||
# "https://github.com/MinetestForFun/unified_inventory" # fork with "nicer interface" |
|||
# ]}, |
|||
{'name': "sfinv", 'repo': "https://github.com/rubenwardy/sfinv.git", |
|||
'why': "Unified inventory is ugly and has a bloated API."}, |
|||
# {'name': "bags", 'repo': "https://github.com/cornernote/minetest-bags.git"}, |
|||
{'name': "prestibags", 'repo': "https://github.com/Poikilos/prestibags.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"}, |
|||
] |
|||
|
|||
# Preprocess add_mods for repo.or.cz |
|||
for entry in gamespec['add_mods']: |
|||
repos = entry.get('repo') |
|||
if isinstance(repos, str): |
|||
repos = [repos] |
|||
for repo in repos: |
|||
if repo and "repo.or.cz" in repo: |
|||
entry['distributor'] = "Wuzzy" |
|||
break |
|||
|
|||
# Per-mod overrides / extras |
|||
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", |
|||
] |
|||
|
|||
|
|||
server_only_mods = [ |
|||
'ircpack', |
|||
'chat3', |
|||
] |
|||
|
|||
# ────────────────────────────────────────────────────────────── |
|||
|
|||
# ────────────────────────────────────────────────────────────── |
|||
# Collected minetest.conf settings from the bash script |
|||
# ────────────────────────────────────────────────────────────── |
|||
|
|||
BASE_ENLIVEN_CONF_SETTINGS = OrderedDict( |
|||
# General / map |
|||
map_generation_limit=5000, |
|||
# HUD / Controls |
|||
# "stamina_disable_aux1 = true", # require double tap for run (Prevent stamina from taking up aux1 key) |
|||
# "stamina_hud_x =", |
|||
# "stamina_hud_y =", |
|||
# "stamina_double_tap_time =", # 0 to disable |
|||
# 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, |
|||
server_dedicated=False, |
|||
bones_position_message=True, |
|||
# Sprint (GunshipPenguin sprint settings) |
|||
sprint_speed=2.25, |
|||
sprint_jump=1.25, |
|||
sprint_stamina_drain=.5, |
|||
) |
|||
BASE_ENLIVEN_CONF_SETTINGS['secure.trusted_mods'] = "advanced_npc" |
|||
|
|||
|
|||
|
|||
def encode_cv(v): |
|||
"""Encode the value to minetest.conf syntax""" |
|||
if v is None: |
|||
return "" # results in "name =" which is valid syntax |
|||
if isinstance(v, bool): |
|||
return "true" if v else "false" |
|||
if isinstance(v, (int, float, Decimal)): |
|||
return str(v) |
|||
if isinstance(v, str): |
|||
return f'"{v}"' |
|||
raise TypeError( |
|||
"{} type is not implemented in pyenliven Luanti conf encoder" |
|||
.format(type(v).__name__)) |
|||
|
|||
|
|||
def update_conf(path, new_settings): |
|||
# 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() |
|||
|
|||
tmp_path = path + ".tmp" |
|||
if os.path.isfile(tmp_path): |
|||
os.remove(tmp_path) |
|||
changed = 0 |
|||
added = 0 |
|||
same = 0 |
|||
if os.path.isfile(path): |
|||
lineN = 0 |
|||
with open(path, 'r', encoding="utf-8") as src: |
|||
with open(tmp_path, 'w', encoding="utf-8") as dst: |
|||
for rawL in src: |
|||
lineN += 1 |
|||
line = rawL.strip() |
|||
if line.startswith("#"): |
|||
dst.write(rawL) |
|||
continue |
|||
if not line: |
|||
dst.write(rawL) |
|||
continue |
|||
if "=" not in line: |
|||
logger.warning( |
|||
f"{path}, line {lineN}:" |
|||
f" No '=' in {rawL.rstrip()}") |
|||
dst.write(rawL) |
|||
continue |
|||
parts = line.split(line, 1) |
|||
parts[0] = parts[0].strip() |
|||
if not parts[0]: |
|||
logger.warning( |
|||
f"{path}, line {lineN}:" |
|||
f" No name before '=' in {rawL.rstrip()}") |
|||
dst.write(rawL) |
|||
continue |
|||
if len(parts) > 1: |
|||
parts[1] = parts[1].strip() |
|||
else: |
|||
# f"{name} =" is null value syntax for conf |
|||
parts[1] = None |
|||
if parts[0] in new_settings: |
|||
encoded = encode_cv(new_settings[parts[0]]) |
|||
if parts[1] == encoded: |
|||
# no change |
|||
same += 1 |
|||
dst.write(rawL) |
|||
continue |
|||
changed += 1 |
|||
if new_settings[parts[0]] is not None: |
|||
dst.write(f"{parts[0]} = {encoded}\n") |
|||
else: |
|||
dst.write(f"{parts[0]} =\n") |
|||
del new_settings[parts[0]] |
|||
if new_settings: |
|||
mode = "a" if os.path.isfile(tmp_path) else "w" |
|||
with open(tmp_path, mode, encoding="utf-8") as dst: |
|||
for k, v in new_settings.items(): |
|||
if v is not None: |
|||
dst.write(f"{k} = {encode_cv(v)}\n") |
|||
else: |
|||
dst.write(f"{k} =\n") # "{k} = " is null value syntax |
|||
if os.path.isfile(path): |
|||
os.remove(path) |
|||
shutil.move(tmp_path, path) |
|||
|
|||
echo0(f"Updated {path}: added {added} new line(s), changed {changed}, {same} value(s) already matched") |
|||
Loading…
Reference in new issue