wesnoth/data/tools/wmlunits

676 lines
22 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env python3
# encoding: utf-8
"""
wmlunits -- tool to output information on all units in HTML
Run without arguments to see usage.
"""
2017-08-12 06:24:07 +00:00
import argparse
import os
import shutil
import sys
import traceback
import subprocess
import multiprocessing
import queue
import yaml
import wesnoth.wmlparser3 as wmlparser3
import unit_tree.helpers as helpers
import unit_tree.animations as animations
import unit_tree.html_output as html_output
2012-03-04 23:11:15 +00:00
import unit_tree.overview
2012-03-16 18:16:47 +00:00
import unit_tree.wiki_output as wiki_output
TIMEOUT = 20
def copy_images():
print("Recolorizing pictures.")
image_collector.copy_and_color_images(options.output)
shutil.copy2(os.path.join(os.path.dirname(os.path.realpath(__file__)),
"unit_tree/menu.js"),
2017-08-12 06:24:07 +00:00
options.output)
def shell(com):
#print(com)
2017-08-12 06:24:07 +00:00
p = subprocess.Popen(com,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True)
out, err = p.communicate()
#if out: sys.stdout.write(out)
#if err: sys.stdout.write(err)
return p.returncode
def shell_out(com):
p = subprocess.Popen(com,
2017-08-12 06:24:07 +00:00
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = p.communicate()
return out
def bash(name):
return "'" + name.replace("'", "'\\''") + "'"
def move(f, t, name):
if os.path.exists(f + "/" + name + ".cfg"):
com = "mv " + f + "/" + bash(name + ".cfg") + " " + t + "/"
shell(com)
com = "mv " + f + "/" + bash(name) + " " + t + "/"
return shell(com)
2017-08-12 06:24:07 +00:00
_info = {}
def get_info(addon):
global _info
if addon in _info:
return _info[addon]
_info[addon] = None
try:
path = options.addons + "/" + addon + "/_info.cfg"
if os.path.exists(path):
2017-08-12 06:24:07 +00:00
parser = wmlparser3.Parser(options.wesnoth,
options.config_dir,
options.data_dir)
parser.parse_file(path)
_info[addon] = parser
else:
2017-08-12 06:24:07 +00:00
print("Cannot find " + path)
except wmlparser3.WMLError as e:
print(e)
return _info[addon]
_deps = {}
global_addons = set()
def get_dependencies(addon):
global _deps
global global_addons
if addon in _deps:
return _deps[addon]
_deps[addon] = []
try:
2017-08-12 06:24:07 +00:00
info = get_info(addon).get_all(tag="info")[0]
row = info.get_text_val("dependencies")
if row:
deps1 = row.split(",")
else:
deps1 = []
for d in deps1:
if d in global_addons:
_deps[addon].append(d)
else:
2017-08-12 06:24:07 +00:00
print("Missing dependency for " + addon + ": " + d)
except Exception as e:
print(e)
return _deps[addon]
def set_dependencies(addon, depends_on):
_deps[addon] = depends_on
def get_all_dependencies(addon):
result = []
check = get_dependencies(addon)[:]
while check:
d = check.pop()
2017-08-12 06:24:07 +00:00
if d == addon or d in result:
continue
result.append(d)
check += get_dependencies(d)
return result
def sorted_by_dependencies(addons):
sorted = []
unsorted = addons[:]
while unsorted:
n = 0
for addon in unsorted:
for d in get_dependencies(addon):
if d not in sorted:
break
else:
sorted.append(addon)
unsorted.remove(addon)
n += 1
continue
if n == 0:
2017-08-12 06:24:07 +00:00
print("Cannot sort dependencies for these addons: " + str(unsorted))
sorted += unsorted
break
return sorted
def search(batchlist, name):
for info in batchlist:
2017-08-12 06:24:07 +00:00
if info and info["name"] == name:
return info
batchlist.append({})
batchlist[-1]["name"] = name
return batchlist[-1]
def list_contents():
2017-08-12 06:24:07 +00:00
class Empty:
pass
2017-08-12 06:24:07 +00:00
local = Empty()
mainline_eras = set()
filename = options.list
2017-08-12 06:24:07 +00:00
def append(info, id, define, c=None, name=None, domain=None):
info.append({})
2017-08-12 06:24:07 +00:00
info[-1]["id"] = id
info[-1]["define"] = define
2017-08-12 06:24:07 +00:00
info[-1]["name"] = c.get_text_val("name") if c else name
info[-1]["units"] = "?"
info[-1]["translations"] = {}
for isocode in languages:
translation = html_output.Translation(options.transdir, isocode)
def translate(string, domain):
return translation.translate(string, domain)
if c:
2017-08-12 06:24:07 +00:00
t = c.get_text_val("name", translation=translate)
else:
t = translate(name, domain)
if t != info[-1]["name"]:
info[-1]["translations"][isocode] = t
def get_dependency_eras(batchlist, addon):
dependency_eras = list(mainline_eras)
for d in get_all_dependencies(addon):
dinfo = search(batchlist, d)
for era in dinfo.get("eras", []):
dependency_eras.append(era["id"])
return dependency_eras
def list_eras(batchlist, addon):
2017-08-12 06:24:07 +00:00
eras = local.wesnoth.parser.get_all(tag="era")
if addon != "mainline":
dependency_eras = get_dependency_eras(batchlist, addon)
eras = [x for x in eras if not x.get_text_val("id") in dependency_eras]
info = []
for era in eras:
eid = era.get_text_val("id")
if addon == "mainline":
mainline_eras.add(eid)
2017-08-12 06:24:07 +00:00
append(info, eid, "MULTIPLAYER", c=era)
return info
2017-08-12 06:24:07 +00:00
def list_campaigns(batchlist, addon):
2017-08-12 06:24:07 +00:00
campaigns = local.wesnoth.parser.get_all(tag="campaign")
info = []
for campaign in campaigns:
cid = campaign.get_text_val("id")
d = campaign.get_text_val("define")
d2 = campaign.get_text_val("extra_defines")
2017-08-12 06:24:07 +00:00
if d2:
d += "," + d2
d3 = campaign.get_text_val("difficulties")
if d3:
d += "," + d3.split(",")[0]
else:
2017-08-12 06:24:07 +00:00
difficulty = campaign.get_all(tag="difficulty")
if difficulty:
d3 = difficulty[0].get_text_val("define")
if d3:
d += "," + d3
2017-08-12 06:24:07 +00:00
append(info, cid, d, c=campaign)
return info
def parse(wml, defines):
def f(options, wml, defines, q):
local.wesnoth = helpers.WesnothList(
options.wesnoth,
options.config_dir,
options.data_dir,
options.transdir)
#print("remote", local.wesnoth)
try:
local.wesnoth.parser.parse_text(wml, defines)
q.put(("ok", local.wesnoth))
except Exception as e:
q.put(("e", e))
q = multiprocessing.Queue()
2017-08-12 06:24:07 +00:00
p = multiprocessing.Process(target=f, args=(options, wml, defines, q))
p.start()
try:
2017-08-12 06:24:07 +00:00
s, local.wesnoth = q.get(timeout=TIMEOUT)
except queue.Empty:
p.terminate()
raise
#print("local", s, local.wesnoth)
p.join()
if s == "e":
remote_exception = local.wesnoth
raise remote_exception
2017-08-12 06:24:07 +00:00
def get_version(addon):
parser = get_info(addon)
if parser:
2017-08-12 06:24:07 +00:00
for info in parser.get_all(tag="info"):
return info.get_text_val("version") + "*" + info.get_text_val("uploads")
2017-08-12 06:24:07 +00:00
try:
os.makedirs(options.output + "/mainline")
except OSError:
pass
try:
batchlist = yaml.load(open(options.list))
except IOError:
batchlist = []
print("mainline")
info = search(batchlist, "mainline")
info["version"] = "mainline"
info["parsed"] = "false"
parse("{core}{multiplayer/eras.cfg}", "__WMLUNITS__,SKIP_CORE")
info["eras"] = list_eras(batchlist, "mainline")
# Fake mainline campaign to have an overview of the mainline units
info["campaigns"] = []
2017-08-12 06:24:07 +00:00
append(info["campaigns"], "mainline", "", name="Units", domain="wesnoth-help")
if not options.addons_only:
parse("{core}{campaigns}", "__WMLUNITS__,SKIP_CORE")
info["campaigns"] += list_campaigns(batchlist, "mainline")
addons = []
if options.addons:
addons = os.listdir(options.addons)
global global_addons
global_addons = set(addons)
# fill in the map for all dependencies
2014-01-18 15:38:33 +00:00
for addon in addons:
get_dependencies(addon)
# this ensures that info about eras in dependant addons is available
# already
addons = sorted_by_dependencies(addons)
for i, addon in enumerate(addons):
2017-08-12 06:24:07 +00:00
if not os.path.isdir(options.addons + "/" + addon):
continue
2015-09-09 01:19:42 +00:00
mem = "? KB"
try:
mem = dict([x.split("\t", 1) for x in open("/proc/self/status").read().split("\n") if x])["VmRSS:"]
except:
pass
sys.stdout.write("%4d/%4d " % (1 + i, len(addons)) + addon + " [" + mem + "] ... ")
sys.stdout.flush()
d = options.output + "/" + addon
logname = d + "/error.log"
2017-08-12 06:24:07 +00:00
try:
os.makedirs(d)
except OSError:
pass
version = get_version(addon)
move(options.addons, options.config_dir + "/data/add-ons", addon)
2014-01-18 15:38:33 +00:00
for d in get_dependencies(addon):
move(options.addons, options.config_dir + "/data/add-ons", d)
try:
info = search(batchlist, addon)
2017-08-12 06:24:07 +00:00
if info.get("version", "") == version and info.get("parsed", False) is True:
sys.stdout.write("up to date\n")
continue
info["parsed"] = False
info["dependencies"] = get_dependencies(addon)
parse("{core}{multiplayer}{multiplayer/eras.cfg}{~add-ons}", "__WMLUNITS__,MULTIPLAYER,SKIP_CORE")
info["eras"] = list_eras(batchlist, addon)
info["campaigns"] = list_campaigns(batchlist, addon)
info["version"] = version
sys.stdout.write("ok\n")
except wmlparser3.WMLError as e:
ef = open(logname, "w")
2012-03-07 13:20:26 +00:00
ef.write("<PARSE ERROR>\n")
ef.write(str(e))
2012-03-07 13:20:26 +00:00
ef.write("</PARSE ERROR>\n")
ef.close()
sys.stdout.write("failed\n")
except queue.Empty as e:
ef = open(logname, "w")
ef.write("<TIMEOUT ERROR>\n")
ef.write("Failed to parse the WML within " + str(TIMEOUT) + " seconds.")
ef.write("</TIMEOUT ERROR>\n")
ef.close()
sys.stdout.write("failed\n")
except Exception as e:
ef = open(logname, "w")
ef.write("<INTERNAL ERROR>\n")
ef.write(repr(e))
ef.write("</INTERNAL ERROR>\n")
ef.close()
sys.stdout.write("failed\n")
finally:
move(options.config_dir + "/data/add-ons", options.addons, addon)
2014-01-18 15:38:33 +00:00
for d in get_dependencies(addon):
move(options.config_dir + "/data/add-ons", options.addons, d)
2017-08-12 06:24:07 +00:00
yaml.safe_dump(batchlist, open(filename, "w"),
2017-08-12 06:24:07 +00:00
encoding="utf-8", default_flow_style=False)
def process_campaign_or_era(addon, cid, define, batchlist):
n = 0
2017-08-12 06:24:07 +00:00
print(str(addon) + ": " + str(cid) + " " + str(define))
if not define:
define = ""
wesnoth = helpers.WesnothList(
2010-08-18 16:05:36 +00:00
options.wesnoth,
options.config_dir,
options.data_dir,
options.transdir)
wesnoth.batchlist = batchlist
wesnoth.cid = cid
2017-08-12 06:24:07 +00:00
wesnoth.parser.parse_text("{core/units.cfg}", "__WMLUNITS__,NORMAL")
wesnoth.add_units("mainline")
2017-08-12 06:24:07 +00:00
if define == "MULTIPLAYER":
wesnoth.parser.parse_text("{core}{multiplayer}{multiplayer/eras.cfg}{~add-ons}", "__WMLUNITS__,MULTIPLAYER,SKIP_CORE")
wesnoth.add_units(cid)
else:
if addon == "mainline":
if cid != "mainline":
wesnoth.parser.parse_text("{core}{campaigns}", "__WMLUNITS__,SKIP_CORE,NORMAL," + define)
wesnoth.add_units(cid)
else:
wesnoth.parser.parse_text("{core}{~add-ons}", "__WMLUNITS__,SKIP_CORE," + define)
wesnoth.add_units(cid)
2017-08-12 06:24:07 +00:00
if addon == "mainline" and cid == "mainline":
write_animation_statistics(wesnoth)
wesnoth.add_binary_paths(addon, image_collector)
if define == "MULTIPLAYER":
2017-08-12 06:24:07 +00:00
eras = wesnoth.parser.get_all(tag="era")
for era in eras:
wesnoth.add_era(era)
wesnoth.find_unit_factions()
else:
2017-08-12 06:24:07 +00:00
campaigns = wesnoth.parser.get_all(tag="campaign")
for campaign in campaigns:
wesnoth.add_campaign(campaign)
2008-03-31 09:58:49 +00:00
wesnoth.add_languages(languages)
wesnoth.add_terrains()
wesnoth.check_units()
2008-03-31 09:58:49 +00:00
for isocode in languages:
2017-08-12 06:24:07 +00:00
if addon != "mainline" and isocode != "en_US":
continue
if define == "MULTIPLAYER":
for era in list(wesnoth.era_lookup.values()):
if era.get_text_val("id") == cid:
n = html_output.generate_era_report(addon, isocode, era, wesnoth)
break
else:
if cid == "mainline":
n = html_output.generate_campaign_report(addon, isocode, None, wesnoth)
for campaign in list(wesnoth.campaign_lookup.values()):
if campaign.get_text_val("id") == cid:
n = html_output.generate_campaign_report(addon, isocode, campaign, wesnoth)
break
html_output.generate_single_unit_reports(addon, isocode, wesnoth)
2012-03-04 23:11:15 +00:00
return n
def batch_process():
batchlist = yaml.load(open(options.batch))
for addon in batchlist:
name = addon["name"]
set_dependencies(name, addon.get("dependencies", []))
2017-08-12 06:24:07 +00:00
for addon in batchlist:
name = addon["name"]
2017-08-12 06:24:07 +00:00
if not options.reparse and addon.get("parsed", False) == True:
continue
if name == "mainline":
worked = True
else:
worked = (move(options.addons, options.config_dir + "/data/add-ons", name) == 0)
for d in addon.get("dependencies", []):
move(options.addons, options.config_dir + "/data/add-ons", d)
d = options.output + "/" + name
2017-08-12 06:24:07 +00:00
try:
os.makedirs(d)
except OSError:
pass
logname = d + "/error.log"
2017-08-12 06:24:07 +00:00
def err(mess):
ef = open(logname, "a")
ef.write(str(mess))
ef.close()
html_output.write_error = err
2017-08-12 06:24:07 +00:00
try:
if not worked:
2017-08-12 06:24:07 +00:00
print(name + " not found")
continue
2017-08-12 06:24:07 +00:00
for era in addon.get("eras", []):
eid = era["id"]
n = process_campaign_or_era(name, eid, era["define"], batchlist)
era["units"] = n
for campaign in addon.get("campaigns", []):
cid = campaign["id"]
2017-08-12 06:24:07 +00:00
if cid is None:
cid = campaign["define"]
if cid is None:
cid = name
n = process_campaign_or_era(name, cid, campaign["define"], batchlist)
campaign["units"] = n
2017-08-12 06:24:07 +00:00
except wmlparser3.WMLError as e:
ef = open(logname, "a")
2012-03-07 13:20:26 +00:00
ef.write("<WML ERROR>\n")
ef.write(str(e))
2012-03-07 13:20:26 +00:00
ef.write("</WML ERROR>\n")
ef.close()
2017-08-12 06:24:07 +00:00
print(" " + name + " failed")
except Exception as e:
traceback.print_exc()
2017-08-12 06:24:07 +00:00
print(" " + name + " failed")
2012-03-07 13:20:26 +00:00
ef = open(logname, "a")
ef.write("<INTERNAL ERROR>\n")
ef.write("please report as bug\n")
2014-01-27 22:10:13 +00:00
ef.write(str(e))
2012-03-07 13:20:26 +00:00
ef.write("</INTERNAL ERROR>\n")
ef.close()
finally:
if name != "mainline":
move(options.config_dir + "/data/add-ons", options.addons, name)
for d in addon.get("dependencies", []):
move(options.config_dir + "/data/add-ons", options.addons, d)
2017-08-12 06:24:07 +00:00
addon["parsed"] = True
yaml.safe_dump(batchlist, open(options.batch, "w"),
2017-08-12 06:24:07 +00:00
encoding="utf-8", default_flow_style=False)
2012-03-04 23:11:15 +00:00
try:
2017-08-12 06:24:07 +00:00
unit_tree.overview.write_addon_overview(
os.path.join(options.output, name), addon)
2012-03-04 23:11:15 +00:00
except Exception as e:
pass
html_output.html_postprocess_all(batchlist)
def write_unit_ids_UNUSED():
# Write a list with all unit ids, just for fun.
uids = list(wesnoth.unit_lookup.keys())
def by_race(u1, u2):
r = cmp(wesnoth.unit_lookup[u1].rid,
2017-08-12 06:24:07 +00:00
wesnoth.unit_lookup[u2].rid)
if r == 0:
r = cmp(u1, u2)
return r
uids.sort(by_race)
race = None
f = MyFile(os.path.join(options.output, "uids.html"), "w")
f.write("<html><body>")
for uid in uids:
u = wesnoth.unit_lookup[uid]
if u.rid != race:
2017-08-12 06:24:07 +00:00
if race is not None:
f.write("</ul>")
f.write("<p>%s</p>\n" % (u.rid,))
f.write("<ul>")
race = u.rid
f.write("<li>%s</li>\n" % (uid, ))
f.write("</ul>")
f.write("</body></html>")
f.close()
def write_animation_statistics(wesnoth):
# Write animation statistics
f = html_output.MyFile(os.path.join(options.output, "animations.html"), "w")
animations.write_table(f, wesnoth)
f.close()
if __name__ == '__main__':
# We change the process name to "wmlunits"
try:
import ctypes
libc = ctypes.CDLL("libc.so.6")
libc.prctl(15, b"wmlunits", 0, 0, 0)
except: # oh well...
pass
global options
global image_collector
ap = argparse.ArgumentParser()
ap.add_argument("-C", "--config-dir",
2017-08-12 06:24:07 +00:00
help="Specify the user configuration dir (wesnoth --config-path).")
ap.add_argument("-D", "--data-dir",
2017-08-12 06:24:07 +00:00
help="Specify the wesnoth data dir (wesnoth --path).")
ap.add_argument("-l", "--language", default="all",
2017-08-12 06:24:07 +00:00
help="Specify a language to use. Else output is produced for all languages.")
ap.add_argument("-o", "--output",
2017-08-12 06:24:07 +00:00
help="Specify the output directory.")
ap.add_argument("-n", "--nocopy", action="store_true",
2017-08-12 06:24:07 +00:00
help="No copying of files. By default all images are copied to the output dir.")
ap.add_argument("-w", "--wesnoth",
2017-08-12 06:24:07 +00:00
help="Specify the wesnoth executable to use. Whatever data " +
"and config paths that executable is configured for will be " +
"used to find game files and addons.")
ap.add_argument("-t", "--transdir",
2017-08-12 06:24:07 +00:00
help="Specify the directory with gettext message catalogues. " +
"Defaults to ./translations.", default="translations")
ap.add_argument("-T", "--timeout",
2017-08-12 06:24:07 +00:00
help="Use the timeout iven in seconds when parsing WML, default is 20 " +
"seconds.")
ap.add_argument("-r", "--reparse", action="store_true",
2017-08-12 06:24:07 +00:00
help="Reparse everything.")
ap.add_argument("-a", "--addons",
2017-08-12 06:24:07 +00:00
help="Specify path to a folder with all addons. This should be " +
"outside the user config folder.")
ap.add_argument("-L", "--list",
2017-08-12 06:24:07 +00:00
help="List available eras and campaigns.")
ap.add_argument("-B", "--batch",
2017-08-12 06:24:07 +00:00
help="Batch process the given list.")
ap.add_argument("-A", "--addons-only", action="store_true",
help="Do only process addons (for debugging).")
ap.add_argument("-v", "--verbose", action="store_true")
ap.add_argument("-W", "--wiki", action="store_true",
help="write wikified units list to stdout")
options = ap.parse_args()
2017-08-12 06:24:07 +00:00
html_output.options = options
helpers.options = options
2012-03-04 23:11:15 +00:00
unit_tree.overview.options = options
2012-03-16 18:16:47 +00:00
wiki_output.options = options
2012-03-16 18:16:47 +00:00
if not options.output and not options.wiki:
sys.stderr.write("Need --output (or --wiki).\n")
2014-01-19 20:33:46 +00:00
ap.print_help()
sys.exit(-1)
2012-03-16 18:16:47 +00:00
if options.output:
options.output = os.path.expanduser(options.output)
if options.timeout:
TIMEOUT = int(options.timeout)
if not options.wesnoth:
options.wesnoth = "wesnoth"
if not options.data_dir:
options.data_dir = shell_out([options.wesnoth, "--path"]).strip().decode("utf8")
2017-08-12 06:24:07 +00:00
print("Using " + options.data_dir + " as data dir.")
if not options.config_dir:
options.config_dir = shell_out([options.wesnoth, "--config-path"]).strip().decode("utf8")
2017-08-12 06:24:07 +00:00
print("Using " + options.config_dir + " as config dir.")
if not options.transdir:
options.transdir = os.getcwd()
2017-08-12 06:24:07 +00:00
2012-03-16 18:16:47 +00:00
if options.wiki:
wiki_output.main()
sys.exit(0)
image_collector = helpers.ImageCollector(options.wesnoth,
2017-08-12 06:24:07 +00:00
options.config_dir,
options.data_dir)
html_output.image_collector = image_collector
if options.language == "all":
languages = []
2017-08-12 06:24:07 +00:00
parser = wmlparser3.Parser(options.wesnoth,
options.config_dir,
options.data_dir)
parser.parse_text("{languages}", "__WMLUNITS__")
for locale in parser.get_all(tag="locale"):
isocode = locale.get_text_val("locale")
name = locale.get_text_val("name")
if isocode == "ang_GB":
continue
languages.append(isocode)
languages.sort()
else:
languages = options.language.split(",")
if not options.list and not options.batch:
sys.stderr.write("Need --list or --batch (or both).\n")
sys.exit(-1)
2017-08-12 06:24:07 +00:00
2012-03-16 18:16:47 +00:00
if options.output:
# Generate output dir.
if not os.path.isdir(options.output):
os.mkdir(options.output)
2017-08-12 06:24:07 +00:00
if options.list:
list_contents()
2017-08-12 06:24:07 +00:00
if options.batch:
batch_process()
2017-08-12 06:24:07 +00:00
2012-03-04 23:11:15 +00:00
unit_tree.overview.main(options.output)
if not options.nocopy:
copy_images()
2017-08-12 06:24:07 +00:00
html_output.write_index(options.output)