quakeforge/tools/misc/cvar2.py

336 lines
12 KiB
Python

import re
import sys
from pprint import pprint
from io import StringIO
from textwrap import TextWrapper
import os
STRING = r'\s*"((\\.|[^"\\])*)"\s*'
QSTRING = r'\s*("(\\.|[^"\\])*")\s*'
ID = r'([a-zA-Z_][a-zA-Z_0-9]*)'
STORE = r'(VISIBLE\s+|static\s+|extern\s+|)'
FIELD = r'(string|value|int_val|vec)\b'
FLAGS = r'\s*(' + ID + r'\s*(\|\s*' + ID + r')*)\s*'
TYPE = r'(cvar_t|struct cvar_s)\s*\*'
STUFF = r'\s*(.*)\s*'
cvar_decl_re = re.compile(r'^' + STORE + TYPE + ID + r';.*')
cvar_get_re = re.compile(r'.*\bCvar_Get\s*\(\s*"')
cvar_get_join_prev_re = re.compile(r'^\s*Cvar_Get\s*\(\s*".*')
cvar_get_assign_re = re.compile(r'\s*' + ID + r'\s*=\s*$')
cvar_get_incomplete_re = re.compile(r'.*Cvar_Get\s*\(\s*"[^;]*$')
cvar_get_complete_re = re.compile(r'.*Cvar_Get\s*\(\s*".*\);.*$')
cvar_create_re = re.compile(r'\s*(\w+)\s*=\s*Cvar_Get\s*\('
+ STRING
+ ',(' + STRING + '|\s*(.*)\s*),' + FLAGS + r',\s*([^,]+),\s*' + STUFF + r'\);.*')
cvar_use_re = re.compile(ID + r'\s*\->\s*' + FIELD)
cvar_listener_re = re.compile(ID + r'\s*\(\s*cvar_t\s*\*' + ID + '\s*\)\s*')
string_or_id_re = re.compile(f'({ID}|{QSTRING})')
cvar_setrom_re = re.compile(r'\s*Cvar_SetFlags\s*\(\s*' + ID + '.*CVAR_ROM\);.*')
id_re = re.compile(f'\\b{ID}\\b')
#cvar_decl_re = re.compile(r'^cvar_t\s+(\w+)\s*=\s*\{\s*("[^"]*")\s*,\s*("[^"]*")(\s*,\s*(\w+)(\s*,\s*(\w+))?)?\s*\}\s*;(\s*//\s*(.*))?\n')
cvar_set_re = re.compile(r'^(\s+)(Cvar_Set|Cvar_SetValue)\s*\(' + ID + ',\s*(.*)\);(.*)')
wrapper = TextWrapper(width = 69, drop_whitespace = False,
break_long_words = False)
class cvar:
def __init__(self, c_name, name, default, flags, callback, description):
self.c_name = c_name
self.name = name
self.default = default
self.flags = flags
if callback in ['NULL', '0']:
callback = 0
self.callback = callback
if description in ['NULL', '0', '']:
description = 'FIXME no description'
self.desc = description
if self.desc==None:
self.desc = 'None'
self.data = 0
self.files = []
self.type = None
self.c_type = None
self.used_field = None
self.uses = []
def guess_type(self):
if self.used_field == "value":
self.type = "&cexpr_float"
self.c_type = "float "
elif self.used_field == "int_val":
self.type = "&cexpr_int"
self.c_type = "int "
elif self.used_field == "vec":
self.type = "&cexpr_vector"
self.c_type = "vec4f_t "
elif self.used_field == "string":
self.type = 0
self.c_type = "char *"
else:
self.type = "0/* not used */"
self.c_type = "char *"
def add_file(self, file, line):
self.files.append((file, line))
def add_use(self, field, file, line):
if self.used_field and self.used_field != field:
print(f"{file}:{line}: cvar {self.name} used inconsistently")
self.used_field = field
self.uses.append((field, file, line))
def __repr__(self):
return f"cvar(({self.c_name}, {self.name}, {self.default}, {self.flags}, {self.callback}, {self.desc})"
def __str__(self):
return self.__repr__()
def struct(self):
A = [
f'static cvar_t {self.c_name}_cvar = {{\n',
f'\t.name = "{self.name}",\n',
f'\t.description =\n',
]
B = [
f'\t.default_value = "{self.default}",\n',
f'\t.flags = {self.flags},\n',
f'\t.value = {{ .type = {self.type}, .value = &{self.c_name} }},\n',
f'}};\n',
]
desc = []
dstrings = [m[0].strip() for m in string_or_id_re.findall(self.desc)]
def wrap(string, comma = ""):
if string[0] == '"' and string != '""':
string = string[1:-1]
dlines = wrapper.wrap(string)
desc.extend([f'\t\t"{l}"\n' for l in dlines[:-1]])
desc.append(f'\t\t"{dlines[-1]}"{comma}\n')
else:
desc.append(f'\t\t{string}{comma}\n')
for dstring in dstrings[:-1]:
wrap (dstring)
wrap(dstrings[-1], ",")
return A + desc + B
def var(self, storage):
if storage:
storage += " "
return [f'{storage}{self.c_type}{self.c_name};\n']
def set_rom(self):
if self.flags == 'CVAR_NONE':
self.flags = 'CVAR_ROM'
else:
self.flags = '|'.join([self.flags, 'CVAR_ROM'])
def register(self):
return [f'\tCvar_Register (&{self.c_name}_cvar, {self.callback}, {self.data});\n']
cvar_decls = {}
cvar_dict = {}
cvar_sets = []
cvar_rom = set()
cvar_listeners = {}
cvar_listeners_done = set()
cvar_listeners_multi = set()
files = {}
modified = set()
cvar_struct = [
"\tconst char *name;\n",
"\tconst char *description;\n",
"\tconst char *default_value;\n",
"\tunsigned flags;\n",
"\texprval_t value;\n",
"\tint (*validator) (const struct cvar_s *var);\n"
"\tstruct cvar_listener_set_s *listeners;\n"
"\tstruct cvar_s *next;\n"
]
cvar_reg = "void Cvar_Register (cvar_t *var, cvar_listener_t listener, void *data);\n"
cvar_cexpr = '#include "QF/cexpr.h"\n'
cvar_set = 'void Cvar_Set (const char *var, const char *value);\n'
cvar_setvar = 'void Cvar_SetVar (cvar_t *var, const char *value);\n'
cvar_info = 'struct cvar_s;\nvoid Cvar_Info (void *data, const struct cvar_s *cvar);\n'
nq_cl_cvar_info = 'nq/include/client.h'
nq_sv_cvar_info = 'nq/include/server.h'
qw_cl_cvar_info = 'qw/include/client.h'
qw_sv_cvar_info = 'qw/include/server.h'
cvar_file = "include/QF/cvar.h"
line_substitutions = [
(cvar_file, (40, 60), cvar_struct),
(cvar_file, (96, 100), cvar_reg),
(cvar_file, (35, 35), cvar_cexpr),
(cvar_file, (109, 110), cvar_set),
(cvar_file, (110, 111), cvar_setvar),
(nq_cl_cvar_info, (294,295), cvar_info),
(nq_sv_cvar_info, (300,301), cvar_info),
(qw_cl_cvar_info, (296,297), cvar_info),
(qw_sv_cvar_info, (634,635), cvar_info),
]
def get_cvar_defs(fname):
fi = open(fname,'rt')
f = files[fname] = fi.readlines()
i = 0
while i < len(f):
cr = cvar_setrom_re.match(f[i])
if cr:
cvar_rom.add(cr[1])
del(f[i])
continue
if cvar_get_join_prev_re.match(f[i]):
if cvar_get_assign_re.match(f[i - 1]):
f[i - 1] = f"{f[i - 1].rstrip()} {f[i].lstrip()}"
del(f[i])
i -= 1
if cvar_get_incomplete_re.match(f[i]):
while not cvar_get_complete_re.match(f[i]):
f[i + 1] = f[i + 1].lstrip()
f[i] = f[i].rstrip()
if f[i][-1] == '"' and f[i + 1][0] == '"':
#string concatentation
f[i] = f[i][:-1] + f[i + 1][1:]
else:
f[i] = f"{f[i]} {f[i + 1]}"
del(f[i + 1])
cd = cvar_decl_re.match(f[i])
if cd:
if cd[3] not in cvar_decls:
cvar_decls[cd[3]] = []
cvar_decls[cd[3]].append((fname, i))
cl = cvar_listener_re.match(f[i])
if cl:
if cl[1] not in cvar_listeners:
cvar_listeners[cl[1]] = []
cvar_listeners[cl[1]].append((fname, i, cl[2]))
cc = cvar_create_re.match(f[i])
if cc:
if cc.group(7):
default = 7
else:
default = 5
cc = cc.group(1, 2, default, 8, 12, 13)
if cc[0] not in cvar_dict:
cvar_dict[cc[0]] = cvar(*cc)
cvar_dict[cc[0]].add_file(fname, i)
cs = cvar_set_re.match(f[i])
if cs:
cvar_sets.append((fname, i))
i += 1
fi.close()
for f in sys.argv[1:]:
get_cvar_defs(f)
#for cv in cvar_decls.keys():
# if cv not in cvar_dict:
# print(f"cvar {cv} declared but never created")
for file in files.items():
fname,f = file
for i in range(len(f)):
for use in cvar_use_re.finditer(f[i]):
if use[1] in cvar_dict and cvar_dict[use[1]]:
cvar_dict[use[1]].add_use(use[2], fname, i)
keys = list(cvar_dict.keys())
for cv in keys:
var = cvar_dict[cv]
if cv not in cvar_decls or var.callback not in cvar_listeners:
continue
if var.callback in cvar_listeners:
if var.callback in cvar_listeners_done:
if not var.callback in cvar_listeners_multi:
print(f"WARNING: {var.callback} has multiple uses")
cvar_listeners_multi.add (var.callback)
else:
cvar_listeners_done.add (var.callback);
cvar_listeners_done.clear()
for cv in keys:
var = cvar_dict[cv]
var.guess_type()
if var.c_name in cvar_rom:
print(var.c_name, 'rom')
var.set_rom()
if cv in cvar_decls:
if var.callback in cvar_listeners_multi:
var.data = f'&{var.c_name}'
for d in cvar_decls[cv]:
decl = cvar_decl_re.match(files[d[0]][d[1]])
storage = decl[1].strip()
if storage == "extern":
subst = var.var(storage)
else:
subst = var.var(storage)+var.struct()
line_substitutions.append((d[0], (d[1], d[1] + 1), subst))
for c in var.files:
line_substitutions.append((c[0], (c[1], c[1] + 1), var.register()))
for u in var.uses:
# use substitutions are done immediately because they never
# change the number of lines
field, fname, line = u
file = files[fname]
places = []
for use in cvar_use_re.finditer(file[line]):
cv, field = use.group(1, 2)
if cv == var.c_name:
places.append((use.start(), use.end()))
places.reverse()
for p in places:
file[line] = file[line][:p[0]] + var.c_name + file[line][p[1]:]
modified.add(fname)
else:
#for f in cvar_dict[cv].files:
# print(f"{f[0]}:{f[1]}: {cv} created but not kept")
pass
for cv in keys:
var = cvar_dict[cv]
if cv not in cvar_decls or var.callback not in cvar_listeners:
continue
if var.callback in cvar_listeners_done:
continue
for listener in cvar_listeners[var.callback]:
fname, line, c_name = listener
file = files[fname]
file[line] = f"{var.callback} (void *data," " const cvar_t *cvar)\n"
modified.add(fname)
cvar_listeners_done.add (var.callback);
multi = var.callback in cvar_listeners_multi
line += 1
while line < len(file) and file[line] != '}\n':
line += 1
places = []
for use in cvar_use_re.finditer(file[line]):
cv, field = use.group(1, 2)
if cv == c_name:
subst = f'*({var.c_type}*)data' if multi else var.c_name
places.append((use.start(), use.end(), subst))
places.reverse()
for p in places:
file[line] = file[line][:p[0]] + p[2] + file[line][p[1]:]
modified.add(fname)
places = []
for v in id_re.finditer(file[line]):
if v[1] == c_name:
places.append((v.start(), v.end(), "cvar"))
for p in places:
file[line] = file[line][:p[0]] + p[2] + file[line][p[1]:]
modified.add(fname)
for s in cvar_sets:
cs = cvar_set_re.match(files[s[0]][s[1]])
subst = None
val = cs[4]
if cs[2] == "Cvar_SetValue" and cs[3] in cvar_dict:
if val[0] == '(':
#strip off a cast
val = val[val.index(')') + 1:].strip()
subst = f'{cs[1]}{cs[3]} = {val};{cs[5]}\n'
elif cs[2] == "Cvar_Set" and cs[3] in cvar_dict:
subst = f'{cs[1]}{cs[2]} ("{cvar_dict[cs[3]].name}", {val});{cs[5]}\n'
elif cs[2] == "Cvar_Set":
subst = f'{cs[1]}Cvar_SetVar ({cs[3]}, {val});{cs[5]}\n'
if subst:
line_substitutions.append((s[0], (s[1], s[1] + 1), subst))
line_substitutions.sort(reverse=True, key=lambda s: s[1][1])
for substitution in line_substitutions:
file, lines, subst = substitution
modified.add(file)
files[file][lines[0]:lines[1]] = subst
for f in files.keys():
if f in modified:
file = open(f, "wt")
file.writelines(files[f])
file.close ()