def go(i): if i in on_stack: idx = stack.index(i) names = [self.compiled[j]['name'] for j in stack[idx:]] names.append(names[0]) util.err('detected cycle among loot tables: %s' % names) if i in visited: return stack.append(i) on_stack.add(i) visited.add(i) c = self.compiled[i] if c['type'] == 'choose': for v in c['variants']: go(v['id']) elif c['type'] == 'multi': for v in c['parts']: go(v['id']) elif c['type'] == 'object': pass else: assert False, 'unrecognized compiled table type: %r' % (c['type'],) on_stack.remove(i) stack.pop()
def check_group(self, fields, optional=()): """Get all fields in a group. If any field in the group is set, then all required fields must be set or an error will be reported.""" result = [None] * (len(fields) + len(optional)) first_set = None all_set = True i = 0 for f in fields: val = getattr(self, f) if val is not None: first_set = first_set or f result[i] = val else: all_set = False i += 1 for f in optional: val = getattr(self, f) if val is not None: first_set = first_set or f result[i] = val i += 1 if first_set is not None and not all_set: for f in fields: if getattr(self, f) is None: util.err('%s %r: field %r must be set because %r is set' % (self.KIND, self.name, f, first_set)) return result
def error(self, expected, token=None): token = token or self.peek() text = token.text if len(text) > 13: text = text[:10] + '...' util.err('%s:%d:%d: parse error: expected %s, but saw %s "%s"' % (self.filename, token.line, token.col, expected, token.kind, text)) raise ParseError()
def parse_section(self): t_sect = self.peek() self.take_punct('[') ty = self.take_word('section type') name = self.take_word('section name') self.take_punct(']') self.take_eol() ext = ty.endswith('_ext') if ext: ty = ty[:-len('_ext')] mode, _, obj_kind = ty.partition('_') if mode not in ('choose', 'multi') or obj_kind not in ('structure', 'item'): util.err('%s:%d:%d: unknown section type %r' % (self.filename, t_sect.line, t_sect.col, ty)) entries = [] while True: t_field = self.peek() if t_field.kind == 'eol': self.take() continue if t_field.text == '[' or t_field.kind == 'eof': break try: weight, chance = None, None if self.peek().text == '(': weight, chance = self.parse_weight_or_chance() min_count, max_count = 1, 1 if INT_RE.match(self.peek().text): min_count, max_count = self.parse_counts() table_ref = self.peek().text == '*' if table_ref: self.take_punct('*') if min_count != 1 or max_count != 1: util.error( '%s:%d:%d: cannot specify count for table reference' % (self.filename, t_field.line, t_field.col)) ref_name = self.take_word() self.take_eol() entries.append( Entry('table' if table_ref else 'object', ref_name, min_count, max_count, weight, chance, t_field.line, t_field.col)) except ParseError: self.skip_to_eol() return Section(mode, obj_kind, ext, name, entries, t_sect.line, t_sect.col)
def require(self, field, default=None, reason=None): val = getattr(self, field) if val is None: if reason: util.err('%s %r: field %r must be set because %r is set' % (self.KIND, self.name, field, reason)) else: util.err('%s %r: field %r must be set' % (self.KIND, self.name, field)) return default else: return val
def parse_section(self): t_sect = self.peek() self.take_punct('[') ty = self.take_word('section type') name = self.take_word('section name') self.take_punct(']') self.take_eol() ext = ty.endswith('_ext') if ext: ty = ty[:-len('_ext')] mode, _, obj_kind = ty.partition('_') if mode not in ('choose', 'multi') or obj_kind not in ('structure', 'item'): util.err('%s:%d:%d: unknown section type %r' % (self.filename, t_sect.line, t_sect.col, ty)) entries = [] while True: t_field = self.peek() if t_field.kind == 'eol': self.take() continue if t_field.text == '[' or t_field.kind == 'eof': break try: weight, chance = None, None if self.peek().text == '(': weight, chance = self.parse_weight_or_chance() min_count, max_count = 1, 1 if INT_RE.match(self.peek().text): min_count, max_count = self.parse_counts() table_ref = self.peek().text == '*' if table_ref: self.take_punct('*') if min_count != 1 or max_count != 1: util.error('%s:%d:%d: cannot specify count for table reference' % (self.filename, t_field.line, t_field.col)) ref_name = self.take_word() self.take_eol() entries.append(Entry('table' if table_ref else 'object', ref_name, min_count, max_count, weight, chance, t_field.line, t_field.col)) except ParseError: self.skip_to_eol() return Section(mode, obj_kind, ext, name, entries, t_sect.line, t_sect.col)
def parse_section(self): t_sect = self.peek() self.take_punct('[') ty = self.take_word('section type') name = self.take_word('section name') self.take_punct(']') self.take_eol() if ty in FIELD_MAP: field_map = FIELD_MAP[ty] else: util.err('%s:%d:%d: unknown section type %r' % (self.filename, t_sect.line, t_sect.col, ty)) field_map = {} ty = None parts = [] while True: t_field = self.peek() if t_field.kind == 'eol': self.take() continue if t_field.text == '[' or t_field.kind == 'eof': break try: key = self.take_word() self.take_punct(':') if key not in field_map: if ty is not None: util.err('%s:%d:%d: unknown field %r for section type %r' % (self.filename, t_field.line, t_field.col, key, ty)) raise ParseError() if self.peek().kind == 'backticked': t_val = self.take() val = Backticked(t_val.text, t_val.line, t_val.col) else: t_val = self.peek() val = Value(field_map[key].parse(self), t_val.line, t_val.col) self.take_eol() parts.append(Field(key, val, t_field.line, t_field.col)) except ParseError as e: self.skip_to_eol() return Section(ty, name, parts, t_sect.line, t_sect.col)
def require_one(self, field1, field2): """Check that exactly one of the two fields is set. Returns True if the first is set, otherwise False. Reports an error and defaults to True if both or neither are set.""" val1 = getattr(self, field1) val2 = getattr(self, field2) if val1 is not None and val2 is not None: util.err('%s %r: field %r and field %r must not both be set' % (self.KIND, self.name, field1, field2)) return True elif val1 is not None: return True elif val2 is not None: return False else: util.err('%s %r: either field %r or field %r must be set' % (self.KIND, self.name, field1, field2)) return True
def parse(self): parts = [] while True: t = self.peek() if t.kind == 'eol': self.take() elif t.kind == 'eof': break elif t.kind == 'punct' and t.text == '[': parts.append(self.parse_section()) elif t.kind == 'py_begin': util.err('%s:%d:%d: python blocks are not supported in loot tables' % (self.filename, t.line, t.col)) while self.peek().kind != 'py_end': self.take() self.take() else: self.error('beginning of section', t) return parts
def parse(self): parts = [] while True: t = self.peek() if t.kind == 'eol': self.take() elif t.kind == 'eof': break elif t.kind == 'punct' and t.text == '[': parts.append(self.parse_section()) elif t.kind == 'py_begin': util.err( '%s:%d:%d: python blocks are not supported in loot tables' % (self.filename, t.line, t.col)) while self.peek().kind != 'py_end': self.take() self.take() else: self.error('beginning of section', t) return parts
def register_mod(name, assets, override_dir, deps): # NB: `override_dir` is a directory containing overrides for *other*, # previously loaded mods. The `overrides` field of `ModInfo` is a list of # override directories applied to *this* mod by others. if name in MOD_MAP: util.err('mod %r is loaded multiple times' % name) for dep in deps: if dep not in MOD_MAP: util.err('mod %r depends on %r, which is not (yet) loaded' % (name, dep)) MOD_MAP[name] = ModInfo(assets, [], deps) if override_dir is not None and os.path.exists(override_dir): for override_mod in os.listdir(override_dir): if override_mod not in MOD_MAP: util.err('mod %r applies overrides to %r, which is not (yet) loaded' % (name, override_mod)) continue MOD_MAP[override_mod].overrides.append(os.path.join(override_dir, override_mod))
def build_map(tables, kind): table_map = {} for t in (t for t in tables if t.object_kind() == kind and not t.ext): if t.name in table_map: util.err('multiple definitions of table %r' % t.name) continue table_map[t.name] = [t.table] for t in (t for t in tables if t.object_kind() == kind and t.ext): if t.name not in table_map: util.err('found extension of nonexistent table %r' % t.name) continue ts = table_map[t.name] if type(t.table) is not type(ts[0]): util.err('extension of table %r does not match original type (%s != %s)' % (t.name, type(ts[0]).__name__, type(t.table))) continue ts.append(t.table) return table_map
def resolve_table_ref(self, src_name, ref): if ref.name not in self.id_map: util.err('table %r refers to nonexistent table %r' % (src_name, ref.name)) return None return self.id_map[ref.name]
def resolve_object_ids(self, id_map, name): for v in self.variants: if isinstance(v.ref, ObjectRef): v.ref.id = id_map.get(v.ref.name) if v.ref.id is None: util.err('loot table %r: no such %s: %r' % (name, self.OBJ_KIND, v.ref.name))
def resolve_object_ids(self, id_map, name): for p in self.parts: if isinstance(p.ref, ObjectRef): p.ref.id = id_map.get(p.ref.name) if p.ref.id is None: util.err('loot table %r: no such %s: %r' % (name, self.OBJ_KIND, p.ref.name))
def err(self, msg): util.err('%s:%d:%d: %s' % (self.filename, self.i + 1, self.j, msg))
def require_unset(self, field, reason): if getattr(self, field) is not None: util.err('%s %r: field %r must not be set because %r is set' % (self.KIND, self.name, field, reason))