def preprocess_binop(self, expr): """ Several nml operators are not directly support by nfo so we have to work around that by implementing those operators in terms of others. @return: A pre-processed version of the expression. @rtype: L{Expression} """ assert isinstance(expr, expression.BinOp) if expr.op == nmlop.CMP_LT: # return value is 0, 1 or 2, we want to map 0 to 1 and the others to 0 expr = nmlop.VACT2_CMP(expr.expr1, expr.expr2) # reduce the problem to 0/1 expr = nmlop.MIN(expr, 1) # and invert the result expr = nmlop.XOR(expr, 1) elif expr.op == nmlop.CMP_GT: # return value is 0, 1 or 2, we want to map 2 to 1 and the others to 0 expr = nmlop.VACT2_CMP(expr.expr1, expr.expr2) # subtract one expr = nmlop.SUB(expr, 1) # map -1 and 0 to 0 expr = nmlop.MAX(expr, 0) elif expr.op == nmlop.CMP_LE: # return value is 0, 1 or 2, we want to map 2 to 0 and the others to 1 expr = nmlop.VACT2_CMP(expr.expr1, expr.expr2) # swap 0 and 2 expr = nmlop.XOR(expr, 2) # map 1/2 to 1 expr = nmlop.MIN(expr, 1) elif expr.op == nmlop.CMP_GE: # return value is 0, 1 or 2, we want to map 1/2 to 1 expr = nmlop.VACT2_CMP(expr.expr1, expr.expr2) expr = nmlop.MIN(expr, 1) elif expr.op == nmlop.CMP_EQ: # return value is 0, 1 or 2, we want to map 1 to 1, other to 0 expr = nmlop.VACT2_CMP(expr.expr1, expr.expr2) expr = nmlop.AND(expr, 1) elif expr.op == nmlop.CMP_NEQ: # same as CMP_EQ but invert the result expr = nmlop.VACT2_CMP(expr.expr1, expr.expr2) expr = nmlop.AND(expr, 1) expr = nmlop.XOR(expr, 1) elif expr.op == nmlop.HASBIT: # hasbit(x, n) ==> (x >> n) & 1 expr = nmlop.SHIFTU_RIGHT(expr.expr1, expr.expr2) expr = nmlop.AND(expr, 1) elif expr.op == nmlop.NOTHASBIT: # !hasbit(x, n) ==> ((x >> n) & 1) ^ 1 expr = nmlop.SHIFTU_RIGHT(expr.expr1, expr.expr2) expr = nmlop.AND(expr, 1) expr = nmlop.XOR(expr, 1) return expr.reduce()
def tile_offset(name, args, pos, info, min, max): if len(args) != 2: raise generic.ScriptError("'{}'() requires 2 arguments, encountered {:d}".format(name, len(args)), pos) for arg in args: if isinstance(arg, expression.ConstantNumeric): generic.check_range(arg.value, min, max, "Argument of '{}'".format(name), arg.pos) x = nmlop.AND(args[0], 0xF) y = nmlop.AND(args[1], 0xF) # Shift y left by four y = nmlop.SHIFT_LEFT(y, 4) param = nmlop.ADD(x, y) #Make sure to reduce the result return ( param.reduce(), [] )
def nearest_house_matching_criterion(name, args, pos, info): # nearest_house_matching_criterion(radius, criterion) # parameter is radius | (criterion << 6) if len(args) != 2: raise generic.ScriptError("{}() requires 2 arguments, encountered {:d}".format(name, len(args)), pos) if isinstance(args[0], expression.ConstantNumeric): generic.check_range(args[0].value, 1, 63, "{}()-parameter 1 'radius'".format(name), pos) if isinstance(args[1], expression.ConstantNumeric) and args[1].value not in (0, 1, 2): raise generic.ScriptError("Invalid value for {}()-parameter 2 'criterion'".format(name), pos) radius = nmlop.AND(args[0], 0x3F, pos) criterion = nmlop.AND(args[1], 0x03, pos) criterion = nmlop.MUL(criterion, 0x40) retval = nmlop.OR(criterion, radius).reduce() return (retval, [])
def builtin_slope_to_sprite_offset(name, args, pos): """ builtin function slope_to_sprite_offset(slope) @return sprite offset to use """ if len(args) != 1: raise generic.ScriptError(name + "() must have 1 parameter", pos) if isinstance(args[0], ConstantNumeric): generic.check_range(args[0].value, 0, 15, "Argument of '{}'".format(name), args[0].pos) # step 1: ((slope >= 0) & (slope <= 14)) * slope # This handles all non-steep slopes expr = nmlop.AND(nmlop.CMP_LE(args[0], 14, pos), nmlop.CMP_GE(args[0], 0, pos)) expr = nmlop.MUL(expr, args[0]) # Now handle the steep slopes separately # So add (slope == SLOPE_XX) * offset_of_SLOPE_XX for each steep slope steep_slopes = [(23, 16), (27, 17), (29, 15), (30, 18)] for slope, offset in steep_slopes: to_add = nmlop.MUL(nmlop.CMP_EQ(args[0], slope, pos), offset) expr = nmlop.ADD(expr, to_add) return expr
def signed_byte_parameter(name, args, pos, info): # Convert to a signed byte by AND-ing with 0xFF if len(args) != 1: raise generic.ScriptError("{}() requires one argument, encountered {:d}".format(name, len(args)), pos) if isinstance(args[0], expression.ConstantNumeric): generic.check_range(args[0].value, -128, 127, "parameter of {}()".format(name), pos) ret = nmlop.AND(args[0], 0xFF, pos).reduce() return (ret, [])
def industry_layout_count(name, args, pos, info): if len(args) < 2 or len(args) > 3: raise generic.ScriptError("'{}'() requires between 2 and 3 argument(s), encountered {:d}".format(name, len(args)), pos) grfid = expression.ConstantNumeric(0xFFFFFFFF) if len(args) == 2 else args[2] extra_params = [] extra_params.append( (0x100, grfid) ) extra_params.append( (0x101, nmlop.AND(args[1], 0xFF).reduce()) ) return (args[0], extra_params)
def builtin_relative_coord(name, args, pos): """ relative_coord(x, y) builtin function. @return Coordinates in 0xYYXX format. """ if len(args) != 2: raise generic.ScriptError(name + "() must have x and y coordinates as parameters", pos) if isinstance(args[0], ConstantNumeric): generic.check_range(args[0].value, 0, 255, "Argument of '{}'".format(name), args[0].pos) if isinstance(args[1], ConstantNumeric): generic.check_range(args[1].value, 0, 255, "Argument of '{}'".format(name), args[1].pos) x_coord = nmlop.AND(args[0], 0xFF) y_coord = nmlop.AND(args[1], 0xFF) # Shift Y to its position. y_coord = nmlop.SHIFT_LEFT(y_coord, 8) return nmlop.OR(x_coord, y_coord, pos)
def builtin_num_corners_raised(name, args, pos): """ num_corners_raised(slope) builtin function. slope is a 5-bit value @return Number of raised corners in a slope (4 for steep slopes) """ if len(args) != 1: raise generic.ScriptError(name + "() must have 1 parameter", pos) slope = args[0] # The returned value is ((slope x 0x8421) & 0x11111) % 0xF # Explanation in steps: (numbers in binary) # - Masking constrains the slope to 5 bits, just to be sure (a|bcde) # - Multiplication creates 4 copies of those bits (abcd|eabc|deab|cdea|bcde) # - And-masking leaves only the lowest bit in each nibble (000d|000c|000b|000a|000e) # - The modulus operation adds one to the output for each set bit # - We now have the count of bits in the slope, which is wat we want. yay! slope = nmlop.AND(slope, 0x1F, pos) slope = nmlop.MUL(slope, 0x8421) slope = nmlop.AND(slope, 0x11111) return nmlop.MOD(slope, 0xF)
def builtin_vehicle_curv_info(name, args, pos): """ vehicle_curv_info(prev_cur, cur_next) builtin function @return Value to use with vehicle var curv_info """ if len(args) != 2: raise generic.ScriptError(name + "() must have 2 parameters", pos) for arg in args: if isinstance(arg, ConstantNumeric): generic.check_range(arg.value, -2, 2, "Argument of '{}'".format(name), arg.pos) args = [nmlop.AND(arg, 0xF, pos) for arg in args] cur_next = nmlop.SHIFT_LEFT(args[1], 8) return nmlop.OR(args[0], cur_next)
def builtin_getbits(name, args, pos): """ getbits(value, first, amount) builtin function. @return Extract C{amount} bits starting at C{first} from C{value}, that is (C{value} >> C{first}) & (1 << C{amount} - 1) """ if len(args) != 3: raise generic.ScriptError(name + "() must have exactly three parameters", pos) # getbits(value, first, amount) = (value >> first) & ((0xFFFFFFFF << amount) ^ 0xFFFFFFFF) part1 = nmlop.SHIFTU_RIGHT(args[0], args[1], pos) part2 = nmlop.SHIFT_LEFT(0xFFFFFFFF, args[2], pos) part3 = nmlop.XOR(part2, 0xFFFFFFFF, pos) return nmlop.AND(part1, part3, pos)
def cargo_accepted_nearby(name, args, pos, info): # cargo_accepted_nearby(cargo[, xoffset, yoffset]) if len(args) not in (1, 3): raise generic.ScriptError("{}() requires 1 or 3 arguments, encountered {:d}".format(name, len(args)), pos) if len(args) > 1: offsets = args[1:3] for i, offs in enumerate(offsets[:]): if isinstance(offs, expression.ConstantNumeric): generic.check_range(offs.value, -128, 127, "{}-parameter {:d} '{}offset'".format(name, i + 1, "x" if i == 0 else "y"), pos) offsets[i] = nmlop.AND(offs, 0xFF, pos).reduce() # Register 0x100 should be set to xoffset | (yoffset << 8) reg100 = nmlop.OR(nmlop.MUL(offsets[1], 256, pos), offsets[0]).reduce() else: reg100 = expression.ConstantNumeric(0, pos) return (args[0], [(0x100, reg100)])
def transform_bin_op(assignment): op = assignment.value.op expr1 = assignment.value.expr1 expr2 = assignment.value.expr2 extra_actions = [] if op == nmlop.CMP_GE: expr1, expr2 = expr2, expr1 op = nmlop.CMP_LE if op == nmlop.CMP_LE: extra_actions.extend(parse_actionD(ParameterAssignment(assignment.param, nmlop.SUB(expr1, expr2)))) op = nmlop.CMP_LT expr1 = assignment.param expr2 = expression.ConstantNumeric(1) if op == nmlop.CMP_GT: expr1, expr2 = expr2, expr1 op = nmlop.CMP_LT if op == nmlop.CMP_LT: extra_actions.extend(parse_actionD(ParameterAssignment(assignment.param, nmlop.SUB(expr1, expr2)))) op = nmlop.SHIFTU_LEFT # shift left by negative number = shift right expr1 = assignment.param expr2 = expression.ConstantNumeric(-31) elif op == nmlop.CMP_NEQ: extra_actions.extend(parse_actionD(ParameterAssignment(assignment.param, nmlop.SUB(expr1, expr2)))) op = nmlop.DIV # We rely here on the (ondocumented) behavior of both OpenTTD and TTDPatch # that expr/0==expr. What we do is compute A/A, which will result in 1 if # A != 0 and in 0 if A == 0 expr1 = assignment.param expr2 = assignment.param elif op == nmlop.CMP_EQ: # We compute A==B by doing not(A - B) which will result in a value != 0 # if A is equal to B extra_actions.extend(parse_actionD(ParameterAssignment(assignment.param, nmlop.SUB(expr1, expr2)))) # Clamp the value to 0/1, see above for details extra_actions.extend( parse_actionD(ParameterAssignment(assignment.param, nmlop.DIV(assignment.param, assignment.param))) ) op = nmlop.SUB expr1 = expression.ConstantNumeric(1) expr2 = assignment.param if op == nmlop.SHIFT_RIGHT or op == nmlop.SHIFTU_RIGHT: if isinstance(expr2, expression.ConstantNumeric): expr2.value *= -1 else: expr2 = nmlop.SUB(0, expr2) op = nmlop.SHIFT_LEFT if op == nmlop.SHIFT_RIGHT else nmlop.SHIFTU_LEFT elif op == nmlop.XOR: # a ^ b ==> (a | b) - (a & b) expr1 = parse_subexpression(expr1, extra_actions) expr2 = parse_subexpression(expr2, extra_actions) tmp_param1, tmp_action_list1 = get_tmp_parameter(nmlop.OR(expr1, expr2)) tmp_param2, tmp_action_list2 = get_tmp_parameter(nmlop.AND(expr1, expr2)) extra_actions.extend(tmp_action_list1) extra_actions.extend(tmp_action_list2) expr1 = expression.Parameter(expression.ConstantNumeric(tmp_param1)) expr2 = expression.Parameter(expression.ConstantNumeric(tmp_param2)) op = nmlop.SUB return op, expr1, expr2, extra_actions
'num_vehs_in_vehid_chain' : {'var': 0x41, 'start': 16, 'size': 8}, # One-based, already sane 'cargo_classes_in_consist' : {'var': 0x42, 'start': 0, 'size': 8}, 'most_common_cargo_type' : {'var': 0x42, 'start': 8, 'size': 8}, 'most_common_cargo_subtype' : {'var': 0x42, 'start': 16, 'size': 8}, 'bitmask_consist_info' : {'var': 0x42, 'start': 24, 'size': 8}, 'company_num' : {'var': 0x43, 'start': 0, 'size': 8}, 'company_type' : {'var': 0x43, 'start': 16, 'size': 2}, 'company_colour1' : {'var': 0x43, 'start': 24, 'size': 4}, 'company_colour2' : {'var': 0x43, 'start': 28, 'size': 4}, 'aircraft_height' : {'var': 0x44, 'start': 8, 'size': 8}, 'airport_type' : {'var': 0x44, 'start': 0, 'size': 8}, 'curv_info_prev_cur' : {'var': 0x45, 'start': 0, 'size': 4, 'value_function': value_sign_extend}, 'curv_info_cur_next' : {'var': 0x45, 'start': 8, 'size': 4, 'value_function': value_sign_extend}, 'curv_info_prev_next' : {'var': 0x45, 'start': 16, 'size': 4, 'value_function': value_sign_extend}, 'curv_info' : {'var': 0x45, 'start': 0, 'size': 12, 'value_function': lambda var, info: nmlop.AND(var, 0x0F0F).reduce()}, 'motion_counter' : {'var': 0x46, 'start': 8, 'size': 24}, 'cargo_type_in_veh' : {'var': 0x47, 'start': 0, 'size': 8}, 'cargo_unit_weight' : {'var': 0x47, 'start': 8, 'size': 8}, 'cargo_classes' : {'var': 0x47, 'start': 16, 'size': 16}, 'vehicle_is_available' : {'var': 0x48, 'start': 0, 'size': 1}, 'vehicle_is_testing' : {'var': 0x48, 'start': 1, 'size': 1}, 'vehicle_is_offered' : {'var': 0x48, 'start': 2, 'size': 1}, 'build_year' : {'var': 0x49, 'start': 0, 'size': 32}, 'vehicle_is_potentially_powered' : {'var': 0x4A, 'start': 8, 'size': 1}, 'tile_has_catenary' : {'var': 0x4A, 'start': 9, 'size': 1}, 'date_of_last_service' : {'var': 0x4B, 'start': 0, 'size': 32}, 'position_in_articulated_veh' : {'var': 0x4D, 'start': 0, 'size': 8}, 'position_in_articulated_veh_from_end' : {'var': 0x4D, 'start': 8, 'size': 8}, 'waiting_triggers' : {'var': 0x5F, 'start': 0, 'size': 8}, 'random_bits' : {'var': 0x5F, 'start': 8, 'size': 8},
def reduce(self, id_dicts=None, unknown_id_fatal=True): # Reducing a BinOp expression is done in several phases: # - Reduce both subexpressions. # - If both subexpressions are constant, compute the result and return it. # - If the operator allows it and the second expression is more complex than # the first one swap them. # - If the operation is a no-op, delete it. # - Variables (as used in action2var) can have some computations attached to # them, do that if possible. # - Try to merge multiple additions/subtractions with constant numbers # - Reduce both subexpressions. expr1 = self.expr1.reduce(id_dicts) expr2 = self.expr2.reduce(id_dicts) # Make sure the combination of operands / operator is valid if self.op.validate_func is not None: self.op.validate_func(expr1, expr2, self.pos) # - If both subexpressions are constant, compute the result and return it. if ( isinstance(expr1, ConstantNumeric) and isinstance(expr2, ConstantNumeric) and self.op.compiletime_func is not None ): return ConstantNumeric(self.op.compiletime_func(expr1.value, expr2.value), self.pos) if isinstance(expr1, StringLiteral) and isinstance(expr2, StringLiteral): assert self.op == nmlop.ADD return StringLiteral(expr1.value + expr2.value, expr1.pos) if ( isinstance(expr1, (ConstantNumeric, ConstantFloat)) and isinstance(expr2, (ConstantNumeric, ConstantFloat)) and self.op.compiletime_func is not None ): return ConstantFloat(self.op.compiletime_func(expr1.value, expr2.value), self.pos) # - If the operator allows it and the second expression is more complex than # the first one swap them. op = self.op if op.commutative or op in (nmlop.CMP_LT, nmlop.CMP_GT): prio1 = self.get_priority(expr1) prio2 = self.get_priority(expr2) if prio2 < prio1: expr1, expr2 = expr2, expr1 if op == nmlop.CMP_LT: op = nmlop.CMP_GT elif op == nmlop.CMP_GT: op = nmlop.CMP_LT # - If the operation is a no-op, delete it. if op == nmlop.AND and isinstance(expr2, ConstantNumeric) and (expr2.value == -1 or expr2.value == 0xFFFFFFFF): return expr1 if op in (nmlop.DIV, nmlop.DIVU, nmlop.MUL) and isinstance(expr2, ConstantNumeric) and expr2.value == 1: return expr1 if op in (nmlop.ADD, nmlop.SUB) and isinstance(expr2, ConstantNumeric) and expr2.value == 0: return expr1 # - Variables (as used in action2var) can have some computations attached to # them, do that if possible. if isinstance(expr1, Variable) and expr2.supported_by_actionD(False): # An action2 Variable has some special fields (mask, add, div and mod) that can be used # to perform some operations on the value. These operations are faster than a normal # advanced varaction2 operator so we try to use them whenever we can. if op == nmlop.AND and expr1.add is None: expr1.mask = nmlop.AND(expr1.mask, expr2, self.pos).reduce(id_dicts) return expr1 if op == nmlop.ADD and expr1.div is None and expr1.mod is None: if expr1.add is None: expr1.add = expr2 else: expr1.add = nmlop.ADD(expr1.add, expr2, self.pos).reduce(id_dicts) return expr1 if op == nmlop.SUB and expr1.div is None and expr1.mod is None: if expr1.add is None: expr1.add = ConstantNumeric(0) expr1.add = nmlop.SUB(expr1.add, expr2, self.pos).reduce(id_dicts) return expr1 # The div and mod fields cannot be used at the same time. Also whenever either of those # two are used the add field has to be set, so we change it to zero when it's not yet set. if op == nmlop.DIV and expr1.div is None and expr1.mod is None: if expr1.add is None: expr1.add = ConstantNumeric(0) expr1.div = expr2 return expr1 if op == nmlop.MOD and expr1.div is None and expr1.mod is None: if expr1.add is None: expr1.add = ConstantNumeric(0) expr1.mod = expr2 return expr1 # Since we have a lot of nml-variables that are in fact only the high bits of an nfo # variable it can happen that we want to shift back the variable to the left. # Don't use any extra opcodes but just reduce the shift-right in that case. if ( op == nmlop.SHIFT_LEFT and isinstance(expr2, ConstantNumeric) and expr1.add is None and expr2.value < expr1.shift.value ): expr1.shift.value -= expr2.value expr1.mask = nmlop.SHIFT_LEFT(expr1.mask, expr2).reduce() return expr1 # - Try to merge multiple additions/subtractions with constant numbers if ( op in (nmlop.ADD, nmlop.SUB) and isinstance(expr2, ConstantNumeric) and isinstance(expr1, BinOp) and expr1.op in (nmlop.ADD, nmlop.SUB) and isinstance(expr1.expr2, ConstantNumeric) ): val = expr2.value if op == nmlop.ADD else -expr2.value if expr1.op == nmlop.ADD: return nmlop.ADD(expr1.expr1, (expr1.expr2.value + val), self.pos).reduce() if expr1.op == nmlop.SUB: return nmlop.SUB(expr1.expr1, (expr1.expr2.value - val), self.pos).reduce() if op == nmlop.OR and isinstance(expr1, Boolean) and isinstance(expr2, Boolean): return Boolean(nmlop.OR(expr1.expr, expr2.expr, self.pos)).reduce(id_dicts) return BinOp(op, expr1, expr2, self.pos)