2 from . import MinetestCheckError, ContentType
3 from .config import parse_conf
5 basenamePattern = re.compile("^([a-z0-9_]+)$")
7 def get_base_dir(path):
8 if not os.path.isdir(path):
9 raise IOError("Expected dir")
11 root, subdirs, files = next(os.walk(path))
12 if len(subdirs) == 1 and len(files) == 0:
13 return get_base_dir(path + "/" + subdirs[0])
18 def detect_type(path):
19 if os.path.isfile(path + "/game.conf"):
20 return ContentType.GAME
21 elif os.path.isfile(path + "/init.lua"):
22 return ContentType.MOD
23 elif os.path.isfile(path + "/modpack.txt") or \
24 os.path.isfile(path + "/modpack.conf"):
25 return ContentType.MODPACK
26 elif os.path.isdir(path + "/mods"):
27 return ContentType.GAME
28 elif os.path.isfile(path + "/texture_pack.conf"):
29 return ContentType.TXP
31 return ContentType.UNKNOWN
34 def get_csv_line(line):
38 return [x.strip() for x in line.split(",") if x.strip() != ""]
41 class PackageTreeNode:
42 def __init__(self, baseDir, relative, author=None, repo=None, name=None):
43 self.baseDir = baseDir
44 self.relative = relative
52 self.type = detect_type(baseDir)
55 if self.type == ContentType.GAME:
56 if not os.path.isdir(baseDir + "/mods"):
57 raise MinetestCheckError(("Game at {} does not have a mods/ folder").format(self.relative))
58 self.add_children_from_mod_dir("mods")
59 elif self.type == ContentType.MOD:
60 if self.name and not basenamePattern.match(self.name):
61 raise MinetestCheckError(("Invalid base name for mod {} at {}, names must only contain a-z0-9_.") \
62 .format(self.name, self.relative))
63 elif self.type == ContentType.MODPACK:
64 self.add_children_from_mod_dir(None)
67 def getMetaFilePath(self):
69 if self.type == ContentType.GAME:
70 filename = "game.conf"
71 elif self.type == ContentType.MOD:
73 elif self.type == ContentType.MODPACK:
74 filename = "modpack.conf"
75 elif self.type == ContentType.TXP:
76 filename = "texture_pack.conf"
80 return self.baseDir + "/" + filename
88 with open(self.getMetaFilePath() or "", "r") as myfile:
89 conf = parse_conf(myfile.read())
90 for key, value in conf.items():
96 if not "description" in result:
98 with open(self.baseDir + "/description.txt", "r") as myfile:
99 result["description"] = myfile.read()
104 if "depends" in result or "optional_depends" in result:
105 result["depends"] = get_csv_line(result.get("depends"))
106 result["optional_depends"] = get_csv_line(result.get("optional_depends"))
108 elif os.path.isfile(self.baseDir + "/depends.txt"):
109 pattern = re.compile("^([a-z0-9_]+)\??$")
111 with open(self.baseDir + "/depends.txt", "r") as myfile:
112 contents = myfile.read()
115 for line in contents.split("\n"):
117 if pattern.match(line):
118 if line[len(line) - 1] == "?":
119 soft.append( line[:-1])
123 result["depends"] = hard
124 result["optional_depends"] = soft
127 result["depends"] = []
128 result["optional_depends"] = []
131 def checkDependencies(deps):
132 for dep in result["depends"]:
133 if not basenamePattern.match(dep):
135 raise MinetestCheckError(("Invalid dependency name '{}' for mod at {}, did you forget a comma?") \
136 .format(dep, self.relative))
138 raise MinetestCheckError(("Invalid dependency name '{}' for mod at {}, names must only contain a-z0-9_.") \
139 .format(dep, self.relative))
143 checkDependencies(result["depends"])
144 checkDependencies(result["optional_depends"])
146 # Fix games using "name" as "title"
147 if self.type == ContentType.GAME:
148 result["title"] = result["name"]
152 if "name" in result and not "title" in result:
153 result["title"] = result["name"].replace("_", " ").title()
155 # Calculate short description
156 if "description" in result:
157 desc = result["description"]
158 idx = desc.find(".") + 1
159 cutIdx = min(len(desc), 200 if idx < 5 else idx)
160 result["short_description"] = desc[:cutIdx]
163 self.name = result["name"]
168 def add_children_from_mod_dir(self, subdir):
170 relative = self.relative
173 relative += subdir + "/"
175 for entry in next(os.walk(dir))[1]:
176 path = os.path.join(dir, entry)
177 if not entry.startswith('.') and os.path.isdir(path):
178 child = PackageTreeNode(path, relative + entry + "/", name=entry)
179 if not child.type.isModLike():
180 raise MinetestCheckError(("Expecting mod or modpack, found {} at {} inside {}") \
181 .format(child.type.value, child.relative, self.type.value))
183 if child.name is None:
184 raise MinetestCheckError(("Missing base name for mod at {}").format(self.relative))
186 self.children.append(child)
188 def getModNames(self):
189 return self.fold("name", type=ContentType.MOD)
191 # attr: Attribute name
192 # key: Key in attribute
193 # retval: Accumulator
194 # type: Filter to type
195 def fold(self, attr, key=None, retval=None, type=None):
199 # Iterate through children
200 for child in self.children:
201 child.fold(attr, key, retval, type)
204 if type and type != self.type:
208 at = getattr(self, attr)
213 value = at if key is None else at.get(key)
214 if isinstance(value, list):
222 return self.meta.get(key)
225 for child in self.children: