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 cargo_profit_value(value): # In NFO, calculation is (amount * price_factor * cb_result) / 8192 # Units of the NML price factor differ by about 41.12, i.e. 1 NML unit = 41 NFO units # That'd make the formula (amount * price_factor * cb_result) / (8192 / 41) # This is almost (error 0.01%) equivalent to the following, which is what this calculation does # (amount * price_factor * (cb_result * 329 / 256)) / 256 # This allows us to report a factor of 256 in the documentation, which makes a lot more sense than 199.804... # Not doing the division here would improve accuracy, but limits the range of the return value too much value = nmlop.MUL(value, 329) return nmlop.DIV(value, 256)
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_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 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 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_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 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_mul_div(mul, div): return lambda var, info: nmlop.DIV(nmlop.MUL(var, mul), div)
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