def get_sprite_number(self): # Layout of sprite number # bit 0 - 13: Sprite number # bit 14 - 15: Recolour mode (normal/transparent/remap) # bit 16 - 29: Palette sprite number # bit 30: Always draw sprite, even in transparent mode # bit 31: This is a custom sprite (from action1), not a TTD sprite if not self.is_set("sprite"): raise generic.ScriptError( "'sprite' must be set for this layout sprite", self.pos) # Make sure that recolouring is set correctly if self.get_param("recolour_mode") == 0 and self.is_set("palette"): raise generic.ScriptError( "'palette' may not be set when 'recolour_mode' is RECOLOUR_NONE." ) elif self.get_param("recolour_mode") != 0 and not self.is_set( "palette"): raise generic.ScriptError( "'palette' must be set when 'recolour_mode' is not set to RECOLOUR_NONE." ) # Add the constant terms first sprite_num = self.get_param("recolour_mode") << 14 if self.get_param("always_draw"): sprite_num |= 1 << 30 if self.sprite_from_action1: sprite_num |= 1 << 31 # Add the sprite expr = nmlop.ADD(self.get_param("sprite"), sprite_num, self.pos) # Add the palette expr = nmlop.ADD( nmlop.SHIFT_LEFT(self.get_param("palette"), 16, self.pos), expr) return expr.reduce()
def builtin_date(name, args, pos): """ date(year, month, day) builtin function. @return Days since 1 jan 1 of the given date. """ days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] if len(args) != 3: raise generic.ScriptError("date() requires exactly 3 arguments", pos) identifier.ignore_all_invalid_ids = True year = args[0].reduce(global_constants.const_list) identifier.ignore_all_invalid_ids = False try: month = args[1].reduce_constant().value day = args[2].reduce_constant().value except generic.ConstError: raise generic.ScriptError( "Month and day parameters of date() should be compile-time constants", pos) generic.check_range(month, 1, 12, "month", args[1].pos) generic.check_range(day, 1, days_in_month[month - 1], "day", args[2].pos) if not isinstance(year, ConstantNumeric): if month != 1 or day != 1: raise generic.ScriptError( "when the year parameter of date() is not a compile time constant month and day should be 1", pos) # num_days = year*365 + year/4 - year/100 + year/400 part1 = nmlop.MUL(year, 365) part2 = nmlop.DIV(year, 4) part3 = nmlop.DIV(year, 100) part4 = nmlop.DIV(year, 400) res = nmlop.ADD(part1, part2) res = nmlop.SUB(res, part3) res = nmlop.ADD(res, part4) return res generic.check_range(year.value, 0, 5000000, "year", year.pos) day_in_year = 0 for i in range(month - 1): day_in_year += days_in_month[i] day_in_year += day if month >= 3 and (year.value % 4 == 0) and ((not year.value % 100 == 0) or (year.value % 400 == 0)): day_in_year += 1 return ConstantNumeric( year.value * 365 + calendar.leapdays(0, year.value) + day_in_year - 1, pos)
def builtin_visual_effect_and_powered(name, args, pos): """ Builtin function, used in two forms: visual_effect_and_powered(effect, offset, powered) visual_effect(effect, offset) Use this to set the vehicle property visual_effect[_and_powered] and for the callback VEH_CB_VISUAL_EFFECT[_AND_POWERED] """ arg_len = 2 if name == "visual_effect" else 3 if len(args) != arg_len: raise generic.ScriptError(name + "() must have {:d} parameters".format(arg_len), pos) effect = args[0].reduce_constant(global_constants.const_list).value offset = nmlop.ADD(args[1], 8).reduce_constant().value generic.check_range(offset, 0, 0x0F, "offset in function " + name, pos) if arg_len == 3: powered = args[2].reduce_constant(global_constants.const_list).value if powered != 0 and powered != 0x80: raise generic.ScriptError( "3rd argument to visual_effect_and_powered (powered) must be" " either ENABLE_WAGON_POWER or DISABLE_WAGON_POWER", pos, ) else: powered = 0 return ConstantNumeric(effect | offset | powered)
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 preprocess_ternaryop(self, expr): assert isinstance(expr, expression.TernaryOp) guard = expression.Boolean(expr.guard).reduce() self.parse(guard) if isinstance(expr.expr1, expression.ConstantNumeric) and isinstance(expr.expr2, expression.ConstantNumeric): # This can be done more efficiently as (guard)*(expr1-expr2) + expr2 self.var_list.append(nmlop.MUL) diff_var = VarAction2Var(0x1A, 0, expr.expr1.value - expr.expr2.value) diff_var.comment = "expr1 - expr2" self.var_list.append(diff_var) self.var_list.append(nmlop.ADD) # Add var sizes, +2 for the operators self.var_list_size += 2 + diff_var.get_size() return expr.expr2 else: guard_var = VarAction2StoreTempVar() guard_var.comment = "guard" inverted_guard_var = VarAction2StoreTempVar() inverted_guard_var.comment = "!guard" self.var_list.append(nmlop.STO_TMP) self.var_list.append(guard_var) self.var_list.append(nmlop.XOR) var = VarAction2Var(0x1A, 0, 1) self.var_list.append(var) self.var_list.append(nmlop.STO_TMP) self.var_list.append(inverted_guard_var) self.var_list.append(nmlop.VAL2) # the +4 is for the 4 operators added above (STO_TMP, XOR, STO_TMP, VAL2) self.var_list_size += 4 + guard_var.get_size() + inverted_guard_var.get_size() + var.get_size() expr1 = nmlop.MUL(expr.expr1, VarAction2LoadTempVar(guard_var)) expr2 = nmlop.MUL(expr.expr2, VarAction2LoadTempVar(inverted_guard_var)) return nmlop.ADD(expr1, expr2)
def builtin_palette_2cc(name, args, pos): """ palette_2cc(colour1, colour2) builtin function. @return Recolour sprite to use """ if len(args) != 2: raise generic.ScriptError(name + "() must have 2 parameters", pos) for i in range(0, 2): if isinstance(args[i], ConstantNumeric): generic.check_range(args[i].value, 0, 15, "Argument of '{}'".format(name), args[i].pos) col2 = nmlop.MUL(args[1], 16, pos) col12 = nmlop.ADD(col2, args[0]) # Base sprite is not a constant base = global_constants.patch_variable("base_sprite_2cc", global_constants.patch_variables["base_sprite_2cc"], pos) return nmlop.ADD(col12, base)
def builtin_palette_1cc(name, args, pos): """ palette_1cc(colour) builtin function. @return Recolour sprite 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) return nmlop.ADD(args[0], 775, pos)
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 parse_graphics_block(graphics_block, feature, id, size, is_livery_override=False): """ Parse a graphics block (or livery override) into a list of actions, mainly action3 @param graphics_block: Graphics-block to parse @type graphics_block: L{GraphicsBlock} @param feature: Feature of the associated item @type feature: C{int} @param id: ID of the associated item @type id: L{Expression} @param size: Size of the associated item (relevant for houses only) @type size: L{ConstantNumeric} or C{None} @param is_livery_override: Whether this is a livery override instead of a normal graphics block @type is_livery_override: C{bool} @return: The resulting list of actions @rtype: L{BaseAction} """ action_list = action2real.create_spriteset_actions(graphics_block) if feature == 0x07: # Multi-tile houses need more work size_bit = size.value if size is not None else 0 for i, tile in enumerate(house_tiles[size_bit]): tile_id = id if i == 0 else nmlop.ADD(id, i).reduce() action_list.extend( parse_graphics_block_single_id(graphics_block, feature, tile_id, is_livery_override, tile, id)) else: action_list.extend( parse_graphics_block_single_id(graphics_block, feature, id, is_livery_override)) return action_list
def parse_actionA(replaces): """ Parse replace-block to ActionA. @param replaces: Replace-block to parse. @type replaces: L{ReplaceSprite} """ action_list = [] action6.free_parameters.save() act6 = action6.Action6() real_sprite_list = real_sprite.parse_sprite_data(replaces) block_list = [] total_sprites = len(real_sprite_list) offset = 2 # Skip 0A and <num-sets> sprite_offset = 0 # Number of sprites already covered by previous [<num-sprites> <first-sprite>]-pairs while total_sprites > 0: this_block = min(total_sprites, 255) # number of sprites in this block total_sprites -= this_block offset += 1 # Skip <num-sprites> first_sprite = replaces.start_id # number of first sprite if sprite_offset != 0: first_sprite = nmlop.ADD(first_sprite, sprite_offset).reduce() first_sprite, offset = actionD.write_action_value( first_sprite, action_list, act6, offset, 2) block_list.append((this_block, first_sprite.value)) sprite_offset += this_block # increase first-sprite for next block if len(act6.modifications) > 0: action_list.append(act6) action6.free_parameters.restore() action_list.append(ActionA(block_list)) action_list.extend(real_sprite_list) return action_list
def pre_process(self): new_costs = [] for cost in self.costs: cost.value = cost.value.reduce(global_constants.const_list) if isinstance(cost.value, expression.ConstantNumeric): generic.check_range(cost.value.value, -8, 16, "Base cost value", cost.value.pos) cost.value = nmlop.ADD(cost.value, 8).reduce() if isinstance(cost.name, expression.Identifier): if cost.name.value in base_cost_table: cost.name = expression.ConstantNumeric( base_cost_table[cost.name.value][0]) new_costs.append(cost) elif cost.name.value in generic_base_costs: # create temporary list, so it can be sorted for efficiency tmp_list = [] for num, type in base_cost_table.values(): if type == cost.name.value: tmp_list.append( assignment.Assignment( expression.ConstantNumeric(num), cost.value, cost.name.pos)) tmp_list.sort(key=lambda x: x.name.value) new_costs.extend(tmp_list) else: raise generic.ScriptError( "Unrecognized base cost identifier '{}' encountered". format(cost.name.value), cost.name.pos) else: cost.name = cost.name.reduce() if isinstance(cost.name, expression.ConstantNumeric): generic.check_range(cost.name.value, 0, len(base_cost_table), "Base cost number", cost.name.pos) new_costs.append(cost) self.costs = new_costs
def parse_property_value(prop_info, value, unit=None, size_bit=None): """ Parse a single property value / unit To determine the value that is to be used in nfo @param prop_info: A dictionary with property information @type prop_info: C{dict} @param value: Value of the property @type value: L{Expression} @param unit: Unit of the property value (e.g. km/h) @type unit: L{Unit} or C{None} @param size_bit: Bit that indicates the size of a multitile house Set iff the item is a house @type size_bit: C{int} or C{None} @return: List of values to actually use (in nfo) for the property @rtype: L{Expression} """ # Change value to use, except when the 'nfo' unit is used if unit is None or unit.type != "nfo": # Save the original value to test conversion against it org_value = value # Multiply by property-specific conversion factor mul, div = 1, 1 if "unit_conversion" in prop_info: mul = prop_info["unit_conversion"] if isinstance(mul, tuple): mul, div = mul # Divide by conversion factor specified by unit if unit is not None: if "unit_type" not in prop_info or unit.type != prop_info[ "unit_type"]: raise generic.ScriptError("Invalid unit for property", value.pos) unit_mul, unit_div = unit.convert, 1 if isinstance(unit_mul, tuple): unit_mul, unit_div = unit_mul mul *= unit_div div *= unit_mul # Factor out common factors gcd = generic.greatest_common_divisor(mul, div) mul //= gcd div //= gcd if isinstance(value, (expression.ConstantNumeric, expression.ConstantFloat)): # Even if mul == div == 1, we have to round floats and adjust value value = expression.ConstantNumeric( int(float(value.value) * mul / div + 0.5), value.pos) if unit is not None and "adjust_value" in prop_info: value = adjust_value(value, org_value, unit, prop_info["adjust_value"]) elif mul != div: # Compute (value * mul + div/2) / div value = nmlop.MUL(value, mul) value = nmlop.ADD(value, int(div / 2)) value = nmlop.DIV(value, div) elif isinstance(value, expression.ConstantFloat): # Round floats to ints value = expression.ConstantNumeric(int(value.value + 0.5), value.pos) # Apply value_function if it exists if "value_function" in prop_info: value = prop_info["value_function"](value) # Make multitile houses work if size_bit is not None: num_ids = house_sizes[size_bit] assert "multitile_function" in prop_info ret = prop_info["multitile_function"](value, num_ids, size_bit) assert len(ret) == num_ids return ret else: return [value]
def value_add_constant(const): return lambda var, info: nmlop.ADD(var, const)
def compute_table(snowline): """ Compute the table with snowline height for each day of the year. @param snowline: Snowline definition. @type snowline: L{Snowline} @return: Table of 12*32 entries with snowline heights. @rtype: C{str} """ day_table = [None] * 365 # Height at each day, starting at day 0 for dh in snowline.date_heights: doy = dh.name.reduce() if not isinstance(doy, expression.ConstantNumeric): raise generic.ScriptError( "Day of year is not a compile-time constant", doy.pos) if doy.value < 1 or doy.value > 365: raise generic.ScriptError( "Day of the year must be between 1 and 365", doy.pos) height = dh.value.reduce() if isinstance(height, expression.ConstantNumeric) and height.value < 0: raise generic.ScriptError("Height must be at least 0", height.pos) if dh.unit is None: if isinstance(height, expression.ConstantNumeric) and height.value > 255: raise generic.ScriptError("Height must be at most 255", height.pos) else: unit = dh.unit if unit.type != "snowline": raise generic.ScriptError( 'Expected a snowline percentage ("snow%")', height.pos) if isinstance(height, expression.ConstantNumeric) and height.value > 100: raise generic.ScriptError("Height must be at most 100 snow%", height.pos) mul, div = unit.convert, 1 if isinstance(mul, tuple): mul, div = mul # Factor out common factors gcd = generic.greatest_common_divisor(mul, div) mul //= gcd div //= gcd if isinstance( height, (expression.ConstantNumeric, expression.ConstantFloat)): # Even if mul == div == 1, we have to round floats and adjust value height = expression.ConstantNumeric( int(float(height.value) * mul / div + 0.5), height.pos) elif mul != div: # Compute (value * mul + div/2) / div height = nmlop.MUL(height, mul) height = nmlop.ADD(height, int(div / 2)) height = nmlop.DIV(height, div) # For 'linear' snow-line, only accept integer constants. if snowline.type != "equal" and not isinstance( height, expression.ConstantNumeric): raise generic.ScriptError("Height is not a compile-time constant", height.pos) day_table[doy.value - 1] = height # Find first specified point. start = 0 while start < 365 and day_table[start] is None: start = start + 1 if start == 365: raise generic.ScriptError("No heights given for the snowline table", snowline.pos) first_point = start while True: # Find second point from start end = start + 1 if end == 365: end = 0 while end != first_point and day_table[end] is None: end = end + 1 if end == 365: end = 0 # Fill the days between start and end (exclusive both border values) startvalue = day_table[start] endvalue = day_table[end] unwrapped_end = end if end < start: unwrapped_end += 365 if snowline.type == "equal": for day in range(start + 1, unwrapped_end): if day >= 365: day -= 365 day_table[day] = startvalue else: assert snowline.type == "linear" if start != end: dhd = float(endvalue.value - startvalue.value) / float(unwrapped_end - start) else: assert startvalue.value == endvalue.value dhd = 0 for day in range(start + 1, unwrapped_end): uday = day if uday >= 365: uday -= 365 height = startvalue.value + int(round(dhd * (day - start))) day_table[uday] = expression.ConstantNumeric(height) if end == first_point: # All days done break start = end table = [None] * (12 * 32) for dy in range(365): today = datetime.date.fromordinal(dy + 1) if day_table[dy]: expr = day_table[dy].reduce() else: expr = None table[(today.month - 1) * 32 + today.day - 1] = expr for idx, d in enumerate(table): if d is None: table[idx] = table[idx - 1] # Second loop is needed because we need make sure the first item is also set. for idx, d in enumerate(table): if d is None: table[idx] = table[idx - 1] return table
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)