nml-0.4.5/ 0000755 0005672 0005672 00000000000 13315644467 013465 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/nml/ 0000755 0005672 0005672 00000000000 13315644467 014253 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/nml/generic.py 0000644 0005672 0005672 00000034610 13315644406 016236 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
# -*- coding: utf-8 -*-
import sys, os, time
def truncate_int32(value):
"""
Truncate the given value so it can be stored in exactly 4 bytes. The sign
will be kept. Too high or too low values will be cut off, not clamped to
the valid range.
@param value: The value to truncate.
@type value: C{int}
@return: The truncated value.
@rtype: C{int}.
"""
#source: http://www.tiac.net/~sw/2010/02/PureSalsa20/index.html
return int( (value & 0x7fffFFFF) | -(value & 0x80000000) )
def check_range(value, min_value, max_value, name, pos):
"""
Check if a value is within a certain range and raise an error if it's not.
@param value: The value to check.
@type value: C{int}
@param min_value: Minimum valid value.
@type min_value: C{int}
@param max_value: Maximum valid value.
@type max_value: C{int}
@param name: Name of the variable that is being tested.
@type name: C{str}
@param pos: Position information from the variable being tested.
@type pos: L{Position}
"""
if not min_value <= value <= max_value:
raise RangeError(value, min_value, max_value, name, pos)
def greatest_common_divisor(a, b):
"""
Get the greatest common divisor of two numbers
@param a: First number.
@type a: C{int}
@param b: Second number.
@type b: C{int}
@return: Greatest common divisor.
@rtype: C{int}
"""
while b != 0:
t = b
b = a % b
a = t
return a
def reverse_lookup(dic, val):
"""
Perform reverse lookup of any key that has the provided value.
@param dic: Dictionary to perform reverse lookup on.
@type dic: C{dict}
@param val: Value being searched.
@type val: an existing value.
@return: A key such that C{dict[key] == val}.
@rtype: Type of the matching key.
"""
for k, v in dic.items():
if v == val: return k
raise AssertionError("Value not found in the dictionary.")
def build_position(poslist):
"""
Construct a L{Position} object that takes the other positions as include list.
@param poslist: Sequence of positions to report. First entry is the innermost position,
last entry is the nml statement that started it all.
@type poslist: C{list} of L{Position}
@return: Position to attach to an error.
@rtype: L{Position}
"""
if poslist is None or len(poslist) == 0:
return None
if len(poslist) == 1:
return poslist[0]
pos = poslist[-1]
pos.includes = pos.includes + poslist[:-1]
return pos
class Position(object):
"""
Base class representing a position in a file.
@ivar filename: Name of the file.
@type filename: C{str}
@ivar includes: List of file includes
@type includes: C{list} of L{Position}
"""
def __init__(self, filename, includes):
self.filename = filename
self.includes = includes
class LinePosition(Position):
"""
Line in a file.
@ivar line_start: Line number (starting with 1) where the position starts.
@type line_start: C{int}
"""
def __init__(self, filename, line_start, includes = []):
Position.__init__(self, filename, includes)
self.line_start = line_start
def __str__(self):
return '"{}", line {:d}'.format(self.filename, self.line_start)
class PixelPosition(Position):
"""
Position of a pixel (in a file with graphics).
@ivar xpos: Horizontal position of the pixel.
@type xpos: C{int}
@ivar ypos: Vertical position of the pixel.
@type ypos: C{int}
"""
def __init__(self, filename, xpos, ypos):
Position.__init__(self, filename, [])
self.xpos = xpos
self.ypos = ypos
def __str__(self):
return '"{}" at [x: {:d}, y: {:d}]'.format(self.filename, self.xpos, self.ypos)
class ImageFilePosition(Position):
"""
Generic (not position-dependant) error with an image file
"""
def __init__(self, filename, pos = None):
poslist = []
if pos is not None: poslist.append(pos)
Position.__init__(self, filename, poslist)
def __str__(self):
return 'Image file "{}"'.format(self.filename)
class LanguageFilePosition(Position):
"""
Generic (not position-dependant) error with a language file.
"""
def __init__(self, filename):
Position.__init__(self, filename, [])
def __str__(self):
return 'Language file "{}"'.format(self.filename)
class ScriptError(Exception):
def __init__(self, value, pos = None):
self.value = value
self.pos = pos
def __str__(self):
if self.pos is None:
return self.value
else:
ret = str(self.pos) + ": " + self.value
for inc in reversed(self.pos.includes):
ret += "\nIncluded from: " + str(inc)
return ret
class ConstError(ScriptError):
"""
Error to denote a compile-time integer constant was expected but not found.
"""
def __init__(self, pos = None):
ScriptError.__init__(self, "Expected a compile-time integer constant", pos)
class RangeError(ScriptError):
def __init__(self, value, min_value, max_value, name, pos = None):
ScriptError.__init__(self, name + " out of range " + str(min_value) + ".." + str(max_value) + ", encountered " + str(value), pos)
class ImageError(ScriptError):
def __init__(self, value, filename, pos = None):
ScriptError.__init__(self, value, ImageFilePosition(filename, pos))
class OnlyOnceError(ScriptError):
"""
An error denoting two elements in a single grf were found, where only one is allowed.
"""
def __init__(self, typestr, pos = None):
"""
@param typestr: Description of the type of element encountered.
@type typestr: C{str}
@param pos: Position of the error, if provided.
@type pos: C{None} or L{Position}
"""
ScriptError.__init__(self, "A grf may contain only one {}.".format(typestr), pos)
class OnlyOnce:
"""
Class to enforce that certain objects / constructs appear only once.
"""
seen = {}
@classmethod
def enforce(cls, obj, typestr):
"""
If this method is called more than once for an object of the exact same
class, an OnlyOnceError is raised.
"""
objtype = obj.__class__
if objtype in cls.seen:
raise OnlyOnceError(typestr, obj.pos)
cls.seen[objtype] = None
@classmethod
def clear(cls):
cls.seen = {}
VERBOSITY_WARNING = 1 # Verbosity level for warnings
VERBOSITY_INFO = 2 # Verbosity level for info messages
VERBOSITY_PROGRESS = 3 # Verbosity level for progress feedback
VERBOSITY_TIMING = 4 # Verbosity level for timing information
VERBOSITY_MAX = 4 # Maximum verbosity level
"""
Verbosity level for console output.
"""
verbosity_level = VERBOSITY_PROGRESS
def set_verbosity(level):
global verbosity_level
verbosity_level = level
def print_eol(msg):
"""
Clear current line and print message without linefeed.
"""
if not os.isatty(sys.stdout.fileno()):
return
print("\r" + msg + "\033[K", end="")
"""
Current progress message.
"""
progress_message = None
"""
Timestamp when the current processing step started.
"""
progress_start_time = None
"""
Timestamp of the last incremental progress update.
"""
progress_update_time = None
def hide_progress():
if progress_message is not None:
print_eol("")
def show_progress():
if progress_message is not None:
print_eol(progress_message)
def clear_progress():
global progress_message
global progress_start_time
global progress_update_time
hide_progress()
if (progress_message is not None) and (verbosity_level >= VERBOSITY_TIMING):
print("{} {:.1f} s".format(progress_message, time.clock() - progress_start_time))
progress_message = None
progress_start_time = None
progress_update_time = None
def print_progress(msg, incremental = False):
"""
Output progess information to the user.
@param msg: Progress message.
@type msg: C{str}
@param incremental: True if this message is updated incrementally (that is, very often).
@type incremental: C{bool}
"""
if verbosity_level < VERBOSITY_PROGRESS:
return
global progress_message
global progress_start_time
global progress_update_time
if (not incremental) and (progress_message is not None):
clear_progress()
progress_message = msg
if incremental:
t = time.clock()
if (progress_update_time is not None) and (t - progress_update_time < 1):
return
progress_update_time = t
else:
progress_start_time = time.clock()
print_eol(msg)
def print_info(msg):
"""
Output a pure informational message to th euser.
"""
if verbosity_level < VERBOSITY_INFO:
return
hide_progress()
print(" nmlc info: " + msg)
show_progress()
def print_warning(msg, pos = None):
"""
Output a warning message to the user.
"""
if verbosity_level < VERBOSITY_WARNING:
return
if pos:
msg = str(pos) + ": " + msg
msg = " nmlc warning: " + msg
if (sys.stderr.isatty()) and (os.name == 'posix'):
msg = "\033[33m" + msg + "\033[0m"
hide_progress()
print(msg, file=sys.stderr)
show_progress()
def print_error(msg):
"""
Output an error message to the user.
"""
clear_progress()
print("nmlc ERROR: " + msg, file=sys.stderr)
def print_dbg(indent, *args):
"""
Output debug text.
@param indent: Indentation to add to the output.
@type indent: C{int}
@param args: Arguments to print. An additional space is printed between them.
@type args: C{Tuple} of C{str}
"""
hide_progress()
print(indent * ' ' + ' '.join(str(arg) for arg in args))
show_progress()
"""
Paths already resolved to correct paths on the system.
The key is the path as specified in the sources. The value is the validated path on the system.
"""
_paths = dict()
def find_file(filepath):
"""
Verify whether L{filepath} exists. If not, try to find a similar one with a
different case.
@param filepath: Path to the file to open.
@type filepath: C{str}
@return: Path name to a file that exists at the file system.
@rtype: C{str}
"""
# Split the filepath in components (directory parts and the filename).
drive, filepath = os.path.splitdrive(os.path.normpath(filepath))
# 'splitdrive' above does not remove the leading / of absolute Unix paths.
# The 'split' below splits on os.sep, which means that loop below fails for "/".
# To prevent that, handle the leading os.sep separately.
if filepath.startswith(os.sep):
drive = drive + os.sep
filepath = filepath[len(os.sep):]
components = [] # Path stored in reverse order (filename at index[0])
while filepath != '':
filepath, filepart = os.path.split(filepath)
components.append(filepart)
# Re-build the absolute path.
path = drive
if path == '':
path = os.getcwd()
while len(components) > 0:
comp = components.pop()
childpath = os.path.join(path, comp)
if childpath in _paths:
path = _paths[childpath]
continue
if os.access(path, os.R_OK):
# Path is readable, compare provided path with the file system.
entries = os.listdir(path) + [os.curdir, os.pardir]
lcomp = comp.lower()
matches = [entry for entry in entries if lcomp == entry.lower()]
if len(matches) == 0:
raise ScriptError("Path \"{}\" does not exist (even after case conversions)".format(os.path.join(path, comp)))
elif len(matches) > 1:
raise ScriptError("Path \"{}\" is not unique (case conversion gave {:d} solutions)".format(os.path.join(path, comp), len(matches)))
if matches[0] != comp:
given_path = os.path.join(path, comp)
real_path = os.path.join(path, matches[0])
msg = "Path \"{}\" at the file system does not match path \"{}\" given in the input (case mismatch in the last component)"
msg = msg.format(real_path, given_path)
print_warning(msg)
elif os.access(path, os.X_OK):
# Path is only accessible, cannot inspect the file system.
matches = [comp]
else:
raise ScriptError("Path \"{}\" does not exist or is not accessible".format(path))
path = os.path.join(path, matches[0])
if len(components) > 0:
_paths[childpath] = path
return path
cache_root_dir = ".nmlcache"
def set_cache_root_dir(dir):
cache_root_dir = os.path.abspath(dir)
os.makedirs(cache_root_dir, exist_ok=True)
def get_cache_file(sources, extension):
"""
Compose a filename for a cache file.
@param sources: List of source files, the cache file depends on / belongs to.
@type sources: C{list} or C{tuple} of C{str} or similar.
@param extension: File extension for the cache file including leading ".".
@type extension: C{str}
@return: Filename for cache file.
@rtype: C{str}
"""
result = ""
for part in sources:
if part is not None:
path, name = os.path.split(part)
if len(result) == 0:
# Make sure that the path does not leave the cache dir
path = os.path.normpath(path).replace(os.path.pardir, "__")
path = os.path.join(cache_root_dir, path)
os.makedirs(path, exist_ok=True)
result = os.path.join(path, name)
else:
# In case of multiple soure files, ignore the path component for all but the first
result += "_" + name
return result + extension
nml-0.4.5/nml/tokens.py 0000644 0005672 0005672 00000022165 13315644406 016127 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import sys, re
import ply.lex as lex
from nml import expression, generic
reserved = {
'grf' : 'GRF',
'var' : 'VARIABLE',
'param' : 'PARAMETER',
'cargotable' : 'CARGOTABLE',
'railtypetable' : 'RAILTYPETABLE',
'if' : 'IF',
'else' : 'ELSE',
'while' : 'WHILE', # reserved
'item' : 'ITEM', # action 0/3
'property' : 'PROPERTY',
'graphics' : 'GRAPHICS',
'snowline' : 'SNOWLINE',
'basecost' : 'BASECOST',
'template' : 'TEMPLATE', # sprite template for action1
'spriteset' : 'SPRITESET', # action 1
'spritegroup' : 'SPRITEGROUP', # action 2
'switch' : 'SWITCH', # deterministic varaction2
'random_switch' : 'RANDOMSWITCH', # random action2
'produce' : 'PRODUCE', # production action2
'error' : 'ERROR', # action B
'disable_item' : 'DISABLE_ITEM',
'replace' : 'REPLACESPRITE', # action A
'replacenew' : 'REPLACENEWSPRITE', # action 5
'font_glyph' : 'FONTGLYPH', # action 12
'deactivate' : 'DEACTIVATE', # action E
'town_names' : 'TOWN_NAMES', # action F
'string' : 'STRING',
'return' : 'RETURN',
'livery_override' : 'LIVERYOVERRIDE',
'exit' : 'SKIP_ALL',
'tilelayout' : 'TILELAYOUT',
'spritelayout' : 'SPRITELAYOUT',
'alternative_sprites' : 'ALT_SPRITES',
'base_graphics' : 'BASE_GRAPHICS',
'recolour_sprite' : 'RECOLOUR_SPRITE',
'engine_override' : 'ENGINE_OVERRIDE',
'sort' : 'SORT_VEHICLES',
}
line_directive1_pat = re.compile(r'\#line\s+(\d+)\s*(\r?\n|"(.*)"\r?\n)')
line_directive2_pat = re.compile(r'\#\s+(\d+)\s+"(.*)"\s*((?:\d+\s*)*)\r?\n')
class NMLLexer(object):
"""
@ivar lexer: PLY scanner object.
@type lexer: L{ply.lex}
@ivar includes: Stack of included files.
@type includes: C{List} of L{generic.LinePosition}
@ivar text: Input text to scan.
@type text: C{str}
"""
# Tokens
tokens = list(reserved.values()) + [
'ID',
'PLUS',
'MINUS',
'TIMES',
'DIVIDE',
'MODULO',
'AND',
'OR',
'XOR',
'LOGICAL_AND',
'LOGICAL_OR',
'LOGICAL_NOT',
'BINARY_NOT',
'EQ',
'LPAREN',
'RPAREN',
'SHIFT_LEFT',
'SHIFT_RIGHT',
'SHIFTU_RIGHT',
'COMP_EQ',
'COMP_NEQ',
'COMP_LE',
'COMP_GE',
'COMP_LT',
'COMP_GT',
'COMMA',
'RANGE',
'LBRACKET',
'RBRACKET',
'LBRACE',
'RBRACE',
'TERNARY_OPEN',
'COLON',
'SEMICOLON',
'STRING_LITERAL',
'NUMBER',
'FLOAT',
'UNIT',
]
t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_MODULO = r'%'
t_DIVIDE = r'/'
t_AND = r'&'
t_OR = r'\|'
t_XOR = r'\^'
t_LOGICAL_AND = r'&&'
t_LOGICAL_OR = r'\|\|'
t_LOGICAL_NOT = r'!'
t_BINARY_NOT = r'~'
t_EQ = r'='
t_LPAREN = r'\('
t_RPAREN = r'\)'
t_SHIFT_LEFT = r'<<'
t_SHIFT_RIGHT = r'>>'
t_SHIFTU_RIGHT = r'>>>'
t_COMP_EQ = r'=='
t_COMP_NEQ = r'!='
t_COMP_LE = r'<='
t_COMP_GE = r'>='
t_COMP_LT = r'<'
t_COMP_GT = r'>'
t_COMMA = r','
t_RANGE = r'\.\.'
t_LBRACKET = r'\['
t_RBRACKET = r'\]'
t_LBRACE = r'{'
t_RBRACE = r'}'
t_TERNARY_OPEN = r'\?'
t_COLON = r':'
t_SEMICOLON = r';'
def t_FLOAT(self, t):
r'\d+\.\d+'
t.value = expression.ConstantFloat(float(t.value), t.lineno)
return t
def t_NUMBER(self, t):
r'(0x[0-9a-fA-F]+)|(\d+)'
base = 10
if len(t.value) >= 2 and t.value[0:2] == "0x":
t.value = t.value[2:]
base = 16
t.value = expression.ConstantNumeric(int(t.value, base), t.lineno)
return t
def t_UNIT(self, t):
r'(nfo)|(mph)|(km/h)|(m/s)|(hpI)|(hpM)|(hp)|(kW)|(tons)|(ton)|(kg)|(snow%)'
return t
def t_ID(self, t):
r'[a-zA-Z_][a-zA-Z0-9_]*'
if t.value in reserved: # Check for reserved words
t.type = reserved[t.value]
else:
t.type = 'ID'
t.value = expression.Identifier(t.value, t.lineno)
return t
def t_STRING_LITERAL(self, t):
r'"([^"\\]|\\.)*"'
t.value = expression.StringLiteral(t.value[1:-1], t.lineno)
return t
# Ignored characters
def t_ignore_comment(self, t):
r'(/\*(\n|.)*?\*/)|(//.*)'
self.increment_lines(t.value.count("\n"))
def t_ignore_whitespace(self, t):
"[ \t\r]+"
pass
def t_line_directive1(self, t):
r'\#line\s+\d+\s*(\r?\n|".*"\r?\n)'
# See: https://gcc.gnu.org/onlinedocs/cpp/Line-Control.html
m = line_directive1_pat.match(t.value)
assert m is not None
fname = self.lexer.lineno.filename if m.group(3) is None else m.group(3)
# This type of line directive contains no information about includes, so we have to make some assumptions
if self.includes and self.includes[-1].filename == fname:
# Filename equal to the one on top of the stack -> end of an include
self.includes.pop()
elif fname != self.lexer.lineno.filename:
# Not an end of include and not the current file -> start of an include
self.includes.append(self.lexer.lineno)
self.set_position(fname, int(m.group(1), 10))
self.increment_lines(t.value.count('\n') - 1)
def t_line_directive2(self, t):
r'\#\s+\d+\s+".*"\s*(\d+\s*)*\r?\n'
# Format: # lineno filename flags
# See: https://gcc.gnu.org/onlinedocs/cpp/Preprocessor-Output.html
m = line_directive2_pat.match(t.value)
assert m is not None
line, fname, flags = m.groups()
line = int(line, 10)
flags = [int(f, 10) for f in flags.split(" ") if f != ""] if flags is not None else []
if 1 in flags:
# File is being included, add current file/line to stack
self.includes.append(self.lexer.lineno)
elif 2 in flags:
# End of include, new file should be equal to the one on top of the stack
if self.includes and self.includes[-1].filename == fname:
self.includes.pop()
else:
# But of course user input can never be trusted
generic.print_warning("Information about included files is inconsistent, position information for errors may be wrong.")
self.set_position(fname, line)
self.increment_lines(t.value.count('\n') - 1)
def t_newline(self, t):
r'\n+'
self.increment_lines(len(t.value))
def t_error(self, t):
print(("Illegal character '{}' (character code 0x{:02X}) at {}, column {:d}".format(t.value[0], ord(t.value[0]), t.lexer.lineno, self.find_column(t))))
sys.exit(1)
def build(self):
"""
Initial construction of the scanner.
"""
self.lexer = lex.lex(module=self)
def setup(self, text, fname):
"""
Setup scanner for scanning an input file.
@param text: Input text to scan.
@type text: C{str}
@param fname: Filename associated with the input text (main input file).
@type fname: C{str}
"""
self.includes = []
self.text = text
self.set_position(fname, 1)
self.lexer.input(text)
def set_position(self, fname, line):
"""
@note: The lexer.lineno contains a Position object.
"""
self.lexer.lineno = generic.LinePosition(fname, line, self.includes[:])
def increment_lines(self, count):
self.set_position(self.lexer.lineno.filename, self.lexer.lineno.line_start + count)
def find_column(self, t):
last_cr = self.text.rfind('\n', 0, t.lexpos)
if last_cr < 0: last_cr = 0
return t.lexpos - last_cr
nml-0.4.5/nml/spritecache.py 0000644 0005672 0005672 00000030141 13315644406 017107 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import array, json, os
from nml import generic
keep_orphaned = True
class SpriteCache(object):
"""
Cache for compressed sprites.
@ivar cache_filename: Filename of cache data file.
@type cache_filename: C{str}
@ivar cache_index_filename: Filename of cache index file.
@type cache_index_filename: C{str}
@ivar cache_time: Date of cache files. The cache is invalid, if the source image files are newer.
@type cache_time: C{int}
@ivar cached_sprites: Cache contents
@type cached_sprites: C{dict} mapping cache keys to cache items.
Cache file format description:
Format of cache index is JSON (JavaScript Object Notation), which is
easily readable by both humans (for debugging) and machines. Format is as follows:
A list of dictionaries, each corresponding to a sprite, with the following keys
- rgb_file: filename of the 32bpp sprite (string)
- rgb_rect: (uncropped) rectangle of the 32bpp sprite (list with 4 elements (x,y,w,h))
- mask_file, mask_rect: same as above, but for 8bpp sprite
- mask_pal: palette of mask file, 'DEFAULT' or 'LEGACY'. Not present if 'mask_file' is not present.
- crop: List of 4 positive integers, indicating how much to crop if cropping is enabled
Order is (left, right, top, bottom), it is not present if cropping is disabled
- info: Info byte of the sprite
- pixel_stats: Dictionary with statistics about pixels:
'total': Total amount of pixels.
'alpha': Amount of semi-transparent pixels in 32bpp.
'white': Amount of pure-white pixels in 8bpp.
'anim': Amount of animated pixels in 8bpp.
- offset: Offset into the cache file for this sprite
- size: Length of this sprite in the cache file
Either rgb_file/rect, mask_file/rect, or both must be specified, depending on the sprite
The cache should contain the sprite data, but not the header (sizes/offsets and such)
For easy lookup, this information is stored in a dictionary
Tuples are used because these are hashable
The key is a (rgb_file, rgb_rect, mask_file, mask_rect, do_crop, palette)-tuple
The rectangles are 4-tuples, non-existent items (e.g. no rgb) are None
do_crop is a boolean indicating if this sprite has been cropped
palette is a string identifier
The value that this key maps to is a 6-tuple, containing:
- the sprite data (as a byte array)
- The 'info' byte of the sprite
- The cropping information (see above) (None if 'do_crop' in the key is false)
- The pixel_stats dictionary with statistics.
- Whether the sprite exists in the old (loaded)cache
- whether the sprite is used by the current GRF
The cache file itself is simply the binary sprite data with no
meta-information or padding. Offsets and sizes for the various sprites
are in the cacheindex file.
"""
def __init__(self, filename):
self.cache_filename = filename + ".cache"
self.cache_index_filename = filename + ".cacheindex"
self.cache_time = 0
self.cached_sprites = {}
def get_item(self, cache_key, palette):
"""
Get item from cache.
@param cache_key: Sprite metadata key, as returned by RealSprite.get_cache_key
@type cache_key: C{tuple}
@param palette: Palette identifier, one of palette.palette_name
@type palette: C{str}
@return: Cache item
@rtype: C{tuple} or C{None}
"""
if cache_key[2] is not None:
key = cache_key + (palette, )
else:
key = cache_key + (None, )
return self.cached_sprites.get(key, None)
def add_item(self, cache_key, palette, item):
"""
Add item to cache.
@param cache_key: Sprite metadata key, as returned by RealSprite.get_cache_key
@type cache_key: C{tuple}
@param palette: Palette identifier, one of palette.palette_name
@type palette: C{str}
@param item: Cache item
@type item: C{tuple}
"""
if cache_key[2] is not None:
key = cache_key + (palette, )
else:
key = cache_key + (None, )
self.cached_sprites[key] = item
def count_orphaned(self):
"""
Count number of items in cache, which have not been used.
@return: Number of unused items.
@type: C{int}
"""
return sum(not item[5] for item in self.cached_sprites.values())
def read_cache(self):
"""
Read the *.grf.cache[index] files.
"""
if not (os.access(self.cache_filename, os.R_OK) and os.access(self.cache_index_filename, os.R_OK)):
# Cache files don't exist
return
index_file = open(self.cache_index_filename, 'r')
cache_file = open(self.cache_filename, 'rb')
cache_data = array.array('B')
cache_size = os.fstat(cache_file.fileno()).st_size
cache_data.fromfile(cache_file, cache_size)
assert cache_size == len(cache_data)
self.cache_time = os.path.getmtime(self.cache_filename)
source_mtime = dict()
try:
# Just assert and print a generic message on errors, as the cache data should be correct
# Not asserting could lead to errors later on
# Also, it doesn't make sense to inform the user about things he shouldn't know about and can't fix
sprite_index = json.load(index_file)
assert isinstance(sprite_index, list)
for sprite in sprite_index:
assert isinstance(sprite, dict)
# load RGB (32bpp) data
rgb_key = (None, None)
if 'rgb_file' in sprite and 'rgb_rect' in sprite:
assert isinstance(sprite['rgb_file'], str)
assert isinstance(sprite['rgb_rect'], list) and len(sprite['rgb_rect']) == 4
assert all(isinstance(num, int) for num in sprite['rgb_rect'])
rgb_key = (sprite['rgb_file'], tuple(sprite['rgb_rect']))
# load Mask (8bpp) data
mask_key = (None, None)
if 'mask_file' in sprite and 'mask_rect' in sprite:
assert isinstance(sprite['mask_file'], str)
assert isinstance(sprite['mask_rect'], list) and len(sprite['mask_rect']) == 4
assert all(isinstance(num, int) for num in sprite['mask_rect'])
mask_key = (sprite['mask_file'], tuple(sprite['mask_rect']))
palette_key = None
if 'mask_pal' in sprite:
palette_key = sprite['mask_pal']
# Compose key
assert any(i is not None for i in rgb_key + mask_key)
key = rgb_key + mask_key + ('crop' in sprite, palette_key)
assert key not in self.cached_sprites
# Read size/offset from cache
assert 'offset' in sprite and 'size' in sprite
offset, size = sprite['offset'], sprite['size']
assert isinstance(offset, int) and isinstance(size, int)
assert offset >= 0 and size > 0
assert offset + size <= cache_size
data = cache_data[offset:offset+size]
# Read info / cropping data from cache
assert 'info' in sprite and isinstance(sprite['info'], int)
info = sprite['info']
if 'crop' in sprite:
assert isinstance(sprite['crop'], list) and len(sprite['crop']) == 4
assert all(isinstance(num, int) for num in sprite['crop'])
crop = tuple(sprite['crop'])
else:
crop = None
if 'pixel_stats' in sprite:
assert isinstance(sprite['pixel_stats'], dict)
pixel_stats = sprite['pixel_stats']
else:
pixel_stats = {}
# Compose value
value = (data, info, crop, pixel_stats, True, False)
# Check if cache item is still valid
is_valid = True
if rgb_key[0] is not None:
mtime = source_mtime.get(rgb_key[0])
if mtime is None:
mtime = os.path.getmtime(generic.find_file(rgb_key[0]))
source_mtime[rgb_key[0]] = mtime
if mtime > self.cache_time:
is_valid = False
if mask_key[0] is not None:
mtime = source_mtime.get(mask_key[0])
if mtime is None:
mtime = os.path.getmtime(generic.find_file(mask_key[0]))
source_mtime[mask_key[0]] = mtime
if mtime > self.cache_time:
is_valid = False
# Drop items from older spritecache format without palette entry
if (mask_key[0] is None) != (palette_key is None):
is_valid = False
if is_valid:
self.cached_sprites[key] = value
except:
generic.print_warning(self.cache_index_filename + " contains invalid data, ignoring. Please remove the file and file a bug report if this warning keeps appearing")
self.cached_sprites = {} # Clear cache
index_file.close()
cache_file.close()
def write_cache(self):
"""
Write the cache data to the .cache[index] files.
"""
index_data = []
sprite_data = array.array('B')
offset = 0
old_cache_valid = True
for key, value in list(self.cached_sprites.items()):
# Unpack key/value
rgb_file, rgb_rect, mask_file, mask_rect, do_crop, mask_pal = key
data, info, crop_rect, pixel_stats, in_old_cache, in_use = value
assert do_crop == (crop_rect is not None)
assert (mask_file is None) == (mask_pal is None)
# If this cache information is exactly the same as the old cache, then we don't bother writing later on
if not in_use and not keep_orphaned:
old_cache_valid = False
continue
if not in_old_cache:
old_cache_valid = False
# Create dictionary with information
sprite = {}
if rgb_file is not None:
sprite['rgb_file'] = rgb_file
sprite['rgb_rect'] = tuple(rgb_rect)
if mask_file is not None:
sprite['mask_file'] = mask_file
sprite['mask_rect'] = tuple(mask_rect)
sprite['mask_pal'] = mask_pal
size = len(data)
sprite['offset'] = offset
sprite['size'] = size
sprite['info'] = info
if do_crop: sprite['crop'] = tuple(crop_rect)
sprite['pixel_stats'] = pixel_stats
index_data.append(sprite)
sprite_data.extend(data)
offset += size
if old_cache_valid: return
index_output = json.JSONEncoder(sort_keys = True).encode(index_data)
index_file = open(self.cache_index_filename, 'w')
index_file.write(index_output)
index_file.close()
cache_file = open(self.cache_filename, 'wb')
sprite_data.tofile(cache_file)
cache_file.close()
nml-0.4.5/nml/editors/ 0000755 0005672 0005672 00000000000 13315644467 015724 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/nml/editors/extract_tables.py 0000644 0005672 0005672 00000012645 13315644406 021303 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import unit, tokens, global_constants
from nml.ast import general, switch, basecost
from nml.expression import functioncall
from nml.actions import action0properties, action2layout, action2var_variables
from nml.actions import action3_callbacks, action5, action12, actionB, real_sprite
#Create list of blocks, functions and units
units = set(unit.units.keys())
keywords = set(tokens.reserved.keys())
functions = set(functioncall.function_table.keys())
layouts = set(action2layout.layout_sprite_types.keys())
#No easy way to get action14 stuff
temp1 = units | keywords | functions | layouts | set(["int", "bool"])
block_names_table = sorted(temp1)
##Create list of properties and variables
var_tables = [
action2var_variables.varact2_globalvars,
action2var_variables.varact2vars_vehicles,
action2var_variables.varact2vars_trains,
action2var_variables.varact2vars_roadvehs,
action2var_variables.varact2vars_ships,
action2var_variables.varact2vars_aircraft,
action2var_variables.varact2vars60x_vehicles,
action2var_variables.varact2vars_base_stations,
action2var_variables.varact2vars60x_base_stations,
action2var_variables.varact2vars_stations,
action2var_variables.varact2vars60x_stations,
action2var_variables.varact2vars_canals,
action2var_variables.varact2vars_houses,
action2var_variables.varact2vars60x_houses,
action2var_variables.varact2vars_industrytiles,
action2var_variables.varact2vars60x_industrytiles,
action2var_variables.varact2vars_industries,
action2var_variables.varact2vars60x_industries,
action2var_variables.varact2vars_airports,
action2var_variables.varact2vars_objects,
action2var_variables.varact2vars60x_objects,
action2var_variables.varact2vars_railtype,
action2var_variables.varact2vars_airporttiles,
action2var_variables.varact2vars60x_airporttiles,
action2var_variables.varact2vars_towns]
variables = set()
for d in var_tables:
for key in d.keys():
variables.add(key)
prop_tables = [
action0properties.general_veh_props,
action0properties.properties[0x00],
action0properties.properties[0x01],
action0properties.properties[0x02],
action0properties.properties[0x03],
action0properties.properties[0x05],
action0properties.properties[0x07],
action0properties.properties[0x09],
action0properties.properties[0x0A],
action0properties.properties[0x0B],
action0properties.properties[0x0D],
action0properties.properties[0x0F],
action0properties.properties[0x10],
action0properties.properties[0x11]]
properties = set()
for d in prop_tables:
for key in d.keys():
properties.add(key)
dummy = action2layout.Action2LayoutSprite(None, None)
layout_sprites = set(dummy.params.keys())
cb_tables = [
action3_callbacks.general_vehicle_cbs,
action3_callbacks.callbacks[0x00],
action3_callbacks.callbacks[0x01],
action3_callbacks.callbacks[0x02],
action3_callbacks.callbacks[0x03],
action3_callbacks.callbacks[0x04],
action3_callbacks.callbacks[0x05],
action3_callbacks.callbacks[0x07],
action3_callbacks.callbacks[0x09],
action3_callbacks.callbacks[0x0A],
action3_callbacks.callbacks[0x0B],
action3_callbacks.callbacks[0x0D],
action3_callbacks.callbacks[0x0F],
action3_callbacks.callbacks[0x10],
action3_callbacks.callbacks[0x11]]
callbacks = set()
for d in cb_tables:
for key in d.keys():
callbacks.add(key)
#No easy way to get action14 stuff
act14_vars = set(["grfid", "name", "desc", "version", "min_compatible_version",
"type", "bit", "min_value", "max_value", "def_value", "names"])
temp2 = variables | properties | layout_sprites | callbacks | act14_vars
variables_names_table = sorted(temp2)
#Create list of features
features = set(general.feature_ids.keys())
switch_names = set(switch.var_ranges.keys())
temp3 = features | switch_names
feature_names_table = sorted(temp3)
#Create list of callbacks constants
const_tables = [
global_constants.constant_numbers,
global_constants.global_parameters,
global_constants.misc_grf_bits,
global_constants.patch_variables,
global_constants.config_flags,
global_constants.unified_maglev_var,
global_constants.railtype_table]
constant_names = set()
for d in const_tables:
for key in d.keys():
constant_names.add(key)
act5_names = set(action5.action5_table.keys())
act12_names = set(action12.font_sizes.keys())
actB_names = set(actionB.default_error_msg.keys()) | set(actionB.error_severity.keys())
sprite_names = set(real_sprite.real_sprite_flags.keys())
cost_names = set(basecost.base_cost_table.keys()) | set(basecost.generic_base_costs)
temp4 = constant_names | act5_names | act12_names | actB_names | sprite_names | cost_names
callback_names_table = sorted(temp4)
nml-0.4.5/nml/editors/kate.py 0000644 0005672 0005672 00000016463 13315644406 017225 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.editors import extract_tables
output_file="nml_kate.xml"
header_text = """\
"""
feature_text = """\
"""
builtin_text = """\
"""
constant_text = """\
"""
tail_text = """\
"""
def write_file(fname):
handle = open(fname, "w")
handle.write(header_text)
for word in extract_tables.keywords:
handle.write(" - {}
\n".format(word))
handle.write(feature_text)
for word in extract_tables.features:
handle.write(" - {}
\n".format(word))
handle.write(builtin_text)
for word in extract_tables.functions:
handle.write(" - {}
\n".format(word))
handle.write(constant_text)
for word in extract_tables.callback_names_table:
handle.write(" - {}
\n".format(word))
handle.write(tail_text)
handle.close()
def run():
write_file("nml_kate.xml")
nml-0.4.5/nml/editors/__init__.py 0000644 0005672 0005672 00000001243 13315644406 020026 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
nml-0.4.5/nml/editors/notepadpp.py 0000644 0005672 0005672 00000007664 13315644406 020276 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.editors import extract_tables
#Define parts of np++ xml file
string1 = """\
000000
{
}
( ) , : ; [ ]
1/* 2*/ 0//
"""
string2 = """\
"""
string3 = """\
"""
string4 = """\
"""
string5="""\
"""
#Build np++ xml file
def write_file(fname):
handle = open(fname, "w")
handle.write(string1)
handle.write(" ".join(extract_tables.block_names_table))
handle.write(string2)
handle.write(" ".join(extract_tables.variables_names_table))
handle.write(string3)
handle.write(" ".join(extract_tables.feature_names_table))
handle.write(string4)
handle.write(" ".join(extract_tables.callback_names_table))
handle.write(string5)
handle.close()
def run():
write_file("nml_notepadpp.xml")
nml-0.4.5/nml/main.py 0000644 0005672 0005672 00000047321 13315644406 015551 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import sys, os, codecs, optparse
from nml import generic, grfstrings, parser, version_info, output_nml, output_nfo, output_grf, output_dep, palette, spriteencoder, spritecache, global_constants
from nml.actions import action2layout, action2var, action8, sprite_count, real_sprite, action4, action0, action1, action2, action6, action7, action11, actionF
from nml.ast import grf, alt_sprites
try:
from PIL import Image
except ImportError:
try:
import Image
except ImportError:
pass
developmode = False # Give 'nice' error message instead of a stack dump.
version = version_info.get_nml_version()
def parse_cli(argv):
"""
Parse the command line, and process options.
@return: Options, and input filename (if not supplied, use C{sys.stdin}).
@rtype: C{tuple} (C{Object}, C{str} or C{None})
"""
usage = "Usage: %prog [options] \n" \
"Where is the nml file to parse"
opt_parser = optparse.OptionParser(usage=usage, version=version_info.get_cli_version())
opt_parser.set_defaults(debug=False, crop=False, compress=True, outputs=[], start_sprite_num=0,
custom_tags="custom_tags.txt", lang_dir="lang", default_lang="english.lng", cache_dir=".nmlcache",
forced_palette="ANY", quiet=False, md5_filename=None, keep_orphaned=True, verbosity=generic.verbosity_level)
opt_parser.add_option("-d", "--debug", action="store_true", dest="debug", help="write the AST to stdout")
opt_parser.add_option("-s", "--stack", action="store_true", dest="stack", help="Dump stack when an error occurs")
opt_parser.add_option("--grf", dest="grf_filename", metavar="", help="write the resulting grf to ")
opt_parser.add_option("--md5", dest="md5_filename", metavar="", help="Write an md5sum of the resulting grf to ")
opt_parser.add_option("--nfo", dest="nfo_filename", metavar="", help="write nfo output to ")
opt_parser.add_option("-M", action="store_true", dest="dep_check", help="output a rule suitable for make describing the graphics dependencies of the main grf file (requires input file or --grf)")
opt_parser.add_option("--MF", dest="dep_filename", metavar="", help="When used with -M, specifies a file to write the dependencies to")
opt_parser.add_option("--MT", dest="depgrf_filename", metavar="", help="target of the rule emitted by dependency generation (requires -M)")
opt_parser.add_option("-c", action="store_true", dest="crop", help="crop extraneous transparent blue from real sprites")
opt_parser.add_option("-u", action="store_false", dest="compress", help="save uncompressed data in the grf file")
opt_parser.add_option("--nml", dest="nml_filename", metavar="", help="write optimized nml to ")
opt_parser.add_option("-o", "--output", dest="outputs", action="append", metavar="", help="write output(nfo/grf) to ")
opt_parser.add_option("-t", "--custom-tags", dest="custom_tags", metavar="",
help="Load custom tags from [default: %default]")
opt_parser.add_option("-l", "--lang-dir", dest="lang_dir", metavar="",
help="Load language files from directory [default: %default]")
opt_parser.add_option("--default-lang", dest="default_lang", metavar="",
help="The default language is stored in [default: %default]")
opt_parser.add_option("--start-sprite", action="store", type="int", dest="start_sprite_num", metavar="",
help="Set the first sprite number to write (do not use except when you output nfo that you want to include in other files)")
opt_parser.add_option("-p", "--palette", dest="forced_palette", metavar="", choices = ["DEFAULT", "LEGACY", "DOS", "WIN", "ANY"],
help="Force nml to use the palette [default: %default]. Valid values are 'DEFAULT', 'LEGACY', 'ANY'")
opt_parser.add_option("--quiet", action="store_true", dest="quiet",
help="Disable all warnings. Errors will be printed normally.")
opt_parser.add_option("-n", "--no-cache", action="store_true", dest="no_cache",
help="Disable caching of sprites in .cache[index] files, which may reduce compilation time.")
opt_parser.add_option("--cache-dir", dest="cache_dir", metavar="", help="Cache files are stored in directory [default: %default]")
opt_parser.add_option("--clear-orphaned", action="store_false", dest="keep_orphaned", help="Remove unused/orphaned items from cache files.")
opt_parser.add_option("--verbosity", type="int", dest="verbosity", metavar="", help="Set the verbosity level for informational output. [default: %default, max: {}]".format(generic.VERBOSITY_MAX))
opts, args = opt_parser.parse_args(argv)
generic.set_verbosity(0 if opts.quiet else opts.verbosity)
generic.set_cache_root_dir(opts.cache_dir)
spritecache.keep_orphaned = opts.keep_orphaned
opts.outputfile_given = (opts.grf_filename or opts.nfo_filename or opts.nml_filename or opts.dep_filename or opts.outputs)
if not args:
if not opts.outputfile_given:
opt_parser.print_help()
sys.exit(2)
input_filename = None # Output filenames, but no input -> use stdin.
elif len(args) > 1:
opt_parser.error("Error: only a single nml file can be read per run")
else:
input_filename = args[0]
if not os.access(input_filename, os.R_OK):
raise generic.ScriptError('Input file "{}" does not exist'.format(input_filename))
return opts, input_filename
def main(argv):
global developmode
opts, input_filename = parse_cli(argv)
if opts.stack: developmode = True
grfstrings.read_extra_commands(opts.custom_tags)
generic.print_progress("Reading lang ...")
grfstrings.read_lang_files(opts.lang_dir, opts.default_lang)
generic.clear_progress()
# We have to do the dependency check first or we might later have
# more targets than we asked for
outputs = []
if opts.dep_check:
# First make sure we have a file to output the dependencies to:
dep_filename = opts.dep_filename
if dep_filename is None and opts.grf_filename is not None:
dep_filename = filename_output_from_input(opts.grf_filename, ".dep")
if dep_filename is None and input_filename is not None:
dep_filename = filename_output_from_input(input_filename, ".dep")
if dep_filename is None:
raise generic.ScriptError("-M requires a dependency file either via -MF, an input filename or a valid output via --grf")
# Now make sure we have a file which is the target for the dependencies:
depgrf_filename = opts.depgrf_filename
if depgrf_filename is None and opts.grf_filename is not None:
depgrf_filename = opts.grf_filename
if depgrf_filename is None and input_filename is not None:
depgrf_filename = filename_output_from_input(input_filename, ".grf")
if depgrf_filename is None:
raise generic.ScriptError("-M requires either a target grf file via -MT, an input filename or a valid output via --grf")
# Only append the dependency check to the output targets when we have both,
# a target grf and a file to write to
if dep_filename is not None and depgrf_filename is not None:
outputs.append(output_dep.OutputDEP(dep_filename, depgrf_filename))
if input_filename is None:
input = sys.stdin
else:
input = codecs.open(generic.find_file(input_filename), 'r', 'utf-8')
# Only append an output grf name, if no ouput is given, also not implicitly via -M
if not opts.outputfile_given and not outputs:
opts.grf_filename = filename_output_from_input(input_filename, ".grf")
# Translate the 'common' palette names as used by OpenTTD to the traditional ones being within NML's code
if opts.forced_palette == 'DOS':
opts.forced_palette = 'DEFAULT'
elif opts.forced_palette == 'WIN':
opts.forced_palette = 'LEGACY'
if opts.grf_filename: outputs.append(output_grf.OutputGRF(opts.grf_filename))
if opts.nfo_filename: outputs.append(output_nfo.OutputNFO(opts.nfo_filename, opts.start_sprite_num))
if opts.nml_filename: outputs.append(output_nml.OutputNML(opts.nml_filename))
for output in opts.outputs:
outroot, outext = os.path.splitext(output)
outext = outext.lower()
if outext == '.grf': outputs.append(output_grf.OutputGRF(output))
elif outext == '.nfo': outputs.append(output_nfo.OutputNFO(output, opts.start_sprite_num))
elif outext == '.nml': outputs.append(output_nml.OutputNML(output))
elif outext == '.dep': outputs.append(output_dep.OutputDEP(output, opts.grf_filename))
else:
generic.print_error("Unknown output format {}".format(outext))
sys.exit(2)
ret = nml(input, input_filename, opts.debug, outputs, opts.start_sprite_num, opts.compress, opts.crop, not opts.no_cache, opts.forced_palette, opts.md5_filename)
input.close()
sys.exit(ret)
def filename_output_from_input(name, ext):
return os.path.splitext(name)[0] + ext
def nml(inputfile, input_filename, output_debug, outputfiles, start_sprite_num, compress_grf, crop_sprites, enable_cache, forced_palette, md5_filename):
"""
Compile an NML file.
@param inputfile: File handle associated with the input file.
@type inputfile: C{File}
@param input_filename: Filename of the input file, C{None} if receiving from L{sys.stdin}
@type input_filename: C{str} or C{None}
@param outputfiles: Output streams to write to.
@type outputfiles: C{List} of L{output_base.OutputBase}
@param start_sprite_num: Number of the first sprite.
@type start_sprite_num: C{int}
@param compress_grf: Enable GRF sprite compression.
@type compress_grf: C{bool}
@param crop_sprites: Enable sprite cropping.
@type crop_sprites: C{bool}
@param enable_cache: Enable sprite cache.
@type enable_cache: C{bool}
@param forced_palette: Palette to use for the file.
@type forced_palette: C{str}
@param md5_filename: Filename to use for writing the md5 sum of the grf file. C{None} if the file should not be written.
@type md5_filename: C{str} or C{None}
"""
generic.OnlyOnce.clear()
generic.print_progress("Reading ...")
try:
script = inputfile.read()
except UnicodeDecodeError as ex:
raise generic.ScriptError('Input file is not utf-8 encoded: {}'.format(ex))
# Strip a possible BOM
script = script.lstrip(str(codecs.BOM_UTF8, "utf-8"))
if script.strip() == "":
generic.print_error("Empty input file")
return 4
generic.print_progress("Init parser ...")
nml_parser = parser.NMLParser()
if input_filename is None:
input_filename = 'input'
generic.print_progress("Parsing ...")
result = nml_parser.parse(script, input_filename)
result.validate([])
if output_debug > 0:
result.debug_print(0)
for outputfile in outputfiles:
if isinstance(outputfile, output_nml.OutputNML):
outputfile.open()
outputfile.write(str(result))
outputfile.close()
generic.print_progress("Preprocessing ...")
result.register_names()
result.pre_process()
tmp_actions = result.get_action_list()
generic.print_progress("Generating actions ...")
actions = []
for action in tmp_actions:
if isinstance(action, action1.SpritesetCollection):
actions.extend(action.get_action_list())
else:
actions.append(action)
actions.extend(action11.get_sound_actions())
generic.print_progress("Assigning Action2 registers ...")
action8_index = -1
for i in range(len(actions) - 1, -1, -1):
if isinstance(actions[i], (action2var.Action2Var, action2layout.Action2Layout)):
actions[i].resolve_tmp_storage()
elif isinstance(actions[i], action8.Action8):
action8_index = i
generic.print_progress("Generating strings ...")
if action8_index != -1:
lang_actions = []
# Add plural/gender/case tables
for lang_pair in grfstrings.langs:
lang_id, lang = lang_pair
lang_actions.extend(action0.get_language_translation_tables(lang))
# Add global strings
lang_actions.extend(action4.get_global_string_actions())
actions = actions[:action8_index + 1] + lang_actions + actions[action8_index + 1:]
generic.print_progress("Collecting real sprites ...")
# Collect all sprite files, and put them into buckets of same image and mask files
sprite_files = dict()
for action in actions:
if isinstance(action, real_sprite.RealSpriteAction):
for sprite in action.sprite_list:
if sprite.is_empty: continue
sprite.validate_size()
file = sprite.file
if file is not None:
file = file.value
mask_file = sprite.mask_file
if mask_file is not None:
mask_file = mask_file.value
key = (file, mask_file)
sprite_files.setdefault(key, []).append(sprite)
# Check whether we can terminate sprite processing prematurely for
# dependency checks
skip_sprite_processing = True
for outputfile in outputfiles:
if isinstance(outputfile, output_dep.OutputDEP):
outputfile.open()
for f in sprite_files:
if f[0] is not None:
outputfile.write(f[0])
if f[1] is not None:
outputfile.write(f[1])
outputfile.close()
skip_sprite_processing &= outputfile.skip_sprite_checks()
if skip_sprite_processing:
generic.clear_progress()
return 0
if not Image and len(sprite_files) > 0:
generic.print_error("PIL (python-imaging) wasn't found, no support for using graphics")
sys.exit(3)
generic.print_progress("Checking palette of source images ...")
used_palette = forced_palette
last_file = None
for f_pair in sprite_files:
# Palette is defined by mask_file, if present. Otherwise by the main file.
f = f_pair[1]
if f is None:
f = f_pair[0]
try:
im = Image.open(generic.find_file(f))
except IOError as ex:
raise generic.ImageError(str(ex), f)
if im.mode != "P":
continue
pal = palette.validate_palette(im, f)
if forced_palette != "ANY" and pal != forced_palette and not (forced_palette == "DEFAULT" and pal == "LEGACY"):
raise generic.ImageError("Image has '{}' palette, but you forced the '{}' palette".format(pal, used_palette), f)
if used_palette == "ANY":
used_palette = pal
elif pal != used_palette:
if used_palette in ("LEGACY", "DEFAULT") and pal in ("LEGACY", "DEFAULT"):
used_palette = "DEFAULT"
else:
raise generic.ImageError("Image has '{}' palette, but \"{}\" has the '{}' palette".format(pal, last_file, used_palette), f)
last_file = f
palette_bytes = {"LEGACY": "W", "DEFAULT": "D", "ANY": "A"}
if used_palette in palette_bytes:
grf.set_palette_used(palette_bytes[used_palette])
encoder = None
for outputfile in outputfiles:
outputfile.palette = used_palette # used by RecolourSpriteAction
if isinstance(outputfile, output_grf.OutputGRF):
if encoder is None:
encoder = spriteencoder.SpriteEncoder(compress_grf, crop_sprites, enable_cache, used_palette)
outputfile.encoder = encoder
generic.clear_progress()
# Read all image data, compress, and store in sprite cache
if encoder is not None:
encoder.open(sprite_files)
#If there are any 32bpp sprites hint to openttd that we'd like a 32bpp blitter
if alt_sprites.any_32bpp_sprites:
grf.set_preferred_blitter("3")
generic.print_progress("Linking actions ...")
if action8_index != -1:
actions = [sprite_count.SpriteCountAction(len(actions))] + actions
for idx, action in enumerate(actions):
num = start_sprite_num + idx
action.prepare_output(num)
# Processing finished, print some statistics
action0.print_stats()
actionF.print_stats()
action7.print_stats()
action1.print_stats()
action2.print_stats()
action6.print_stats()
grf.print_stats()
global_constants.print_stats()
action4.print_stats()
action11.print_stats()
generic.print_progress("Writing output ...")
md5 = None
for outputfile in outputfiles:
if isinstance(outputfile, output_grf.OutputGRF):
outputfile.open()
for action in actions:
action.write(outputfile)
outputfile.close()
md5 = outputfile.get_md5()
if isinstance(outputfile, output_nfo.OutputNFO):
outputfile.open()
for action in actions:
action.write(outputfile)
outputfile.close()
if md5 is not None and md5_filename is not None:
with open(md5_filename, 'w', encoding="utf-8") as f:
f.write(md5 + '\n')
if encoder is not None:
encoder.close()
generic.clear_progress()
return 0
def run():
try:
main(sys.argv[1:])
except generic.ScriptError as ex:
generic.print_error(str(ex))
if developmode: raise # Reraise exception in developmode
sys.exit(1)
except SystemExit as ex:
raise
except KeyboardInterrupt as ex:
generic.print_error('Application forcibly terminated by user.')
if developmode: raise # Reraise exception in developmode
sys.exit(1)
except Exception as ex: # Other/internal error.
if developmode: raise # Reraise exception in developmode
# User mode: print user friendly error message.
ex_msg = str(ex)
if len(ex_msg) > 0: ex_msg = '"{}"'.format(ex_msg)
traceback = sys.exc_info()[2]
# Walk through the traceback object until we get to the point where the exception happened.
while traceback.tb_next is not None:
traceback = traceback.tb_next
lineno = traceback.tb_lineno
frame = traceback.tb_frame
code = frame.f_code
filename = code.co_filename
name = code.co_name
del traceback # Required according to Python docs.
ex_data = {'class' : ex.__class__.__name__,
'version' : version,
'msg' : ex_msg,
'cli' : sys.argv,
'loc' : 'File "{}", line {:d}, in {}'.format(filename, lineno, name)}
msg = "nmlc: An internal error has occurred:\n" \
"nmlc-version: {version}\n" \
"Error: ({class}) {msg}.\n" \
"Command: {cli}\n" \
"Location: {loc}\n".format(**ex_data)
generic.print_error(msg)
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
run()
nml-0.4.5/nml/spriteencoder.py 0000644 0005672 0005672 00000051342 13315644406 017471 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import array
from nml import generic, palette, lz77, spritecache
from nml.actions import real_sprite
try:
from PIL import Image
except ImportError:
try:
import Image
except ImportError:
pass
# Some constants for the 'info' byte
INFO_RGB = 1
INFO_ALPHA = 2
INFO_PAL = 4
INFO_TILE = 8
INFO_NOCROP = 0x40
def get_bpp(info):
bpp = 0
if (info & INFO_RGB) != 0: bpp += 3
if (info & INFO_ALPHA) != 0: bpp += 1
if (info & INFO_PAL) != 0: bpp += 1
return bpp
def has_transparency(info):
return (info & (INFO_ALPHA | INFO_PAL)) != 0
def transparency_offset(info):
"""
Determine which byte within a pixel should be 0 for a pixel to be transparent
"""
assert has_transparency(info)
# There is an alpha or palette component, or both, else no transparency at all
# Pixel is transparent if either:
# - alpha channel present and 0
# - palette present and 0, and alpha not present
# In either case, the pixel is transparent,
# if byte 3 (with RGB present) or byte 0 (without RGB) is 0
if (info & INFO_RGB) != 0:
return 3
else:
return 0
class SpriteEncoder(object):
"""
Algorithms for cropping and compressing sprites. That is encoding source images into GRF sprites.
@ivar compress_grf: Compress sprites.
@type compress_grf: C{bool}
@ivar crop_sprites: Crop sprites if possible.
@type crop_sprites: C{bool}
@ivar enable_cache: Read/write cache from/to disk.
@type enable_cache: C{bool}
@ivar palette: Palette for encoding, see L{palette.palette_name}.
@type palette: C{str}
@ivar cached_image_files: Currently opened source image files.
@type cached_image_files: C{dict} mapping C{str} to C{Image}
"""
def __init__(self, compress_grf, crop_sprites, enable_cache, palette):
self.compress_grf = compress_grf
self.crop_sprites = crop_sprites
self.enable_cache = enable_cache
self.palette = palette
self.sprite_cache = spritecache.SpriteCache("")
self.cached_image_files = {}
def open(self, sprite_files):
"""
Start the encoder, read caches, and stuff.
@param sprite_files: List of sprites per source image file.
@type sprite_files: C{dict} that maps (C{tuple} of C{str}) to (C{RealSprite})
"""
num_sprites = sum(len(sprite_list) for sprite_list in sprite_files.values())
generic.print_progress("Encoding ...")
num_cached = 0
num_dup = 0
num_enc = 0
num_orphaned = 0
count_sprites = 0
for sources, sprite_list in sprite_files.items():
# Iterate over sprites grouped by source image file.
# - Open source files only once. (speed)
# - Do not keep files around for long. (memory)
source_name = "_".join(src for src in sources if src is not None)
local_cache = spritecache.SpriteCache(generic.get_cache_file(sources, ""))
if self.enable_cache:
local_cache.read_cache()
for sprite_info in sprite_list:
count_sprites += 1
generic.print_progress("Encoding {}/{}: {}".format(count_sprites, num_sprites, source_name), incremental = True)
cache_key = sprite_info.get_cache_key(self.crop_sprites)
cache_item = local_cache.get_item(cache_key, self.palette)
in_use = False
in_old_cache = False
if cache_item is not None:
# Write a sprite from the cached data
compressed_data, info_byte, crop_rect, pixel_stats, in_old_cache, in_use = cache_item
if in_use:
num_dup += 1
else:
num_cached += 1
else:
size_x, size_y, xoffset, yoffset, compressed_data, info_byte, crop_rect, pixel_stats = self.encode_sprite(sprite_info)
num_enc += 1
# Store sprite in cache, unless already up-to-date
if not in_use:
cache_item = (compressed_data, info_byte, crop_rect, pixel_stats, in_old_cache, True)
local_cache.add_item(cache_key, self.palette, cache_item)
# Delete all files from dictionary to free memory
self.cached_image_files.clear()
num_orphaned += local_cache.count_orphaned()
# Only write cache if compression is enabled. Uncompressed data is not worth to be cached.
if self.enable_cache and self.compress_grf:
local_cache.write_cache()
# Transfer data to global cache for later usage
self.sprite_cache.cached_sprites.update(local_cache.cached_sprites)
generic.print_progress("Encoding ...", incremental = True)
generic.clear_progress()
generic.print_info("{} sprites, {} cached, {} orphaned, {} duplicates, {} newly encoded ({})".format(num_sprites, num_cached, num_orphaned, num_dup, num_enc, "native" if lz77.is_native else "python"))
def close(self):
"""
Close the encoder, validate data, write caches, and stuff.
"""
pass
def get(self, sprite_info):
"""
Get encoded sprite date, either from cache, or new.
@param sprite_info: Sprite meta data
@type sprite_info: C{RealSprite}
"""
# Try finding the file in the cache
# open() should have put all sprites into it.
cache_key = sprite_info.get_cache_key(self.crop_sprites)
cache_item = self.sprite_cache.get_item(cache_key, self.palette)
assert cache_item is not None
compressed_data, info_byte, crop_rect, pixel_stats, in_old_cache, in_use = cache_item
size_x = sprite_info.xsize.value
size_y = sprite_info.ysize.value
xoffset = sprite_info.xrel.value
yoffset = sprite_info.yrel.value
if cache_key[-1]: size_x, size_y, xoffset, yoffset = self.recompute_offsets(size_x, size_y, xoffset, yoffset, crop_rect)
warnings = []
total = pixel_stats.get('total', 0)
if total > 0:
if cache_key[0] is not None:
image_32_pos = generic.PixelPosition(cache_key[0], cache_key[1][0], cache_key[1][1])
alpha = pixel_stats.get('alpha', 0)
if alpha > 0 and (sprite_info.flags.value & real_sprite.FLAG_NOALPHA) != 0:
warnings.append("{}: {:d} of {:d} pixels ({:d}%) are semi-transparent".format(str(image_32_pos), alpha, total, alpha * 100 // total))
if cache_key[2] is not None:
image_8_pos = generic.PixelPosition(cache_key[2], cache_key[3][0], cache_key[3][1])
white = pixel_stats.get('white', 0)
anim = pixel_stats.get('anim', 0)
if white > 0 and (sprite_info.flags.value & real_sprite.FLAG_WHITE) == 0:
warnings.append("{}: {:d} of {:d} pixels ({:d}%) are pure white".format(str(image_8_pos), white, total, white * 100 // total))
if anim > 0 and (sprite_info.flags.value & real_sprite.FLAG_ANIM) == 0:
warnings.append("{}: {:d} of {:d} pixels ({:d}%) are animated".format(str(image_8_pos), anim, total, anim * 100 // total))
return (size_x, size_y, xoffset, yoffset, compressed_data, info_byte, crop_rect, warnings)
def open_image_file(self, filename):
"""
Obtain a handle to an image file
@param filename: Name of the file
@type filename: C{str}
@return: Image file
@rtype: L{Image}
"""
if filename in self.cached_image_files:
im = self.cached_image_files[filename]
else:
im = Image.open(generic.find_file(filename))
self.cached_image_files[filename] = im
return im
def encode_sprite(self, sprite_info):
"""
Crop and compress a real sprite.
@param sprite_info: Sprite meta data
@type sprite_info: C{RealSprite}
@return: size_x, size_y, xoffset, yoffset, compressed_data, info_byte, crop_rect, pixel_stats
@rtype: C{tuple}
"""
filename_8bpp = None
filename_32bpp = None
if sprite_info.bit_depth == 8:
filename_8bpp = sprite_info.file
else:
filename_32bpp = sprite_info.file
filename_8bpp = sprite_info.mask_file
# Get initial info_byte and dimensions from sprite_info.
# These values will be changed depending on cropping and compression.
info_byte = INFO_NOCROP if (sprite_info.flags.value & real_sprite.FLAG_NOCROP) != 0 else 0
size_x = sprite_info.xsize.value
size_y = sprite_info.ysize.value
xoffset = sprite_info.xrel.value
yoffset = sprite_info.yrel.value
im, mask_im = None, None
im_mask_pal = None
# Select region of image bounded by x/ypos and x/ysize
x = sprite_info.xpos.value
y = sprite_info.ypos.value
if sprite_info.bit_depth == 8 or sprite_info.mask_pos is None:
mask_x, mask_y = x, y
else:
mask_x = sprite_info.mask_pos[0].value
mask_y = sprite_info.mask_pos[1].value
pixel_stats = { 'total': size_x * size_y, 'alpha': 0, 'white': 0, 'anim': 0 }
# Read and validate image data
if filename_32bpp is not None:
im = self.open_image_file(filename_32bpp.value)
if im.mode not in ("RGB", "RGBA"):
pos = generic.build_position(sprite_info.poslist)
raise generic.ImageError("32bpp image is not a full colour RGB(A) image.", filename_32bpp.value, pos)
info_byte |= INFO_RGB
if im.mode == "RGBA":
info_byte |= INFO_ALPHA
(im_width, im_height) = im.size
if x < 0 or y < 0 or x + size_x > im_width or y + size_y > im_height:
pos = generic.build_position(sprite_info.poslist)
raise generic.ScriptError("Read beyond bounds of image file '{}'".format(filename_32bpp.value), pos)
sprite = im.crop((x, y, x + size_x, y + size_y))
rgb_sprite_data = sprite.tobytes()
if (info_byte & INFO_ALPHA) != 0:
# Check for half-transparent pixels (not valid for ground sprites)
pixel_stats['alpha'] = sum(0x00 < p < 0xFF for p in rgb_sprite_data[3::4])
if filename_8bpp is not None:
mask_im = self.open_image_file(filename_8bpp.value)
if mask_im.mode != "P":
pos = generic.build_position(sprite_info.poslist)
raise generic.ImageError("8bpp image does not have a palette", filename_8bpp.value, pos)
im_mask_pal = palette.validate_palette(mask_im, filename_8bpp.value)
info_byte |= INFO_PAL
(im_width, im_height) = mask_im.size
if mask_x < 0 or mask_y < 0 or mask_x + size_x > im_width or mask_y + size_y > im_height:
pos = generic.build_position(sprite_info.poslist)
raise generic.ScriptError("Read beyond bounds of image file '{}'".format(filename_8bpp.value), pos)
mask_sprite = mask_im.crop((mask_x, mask_y, mask_x + size_x, mask_y + size_y))
mask_sprite_data = self.palconvert(mask_sprite.tobytes(), im_mask_pal)
# Check for white pixels; those that cause "artefacts" when shading
pixel_stats['white'] = sum(p == 255 for p in mask_sprite_data)
# Check for palette animation colours
if self.palette == 'DEFAULT':
pixel_stats['anim'] = sum(0xE3 <= p <= 0xFE for p in mask_sprite_data)
else:
pixel_stats['anim'] = sum(0xD9 <= p <= 0xF4 for p in mask_sprite_data)
# Compose pixel information in an array of bytes
sprite_data = array.array('B')
if (info_byte & INFO_RGB) != 0 and (info_byte & INFO_PAL) != 0:
mask_data = array.array('B', mask_sprite_data) # Convert to numeric
rgb_data = array.array('B', rgb_sprite_data)
if (info_byte & INFO_ALPHA) != 0:
for i in range(len(mask_sprite_data)):
sprite_data.extend(rgb_data[4*i:4*(i+1)])
sprite_data.append(mask_data[i])
else:
for i in range(len(mask_sprite_data)):
sprite_data.extend(rgb_data[3*i:3*(i+1)])
sprite_data.append(mask_data[i])
elif (info_byte & INFO_RGB) != 0:
sprite_data.frombytes(rgb_sprite_data)
else:
sprite_data.frombytes(mask_sprite_data)
bpp = get_bpp(info_byte)
assert len(sprite_data) == size_x * size_y * bpp
if self.crop_sprites and ((info_byte & INFO_NOCROP) == 0):
sprite_data, crop_rect = self.crop_sprite(sprite_data, size_x, size_y, info_byte, bpp)
size_x, size_y, xoffset, yoffset = self.recompute_offsets(size_x, size_y, xoffset, yoffset, crop_rect)
else:
crop_rect = None
assert len(sprite_data) == size_x * size_y * bpp
compressed_data = self.sprite_compress(sprite_data)
# Try tile compression, and see if it results in a smaller file size
tile_data = self.sprite_encode_tile(size_x, size_y, sprite_data, info_byte, bpp)
if tile_data is not None:
tile_compressed_data = self.sprite_compress(tile_data)
# Tile compression adds another 4 bytes for the uncompressed chunked data in the header
if len(tile_compressed_data) + 4 < len(compressed_data):
info_byte |= INFO_TILE
data_len = len(tile_data)
compressed_data = array.array('B')
compressed_data.append(data_len & 0xFF)
compressed_data.append((data_len >> 8) & 0xFF)
compressed_data.append((data_len >> 16) & 0xFF)
compressed_data.append((data_len >> 24) & 0xFF)
compressed_data.extend(tile_compressed_data)
return (size_x, size_y, xoffset, yoffset, compressed_data, info_byte, crop_rect, pixel_stats)
def fakecompress(self, data):
i = 0
output = array.array('B')
length = len(data)
while i < length:
n = min(length - i, 127)
output.append(n)
output.extend(data[i:i+n])
i += n
return output
def sprite_compress(self, data):
if self.compress_grf:
stream = lz77.encode(data)
else:
stream = self.fakecompress(data)
return stream
def sprite_encode_tile(self, size_x, size_y, data, info, bpp, long_format = False):
long_chunk = size_x > 256
# There are basically four different encoding configurations here,
# but just two variables. If the width of the sprite is more than
# 256, then the chunk could be 'out of bounds' and thus the long
# chunk format is used. If the sprite is more than 65536 bytes,
# then the offsets might not fit and the long format method is
# used. The latter is enabled via recursion if it's needed.
if not has_transparency(info):
return None
trans_offset = transparency_offset(info)
max_chunk_len = 0x7fff if long_chunk else 0x7f
line_offset_size = 4 if long_format else 2 # Whether to use 2 or 4 bytes in the list of line offsets
output = array.array('B', [0] * (line_offset_size * size_y))
for y in range(size_y):
# Write offset in the correct place, in little-endian format
offset = len(output)
output[y*line_offset_size] = offset & 0xFF
output[y*line_offset_size + 1] = (offset >> 8) & 0xFF
if long_format:
output[y*line_offset_size + 2] = (offset >> 16) & 0xFF
output[y*line_offset_size + 3] = (offset >> 24) & 0xFF
line_start = y*size_x*bpp
line_parts = []
x1 = 0
while True:
# Skip transparent pixels
while x1 < size_x and data[line_start + x1*bpp + trans_offset] == 0:
x1 += 1
if x1 == size_x:
# End-of-line reached
break
# Grab as many non-transparent pixels as possible, but not without x2-x1 going out of bounds
# Only stop the chunk when encountering 3 consecutive transparent pixels
x2 = x1 + 1
while x2 - x1 < max_chunk_len and (
(x2 < size_x and data[line_start + x2*bpp + trans_offset] != 0) or
(x2 + 1 < size_x and data[line_start + (x2+1)*bpp + trans_offset] != 0) or
(x2 + 2 < size_x and data[line_start + (x2+2)*bpp + trans_offset] != 0)):
x2 += 1
line_parts.append((x1, x2))
x1 = x2
if len(line_parts) == 0:
# Completely transparent line
if long_chunk:
output.extend((0, 0x80, 0, 0))
else:
output.extend((0x80, 0))
continue
for idx, part in enumerate(line_parts):
x1, x2 = part
last_mask = 0x80 if idx == len(line_parts) - 1 else 0
chunk_len = x2 - x1
if long_chunk:
output.extend((chunk_len & 0xFF,
(chunk_len >> 8) | last_mask,
x1 & 0xFF,
x1 >> 8))
else:
output.extend((chunk_len | last_mask, x1))
output.extend(data[line_start + x1*bpp : line_start + x2*bpp])
if len(output) > 65535 and not long_format:
# Recurse into the long format if that's possible.
return self.sprite_encode_tile(size_x, size_y, data, info, bpp, True)
return output
def recompute_offsets(self, size_x, size_y, xoffset, yoffset, crop_rect):
# Recompute sizes and offsets after cropping a sprite
left, right, top, bottom = crop_rect
size_x -= left + right
size_y -= top + bottom
xoffset += left
yoffset += top
return size_x, size_y, xoffset, yoffset
def crop_sprite(self, data, size_x, size_y, info, bpp):
left, right, top, bottom = 0, 0, 0, 0
if not has_transparency(info):
return (data, (left, right, top, bottom))
trans_offset = transparency_offset(info)
line_size = size_x * bpp # size (no. of bytes) of a scan line
data_size = len(data)
#Crop the top of the sprite
while size_y > 1 and not any(data[line_size * top + trans_offset : line_size * (top+1) : bpp]):
top += 1
size_y -= 1
#Crop the bottom of the sprite
while size_y > 1 and not any(data[data_size - line_size * (bottom+1) + trans_offset : data_size - line_size * bottom : bpp]):
# Don't use negative indexing, it breaks for the last line (where you'd need index 0)
bottom += 1
size_y -= 1
#Modify data by removing top/bottom
data = data[line_size * top : data_size - line_size * bottom]
#Crop the left of the sprite
while size_x > 1 and not any(data[left * bpp + trans_offset : : line_size]):
left += 1
size_x -= 1
#Crop the right of the sprite
while size_x > 1 and not any(data[line_size - (right+1) * bpp + trans_offset : : line_size]):
right += 1
size_x -= 1
#Removing left/right data is not easily done by slicing
#Best to create a new array
if left + right > 0:
new_data = array.array('B')
for y in range(0, size_y):
a = data[y*line_size + left*bpp : (y+1)*line_size - right*bpp]
new_data.extend(a)
data = new_data
return (data, (left, right, top, bottom))
def palconvert(self, sprite_str, orig_pal):
if orig_pal == "LEGACY" and self.palette == "DEFAULT":
return sprite_str.translate(real_sprite.translate_w2d)
else:
return sprite_str
nml-0.4.5/nml/output_grf.py 0000644 0005672 0005672 00000016551 13315644406 017024 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import hashlib, os
from nml import generic, output_base, grfstrings, spriteencoder
class OutputGRF(output_base.BinaryOutputBase):
def __init__(self, filename):
output_base.BinaryOutputBase.__init__(self, filename)
self.encoder = None
self.sprite_output = output_base.BinaryOutputBase(filename + ".sprite.tmp")
self.md5 = hashlib.md5()
# sprite_num is deliberately off-by-one because it is used as an
# id between data and sprite section. For the sprite section an id
# of 0 is invalid (means end of sprites), and for a non-NewGRF GRF
# the first sprite is a real sprite.
self.sprite_num = 1
def open_file(self):
# Remove / unlink the file, most useful for linux systems
# See also issue #4165
# If the file happens to be in use or non-existant, ignore
try:
os.unlink(self.filename)
except OSError:
pass
return open(self.filename, 'wb')
def get_md5(self):
return self.md5.hexdigest()
def assemble_file(self, real_file):
#add end-of-chunks
self.in_sprite = True
self.print_dword(0)
self.in_sprite = False
self.sprite_output.in_sprite = True
self.sprite_output.print_dword(0)
self.sprite_output.in_sprite = False
#add header
header = bytearray([0x00, 0x00, ord('G'), ord('R'), ord('F'), 0x82, 0x0D, 0x0A, 0x1A, 0x0A])
size = len(self.file) + 1
header.append(size & 0xFF)
header.append((size >> 8) & 0xFF)
header.append((size >> 16) & 0xFF)
header.append(size >> 24)
header.append(0) #no compression
header_str = bytes(header)
real_file.write(header_str)
self.md5.update(header_str)
#add data section, and then the sprite section
real_file.write(self.file)
self.md5.update(self.file)
real_file.write(self.sprite_output.file)
def open(self):
output_base.BinaryOutputBase.open(self)
self.sprite_output.open()
def close(self):
output_base.BinaryOutputBase.close(self)
self.sprite_output.discard()
def _print_utf8(self, char, stream):
for c in chr(char).encode('utf8'):
stream.print_byte(c)
def print_string(self, value, final_zero = True, force_ascii = False, stream = None):
if stream is None:
stream = self
if not grfstrings.is_ascii_string(value):
if force_ascii:
raise generic.ScriptError("Expected ascii string but got a unicode string")
stream.print_byte(0xC3)
stream.print_byte(0x9E)
i = 0
while i < len(value):
if value[i] == '\\':
if value[i+1] in ('\\', '"'):
stream.print_byte(ord(value[i+1]))
i += 2
elif value[i+1] == 'U':
self._print_utf8(int(value[i+2:i+6], 16), stream)
i += 6
else:
stream.print_byte(int(value[i+1:i+3], 16))
i += 3
else:
self._print_utf8(ord(value[i]), stream)
i += 1
if final_zero: stream.print_byte(0)
def comment(self, msg):
pass
def start_sprite(self, size, type = 0xFF):
if type == 0xFF:
output_base.BinaryOutputBase.start_sprite(self, size + 5)
self.print_dword(size)
self.print_byte(type)
elif type == 0xFD:
# Real sprite, this means no data is written to the data section
# This call is still needed to open 'output mode'
assert size == 0
output_base.BinaryOutputBase.start_sprite(self, 9)
self.print_dword(4)
self.print_byte(0xfd)
self.print_dword(self.sprite_num)
else:
assert False, "Unexpected info byte encountered."
def print_sprite(self, sprite_list):
"""
@param sprite_list: List of non-empty real sprites for various bit depths / zoom levels
@type sprite_list: C{list} of L{RealSprite}
"""
self.start_sprite(0, 0xFD)
for sprite in sprite_list:
self.print_single_sprite(sprite)
self.end_sprite()
def print_single_sprite(self, sprite_info):
assert sprite_info.file is not None or sprite_info.mask_file is not None
# Position for warning messages
pos_warning = None
if sprite_info.mask_file is not None:
pos_warning = sprite_info.mask_file.pos
elif sprite_info.file is not None:
pos_warning = sprite_info.file.pos
size_x, size_y, xoffset, yoffset, compressed_data, info_byte, crop_rect, warnings = self.encoder.get(sprite_info)
for w in warnings:
generic.print_warning(w, pos_warning)
self.sprite_output.start_sprite(len(compressed_data) + 18)
self.wsprite_header(size_x, size_y, len(compressed_data), xoffset, yoffset, info_byte, sprite_info.zoom_level)
self.sprite_output.print_data(compressed_data)
self.sprite_output.end_sprite()
def print_empty_realsprite(self):
self.start_sprite(1)
self.print_byte(0)
self.end_sprite()
def wsprite_header(self, size_x, size_y, size, xoffset, yoffset, info, zoom_level):
self.sprite_output.print_dword(self.sprite_num)
self.sprite_output.print_dword(size + 10)
self.sprite_output.print_byte(info)
self.sprite_output.print_byte(zoom_level)
self.sprite_output.print_word(size_y)
self.sprite_output.print_word(size_x)
self.sprite_output.print_word(xoffset)
self.sprite_output.print_word(yoffset)
def print_named_filedata(self, filename):
name = os.path.split(filename)[1]
size = os.path.getsize(filename)
self.start_sprite(0, 0xfd)
self.sprite_output.start_sprite(8 + 3 + len(name) + 1 + size)
self.sprite_output.print_dword(self.sprite_num)
self.sprite_output.print_dword(3 + len(name) + 1 + size)
self.sprite_output.print_byte(0xff)
self.sprite_output.print_byte(0xff)
self.sprite_output.print_byte(len(name))
self.print_string(name, force_ascii = True, final_zero = True, stream = self.sprite_output) # ASCII filenames seems sufficient.
fp = open(generic.find_file(filename), 'rb')
while True:
data = fp.read(1024)
if len(data) == 0: break
for d in data:
self.sprite_output.print_byte(d)
fp.close()
self.sprite_output.end_sprite();
self.end_sprite()
def end_sprite(self):
output_base.BinaryOutputBase.end_sprite(self)
self.sprite_num += 1
nml-0.4.5/nml/lz77.py 0000644 0005672 0005672 00000005130 13315644406 015420 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import array
def _encode(data):
"""
GRF compression algorithm.
@param data: Uncompressed data.
@type data: C{str}, C{bytearray}, C{bytes} or similar.
@return: Compressed data.
@rtype: C{bytearray}
"""
stream = data.tostring()
position = 0
output = array.array('B')
literal_bytes = array.array('B')
stream_len = len(stream)
while position < stream_len:
overlap_len = 0
start_pos = max(0, position - (1 << 11) + 1)
# Loop through the lookahead buffer.
for i in range(3, min(stream_len - position + 1, 16)):
# Set pattern to find the longest match.
pattern = stream[position:position+i]
# Find the pattern match in the window.
result = stream.find(pattern, start_pos, position)
# If match failed, we've found the longest.
if result < 0: break
p = position - result
overlap_len = i
start_pos = result
if overlap_len > 0:
if len(literal_bytes) > 0:
output.append(len(literal_bytes))
output.extend(literal_bytes)
literal_bytes = array.array('B')
val = ((-overlap_len) << 3) & 0xFF | (p >> 8)
output.append(val)
output.append(p & 0xFF)
position += overlap_len
else:
literal_bytes.append(stream[position])
if len(literal_bytes) == 0x80:
output.append(0)
output.extend(literal_bytes)
literal_bytes = array.array('B')
position += 1
if len(literal_bytes) > 0:
output.append(len(literal_bytes))
output.extend(literal_bytes)
return output
"""
True if the encoding is provided by a native module.
Used for verbose information.
"""
is_native = False
try:
from nml_lz77 import encode
is_native = True
except ImportError:
encode = _encode
nml-0.4.5/nml/global_constants.py 0000644 0005672 0005672 00000155404 13315644406 020163 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, nmlop
constant_numbers = {
#climates
'CLIMATE_TEMPERATE' : 0,
'CLIMATE_ARCTIC' : 1,
'CLIMATE_TROPIC' : 2,
'CLIMATE_TROPICAL' : 2,
'CLIMATE_TOYLAND' : 3,
'ABOVE_SNOWLINE' : 11, # Only for house property available_mask
'NO_CLIMATE' : 0x00,
'ALL_CLIMATES' : 0x0F,
#never expire
'VEHICLE_NEVER_EXPIRES' : 0xFF,
#default_cargo
'DEFAULT_CARGO_FIRST_REFITTABLE' : 0xFF,
#cargo classes
'CC_PASSENGERS' : 0,
'CC_MAIL' : 1,
'CC_EXPRESS' : 2,
'CC_ARMOURED' : 3,
'CC_BULK' : 4,
'CC_PIECE_GOODS' : 5,
'CC_LIQUID' : 6,
'CC_REFRIGERATED' : 7,
'CC_HAZARDOUS' : 8,
'CC_COVERED' : 9,
'CC_OVERSIZED' : 10,
'CC_POWDERIZED' : 11,
'CC_NON_POURABLE' : 12,
'CC_NEO_BULK' : 12,
'CC_SPECIAL' : 15,
'NO_CARGO_CLASS' : 0,
'ALL_NORMAL_CARGO_CLASSES' : 0x1FFF,
'ALL_CARGO_CLASSES' : 0x9FFF,
#engine class
'ENGINE_CLASS_STEAM' : 0x00,
'ENGINE_CLASS_DIESEL' : 0x08,
'ENGINE_CLASS_ELECTRIC' : 0x28,
'ENGINE_CLASS_MONORAIL' : 0x32,
'ENGINE_CLASS_MAGLEV' : 0x38,
#running costs
'RUNNING_COST_STEAM' : 0x4C30,
'RUNNING_COST_DIESEL' : 0x4C36,
'RUNNING_COST_ELECTRIC' : 0x4C3C,
'RUNNING_COST_ROADVEH' : 0x4C48,
'RUNNING_COST_NONE' : 0x0000,
#shorten factor
'SHORTEN_TO_8_8' : 0x00,
'SHORTEN_TO_7_8' : 0x01,
'SHORTEN_TO_6_8' : 0x02,
'SHORTEN_TO_5_8' : 0x03,
'SHORTEN_TO_4_8' : 0x04,
'SHORTEN_TO_3_8' : 0x05,
'SHORTEN_TO_2_8' : 0x06,
'SHORTEN_TO_1_8' : 0x07,
#vehicle length
'VEHICLE_LENGTH' : 8,
#visual effect
'VISUAL_EFFECT_DEFAULT' : 0x00,
'VISUAL_EFFECT_STEAM' : 0x10,
'VISUAL_EFFECT_DIESEL' : 0x20,
'VISUAL_EFFECT_ELECTRIC' : 0x30,
'VISUAL_EFFECT_DISABLE' : 0x40,
#effect spawn model
'EFFECT_SPAWN_MODEL_NONE' : 0x40,
'EFFECT_SPAWN_MODEL_STEAM' : 0x41,
'EFFECT_SPAWN_MODEL_DIESEL' : 0x42,
'EFFECT_SPAWN_MODEL_ELECTRIC': 0x43,
#visual effect / effect spawn model
'ENABLE_WAGON_POWER' : 0x00,
'DISABLE_WAGON_POWER' : 0x80,
#create_effect
'EFFECT_SPRITE_STEAM' : 0xF1,
'EFFECT_SPRITE_DIESEL' : 0xF2,
'EFFECT_SPRITE_ELECTRIC' : 0xF3,
'EFFECT_SPRITE_AIRCRAFT_BREAKDOWN_SMOKE' : 0xFA,
'EFFECT_SPRITE_NONE' : 0xFF,
'CB_RESULT_CREATE_EFFECT_CENTER' : 0x2000,
'CB_RESULT_CREATE_EFFECT_NO_ROTATION' : 0x4000,
#train misc flags
'TRAIN_FLAG_TILT' : 0,
'TRAIN_FLAG_2CC' : 1,
'TRAIN_FLAG_MU' : 2,
'TRAIN_FLAG_FLIP' : 3,
'TRAIN_FLAG_AUTOREFIT': 4,
'TRAIN_FLAG_NO_BREAKDOWN_SMOKE': 6,
'TRAIN_FLAG_SPRITE_STACK': 7,
#roadveh misc flags
'ROADVEH_FLAG_TRAM' : 0,
'ROADVEH_FLAG_2CC' : 1,
'ROADVEH_FLAG_AUTOREFIT': 4,
'ROADVEH_FLAG_NO_BREAKDOWN_SMOKE': 6,
'ROADVEH_FLAG_SPRITE_STACK': 7,
#ship misc flags
'SHIP_FLAG_2CC' : 1,
'SHIP_FLAG_AUTOREFIT': 4,
'SHIP_FLAG_NO_BREAKDOWN_SMOKE': 6,
'SHIP_FLAG_SPRITE_STACK': 7,
#aircrafts misc flags
'AIRCRAFT_FLAG_2CC' : 1,
'AIRCRAFT_FLAG_AUTOREFIT': 4,
'AIRCRAFT_FLAG_NO_BREAKDOWN_SMOKE': 6,
'AIRCRAFT_FLAG_SPRITE_STACK': 7,
#for those, who can't tell the difference between a train and an aircraft:
'VEHICLE_FLAG_2CC' : 1,
'VEHICLE_FLAG_AUTOREFIT': 4,
'VEHICLE_FLAG_NO_BREAKDOWN_SMOKE': 6,
#Graphic flags for waterfeatures
'WATERFEATURE_ALTERNATIVE_SPRITES' : 0,
#IDs for waterfeatures
'WF_WATERCLIFFS' : 0x00,
'WF_LOCKS' : 0x01,
'WF_DIKES' : 0x02,
'WF_CANAL_GUI' : 0x03,
'WF_FLAT_DOCKS' : 0x04,
'WF_RIVER_SLOPE' : 0x05,
'WF_RIVERBANKS' : 0x06,
'WF_RIVER_GUI' : 0x07,
'WF_BUOY' : 0x08,
#ai flags
'AI_FLAG_PASSENGER' : 0x01,
'AI_FLAG_CARGO' : 0x00,
# Callback results
'CB_RESULT_ATTACH_ALLOW_IF_RAILTYPES' : 0x400,
'CB_RESULT_ATTACH_ALLOW' : 0x401,
'CB_RESULT_ATTACH_DISALLOW' : 0x402,
'CB_RESULT_NO_MORE_ARTICULATED_PARTS' : 0x7FFF,
'CB_RESULT_REVERSED_VEHICLE' : 0x4000,
'CB_RESULT_32_DAYS_TRIGGER' : 0,
'CB_RESULT_32_DAYS_COLOUR_MAPPING' : 1,
'CB_RESULT_COLOUR_MAPPING_ADD_CC' : 0x4000,
'CB_RESULT_AUTOREFIT' : 0x4000,
'CB_RESULT_REFIT_COST_MASK' : 0x3FFF,
'CB_RESULT_NO_SOUND' : 0x7EFF, # Never a valid sound id
# Callback results in registers
'CB_FLAG_MORE_SPRITES' : 0x80000000,
# 1-based, not 0-based
'SOUND_EVENT_START' : 1,
'SOUND_EVENT_TUNNEL' : 2,
'SOUND_EVENT_BREAKDOWN' : 3,
'SOUND_EVENT_RUNNING' : 4,
'SOUND_EVENT_TOUCHDOWN' : 5,
'SOUND_EVENT_VISUAL_EFFECT' : 6,
'SOUND_EVENT_RUNNING_16' : 7,
'SOUND_EVENT_STOPPED' : 8,
'SOUND_EVENT_LOAD_UNLOAD' : 9,
#sound effects
'SOUND_SPLAT' : 0x00,
'SOUND_FACTORY_WHISTLE' : 0x01,
'SOUND_TRAIN' : 0x02,
'SOUND_TRAIN_THROUGH_TUNNEL' : 0x03,
'SOUND_SHIP_HORN' : 0x04,
'SOUND_FERRY_HORN' : 0x05,
'SOUND_PLANE_TAKE_OFF' : 0x06,
'SOUND_JET' : 0x07,
'SOUND_TRAIN_HORN' : 0x08,
'SOUND_MINING_MACHINERY' : 0x09,
'SOUND_ELECTRIC_SPARK' : 0x0A,
'SOUND_STEAM' : 0x0B,
'SOUND_LEVEL_CROSSING' : 0x0C,
'SOUND_VEHICLE_BREAKDOWN' : 0x0D,
'SOUND_TRAIN_BREAKDOWN' : 0x0E,
'SOUND_CRASH' : 0x0F,
'SOUND_EXPLOSION' : 0x10,
'SOUND_BIG_CRASH' : 0x11,
'SOUND_CASHTILL' : 0x12,
'SOUND_BEEP' : 0x13,
'SOUND_MORSE' : 0x14,
'SOUND_SKID_PLANE' : 0x15,
'SOUND_HELICOPTER' : 0x16,
'SOUND_BUS_START_PULL_AWAY' : 0x17,
'SOUND_BUS_START_PULL_AWAY_WITH_HORN' : 0x18,
'SOUND_TRUCK_START' : 0x19,
'SOUND_TRUCK_START_2' : 0x1A,
'SOUND_APPLAUSE' : 0x1B,
'SOUND_OOOOH' : 0x1C,
'SOUND_SPLAT_2' : 0x1D,
'SOUND_SPLAT_3' : 0x1E,
'SOUND_JACKHAMMER' : 0x1F,
'SOUND_CAR_HORN' : 0x20,
'SOUND_CAR_HORN_2' : 0x21,
'SOUND_SHEEP' : 0x22,
'SOUND_COW' : 0x23,
'SOUND_HORSE' : 0x24,
'SOUND_BLACKSMITH_ANVIL' : 0x25,
'SOUND_SAWMILL' : 0x26,
'SOUND_GOOD_YEAR' : 0x27,
'SOUND_BAD_YEAR' : 0x28,
'SOUND_RIP' : 0x29,
'SOUND_EXTRACT_AND_POP' : 0x2A,
'SOUND_COMEDY_HIT' : 0x2B,
'SOUND_MACHINERY' : 0x2C,
'SOUND_RIP_2' : 0x2D,
'SOUND_EXTRACT_AND_POP_2' : 0x2E,
'SOUND_POP' : 0x2F,
'SOUND_CARTOON_SOUND' : 0x30,
'SOUND_EXTRACT' : 0x31,
'SOUND_POP_2' : 0x32,
'SOUND_PLASTIC_MINE' : 0x33,
'SOUND_WIND' : 0x34,
'SOUND_COMEDY_BREAKDOWN' : 0x35,
'SOUND_CARTOON_CRASH' : 0x36,
'SOUND_BALLOON_SQUEAK' : 0x37,
'SOUND_CHAINSAW' : 0x38,
'SOUND_HEAVY_WIND' : 0x39,
'SOUND_COMEDY_BREAKDOWN_2' : 0x3A,
'SOUND_JET_OVERHEAD' : 0x3B,
'SOUND_COMEDY_CAR' : 0x3C,
'SOUND_ANOTHER_JET_OVERHEAD' : 0x3D,
'SOUND_COMEDY_CAR_2' : 0x3E,
'SOUND_COMEDY_CAR_3' : 0x3F,
'SOUND_COMEDY_CAR_START_AND_PULL_AWAY' : 0x40,
'SOUND_MAGLEV' : 0x41,
'SOUND_LOON_BIRD' : 0x42,
'SOUND_LION' : 0x43,
'SOUND_MONKEYS' : 0x44,
'SOUND_PLANE_CRASHING' : 0x45,
'SOUND_PLANE_ENGINE_SPUTTERING' : 0x46,
'SOUND_MAGLEV_2' : 0x47,
'SOUND_DISTANT_BIRD' : 0x48,
#sprite ids
'SPRITE_ID_NEW_TRAIN' : 0xFD,
'SPRITE_ID_NEW_ROADVEH' : 0xFF,
'SPRITE_ID_NEW_SHIP' : 0xFF,
'SPRITE_ID_NEW_AIRCRAFT' : 0xFF,
'NEW_CARGO_SPRITE' : 0xFFFF,
#aircraft type
'AIRCRAFT_TYPE_HELICOPTER' : 0x00,
'AIRCRAFT_TYPE_SMALL' : 0x02,
'AIRCRAFT_TYPE_LARGE' : 0x03,
#ground sprite IDs
'GROUNDSPRITE_CONCRETE' : 1420,
'GROUNDSPRITE_CLEARED' : 3924,
'GROUNDSPRITE_NORMAL' : 3981,
'GROUNDSPRITE_WATER' : 4061,
'GROUNDSPRITE_SNOW_1_4' : 4493,
'GROUNDSPRITE_SNOW_2_4' : 4512,
'GROUNDSPRITE_SNOW_3_4' : 4531,
'GROUNDSPRITE_SNOW_4_4' : 4550,
'GROUNDSPRITE_SNOW' : 4550,
'GROUNDSPRITE_DESERT_1_2' : 4512,
'GROUNDSPRITE_DESERT' : 4550,
# general CB for rerandomizing
'CB_RANDOM_TRIGGER' : 0x01,
#house flags
'HOUSE_FLAG_NOT_SLOPED' : 1,
'HOUSE_FLAG_ANIMATE' : 5,
'HOUSE_FLAG_CHURCH' : 6,
'HOUSE_FLAG_STADIUM' : 7,
'HOUSE_FLAG_ONLY_SE' : 8,
'HOUSE_FLAG_PROTECTED' : 9,
'HOUSE_FLAG_SYNC_CALLBACK' : 10,
'HOUSE_FLAG_RANDOM_ANIMATION' : 11,
#cargo acceptance
'HOUSE_ACCEPT_GOODS' : 0x00,
'HOUSE_ACCEPT_FOOD' : 0x10, # 0x80 / 8
'HOUSE_ACCEPT_FIZZY_DRINKS' : 0x10, # 0x80 / 8
# house sizes
'HOUSE_SIZE_1X1' : 0,
'HOUSE_SIZE_2X1' : 2,
'HOUSE_SIZE_1X2' : 3,
'HOUSE_SIZE_2X2' : 4,
# house tiles
'HOUSE_TILE_NORTH' : 0,
'HOUSE_TILE_EAST' : 1,
'HOUSE_TILE_WEST' : 2,
'HOUSE_TILE_SOUTH' : 3,
# house callback results
'CB_RESULT_HOUSE_NO_MORE_PRODUCTION': 0x20FF,
#town zones
'TOWNZONE_EDGE' : 0,
'TOWNZONE_OUTSKIRT' : 1,
'TOWNZONE_OUTER_SUBURB' : 2,
'TOWNZONE_INNER_SUBURB' : 3,
'TOWNZONE_CENTRE' : 4,
'ALL_TOWNZONES' : 0x1F,
#industry tile special flags
'INDTILE_FLAG_RANDOM_ANIMATION' : 0,
'CB_RESULT_IND_PROD_NO_CHANGE' : 0x00,
'CB_RESULT_IND_PROD_HALF' : 0x01,
'CB_RESULT_IND_PROD_DOUBLE' : 0x02,
'CB_RESULT_IND_PROD_CLOSE' : 0x03,
'CB_RESULT_IND_PROD_RANDOM' : 0x04,
'CB_RESULT_IND_PROD_DIVIDE_BY_4' : 0x05,
'CB_RESULT_IND_PROD_DIVIDE_BY_8' : 0x06,
'CB_RESULT_IND_PROD_DIVIDE_BY_16' : 0x07,
'CB_RESULT_IND_PROD_DIVIDE_BY_32' : 0x08,
'CB_RESULT_IND_PROD_MULTIPLY_BY_4' : 0x09,
'CB_RESULT_IND_PROD_MULTIPLY_BY_8' : 0x0A,
'CB_RESULT_IND_PROD_MULTIPLY_BY_16' : 0x0B,
'CB_RESULT_IND_PROD_MULTIPLY_BY_32' : 0x0C,
'CB_RESULT_IND_PROD_DECREMENT_BY_1' : 0x0D,
'CB_RESULT_IND_PROD_INCREMENT_BY_1' : 0x0E,
'CB_RESULT_IND_PROD_SET_BY_0x100' : 0x0F,
'CB_RESULT_IND_DO_NOT_USE_SPECIAL' : 0x00,
'CB_RESULT_IND_USE_SPECIAL' : 0x01,
'CB_RESULT_IND_NO_CONSTRUCTION' : 0x0000,
'CB_RESULT_IND_PROBABILITY_FROM_PROPERTY' : 0x0100,
'CB_RESULT_NO_TEXT' : 0x400,
'CB_RESULT_IND_NO_TEXT_NO_AMOUNT' : 0x401,
'CB_RESULT_LOCATION_ALLOW' : 0x400,
'CB_RESULT_LOCATION_DISALLOW' : 0x401,
'CB_RESULT_LOCATION_DISALLOW_ONLY_RAINFOREST' : 0x402,
'CB_RESULT_LOCATION_DISALLOW_ONLY_DESERT' : 0x403,
'CB_RESULT_LOCATION_DISALLOW_ONLY_ABOVE_SNOWLINE' : 0x404,
'CB_RESULT_LOCATION_DISALLOW_ONLY_BELOW_SNOWLINE' : 0x405,
'CB_RESULT_LOCATION_DISALLOW_NOT_ON_OPEN_SEA' : 0x406,
'CB_RESULT_LOCATION_DISALLOW_NOT_ON_CANAL' : 0x407,
'CB_RESULT_LOCATION_DISALLOW_NOT_ON_RIVER' : 0x408,
#industry special flags
'IND_FLAG_PLANT_FIELDS_PERIODICALLY' : 0,
'IND_FLAG_CUT_TREES' : 1,
'IND_FLAG_BUILT_ON_WATER' : 2,
'IND_FLAG_ONLY_IN_LARGE_TOWNS' : 3,
'IND_FLAG_ONLY_IN_TOWNS' : 4,
'IND_FLAG_BUILT_NEAR_TOWN' : 5,
'IND_FLAG_PLANT_FIELDS_WHEN_BUILT' : 6,
'IND_FLAG_NO_PRODUCTION_INCREASE' : 7,
'IND_FLAG_BUILT_ONLY_BEFORE_1950' : 8,
'IND_FLAG_BUILT_ONLY_AFTER_1960' : 9,
'IND_FLAG_AI_CREATES_AIR_AND_SHIP_ROUTES' : 10,
'IND_FLAG_MILITARY_AIRPLANE_CAN_EXPLODE' : 11,
'IND_FLAG_MILITARY_HELICOPTER_CAN_EXPLODE' : 12,
'IND_FLAG_CAN_CAUSE_SUBSIDENCE' : 13,
'IND_FLAG_AUTOMATIC_PRODUCTION_MULTIPLIER' : 14,
'IND_FLAG_RANDOM_BITS_IN_PRODUCTION_CALLBACK' : 15,
'IND_FLAG_DO_NOT_FORCE_INSTANCE_AT_MAP_GENERATION' : 16,
'IND_FLAG_ALLOW_CLOSING_LAST_INSTANCE' : 17,
#flags for builtin function industry_type(..)
'IND_TYPE_OLD' : 0,
'IND_TYPE_NEW' : 1,
#founder for industries (founder variable)
'FOUNDER_GAME' : 16,
#object flags
'OBJ_FLAG_ONLY_SE' : 0,
'OBJ_FLAG_IRREMOVABLE' : 1,
'OBJ_FLAG_ANYTHING_REMOVE' : 2,
'OBJ_FLAG_ON_WATER' : 3,
'OBJ_FLAG_REMOVE_IS_INCOME': 4,
'OBJ_FLAG_NO_FOUNDATIONS' : 5,
'OBJ_FLAG_ANIMATED' : 6,
'OBJ_FLAG_ONLY_INGAME' : 7,
'OBJ_FLAG_2CC' : 8,
'OBJ_FLAG_NOT_ON_LAND' : 9,
'OBJ_FLAG_DRAW_WATER' : 10,
'OBJ_FLAG_ALLOW_BRIDGE' : 11,
'OBJ_FLAG_RANDOM_ANIMATION': 12,
'OBJ_FLAG_SCALE_BY_WATER' : 13,
#object animation triggers
'OBJ_ANIM_IS_BUILT' : 0,
'OBJ_ANIM_PERIODIC' : 1,
'OBJ_ANIM_SYNC' : 2,
# Special values for object var 0x60
'OBJECT_TYPE_OTHER_GRF' : 0xFFFE,
'OBJECT_TYPE_NO_OBJECT' : 0xFFFF,
#railtype flags
'RAILTYPE_FLAG_CATENARY' : 0,
'RAILTYPE_FLAG_NO_LEVEL_CROSSING' : 1, # for OpenTTD > r20049
#type of default station graphics used for a railtype
'RAILTYPE_STATION_NORMAL' : 0,
'RAILTYPE_STATION_MONORAIL' : 1,
'RAILTYPE_STATION_MAGLEV' : 2,
#ground tile types as returned by railtypes varaction2 0x40
'TILETYPE_NORMAL' : 0x00,
'TILETYPE_DESERT' : 0x01,
'TILETYPE_RAIN_FOREST' : 0x02,
'TILETYPE_SNOW' : 0x04,
# Water classes
'WATER_CLASS_NONE' : 0,
'WATER_CLASS_SEA' : 1,
'WATER_CLASS_CANAL' : 2,
'WATER_CLASS_RIVER' : 3,
#level crossing status as returned by railtypes varaction2 0x42
'LEVEL_CROSSING_CLOSED' : 1,
'LEVEL_CROSSING_OPEN' : 0,
'LEVEL_CROSSING_NONE' : 0,
#acceleration model for trains
'ACC_MODEL_RAIL' : 0,
'ACC_MODEL_MONORAIL' : 1,
'ACC_MODEL_MAGLEV' : 2,
#default industry IDs
'INDUSTRYTYPE_COAL_MINE' : 0x00,
'INDUSTRYTYPE_POWER_PLANT' : 0x01,
'INDUSTRYTYPE_SAWMILL' : 0x02,
'INDUSTRYTYPE_FOREST' : 0x03,
'INDUSTRYTYPE_OIL_REFINERY' : 0x04,
'INDUSTRYTYPE_OIL_RIG' : 0x05,
'INDUSTRYTYPE_TEMPERATE_FACTORY' : 0x06,
'INDUSTRYTYPE_PRINTING_WORKS' : 0x07,
'INDUSTRYTYPE_STEEL_MILL' : 0x08,
'INDUSTRYTYPE_TEMPERATE_ARCTIC_FARM' : 0x09,
'INDUSTRYTYPE_COPPER_ORE_MINE' : 0x0A,
'INDUSTRYTYPE_OIL_WELLS' : 0x0B,
'INDUSTRYTYPE_TEMPERATE_BANK' : 0x0C,
'INDUSTRYTYPE_FOOD_PROCESSING_PLANT' : 0x0D,
'INDUSTRYTYPE_PAPER_MILL' : 0x0E,
'INDUSTRYTYPE_GOLD_MINE' : 0x0F,
'INDUSTRYTYPE_TROPICAL_ARCTIC_BANK' : 0x10,
'INDUSTRYTYPE_DIAMOND_MINE' : 0x11,
'INDUSTRYTYPE_IRON_ORE_MINE' : 0x12,
'INDUSTRYTYPE_FRUIT_PLANTATION' : 0x13,
'INDUSTRYTYPE_RUBBER_PLANTATION' : 0x14,
'INDUSTRYTYPE_WATER_WELL' : 0x15,
'INDUSTRYTYPE_WATER_TOWER' : 0x16,
'INDUSTRYTYPE_TROPICAL_FACTORY' : 0x17,
'INDUSTRYTYPE_TROPICAL_FARM' : 0x18,
'INDUSTRYTYPE_LUMBER_MILL' : 0x19,
'INDUSTRYTYPE_CANDYFLOSS_FOREST' : 0x1A,
'INDUSTRYTYPE_SWEETS_FACTORY' : 0x1B,
'INDUSTRYTYPE_BATTERY_FARM' : 0x1C,
'INDUSTRYTYPE_COLA_WELLS' : 0x1D,
'INDUSTRYTYPE_TOY_SHOP' : 0x1E,
'INDUSTRYTYPE_TOY_FACTORY' : 0x1F,
'INDUSTRYTYPE_PLASTIC_FOUNTAIN' : 0x20,
'INDUSTRYTYPE_FIZZY_DRINKS_FACTORY' : 0x21,
'INDUSTRYTYPE_BUBBLE_GENERATOR' : 0x22,
'INDUSTRYTYPE_TOFFE_QUARRY' : 0x23,
'INDUSTRYTYPE_SUGAR_MINE' : 0x24,
'INDUSTRYTYPE_UNKNOWN' : 0xFE,
'INDUSTRYTYPE_TOWN' : 0xFF,
#industry life types (industry property 0x0B, life_type)
'IND_LIFE_TYPE_BLACK_HOLE' : 0x00,
'IND_LIFE_TYPE_EXTRACTIVE' : 0x01,
'IND_LIFE_TYPE_ORGANIC' : 0x02,
'IND_LIFE_TYPE_PROCESSING' : 0x04,
#traffic side (bool, true = right hand side)
'TRAFFIC_SIDE_LEFT' : 0,
'TRAFFIC_SIDE_RIGHT' : 1,
#which platform has loaded this grf
'PLATFORM_TTDPATCH' : 0x00,
'PLATFORM_OPENTTD' : 0x01,
#player types (vehicle var 0x43)
'PLAYERTYPE_HUMAN' : 0,
'PLAYERTYPE_AI' : 1,
'PLAYERTYPE_HUMAN_IN_AI' : 2,
'PLAYERTYPE_AI_IN_HUMAN' : 3,
# search criteria for house var 0x65
'SEARCH_HOUSE_BY_TYPE' : 0,
'SEARCH_HOUSE_BY_CLASS' : 1,
'SEARCH_HOUSE_BY_GRFID' : 2,
#build types (industry var 0xB3)
'BUILDTYPE_UNKNOWN' : 0,
'BUILDTYPE_GAMEPLAY' : 1,
'BUILDTYPE_GENERATION' : 2,
'BUILDTYPE_EDITOR' : 3,
# Creation types (several industry[tile] callbacks
'IND_CREATION_GENERATION' : 0,
'IND_CREATION_RANDOM' : 1,
'IND_CREATION_FUND' : 2,
'IND_CREATION_PROSPECT' : 3,
#airport types (vehicle var 0x44, base station var airport_type)
'AIRPORTTYPE_SMALL' : 0,
'AIRPORTTYPE_LARGE' : 1,
'AIRPORTTYPE_HELIPORT' : 2,
'AIRPORTTYPE_OILRIG' : 3,
#Direction for e.g. airport layouts, vehicle direction
'DIRECTION_NORTH' : 0,
'DIRECTION_NORTHEAST' : 1,
'DIRECTION_EAST' : 2,
'DIRECTION_SOUTHEAST' : 3,
'DIRECTION_SOUTH' : 4,
'DIRECTION_SOUTHWEST' : 5,
'DIRECTION_WEST' : 6,
'DIRECTION_NORTHWEST' : 7,
'CANAL_DIRECTION_NORTH' : 7,
'CANAL_DIRECTION_NORTHEAST' : 0,
'CANAL_DIRECTION_EAST' : 4,
'CANAL_DIRECTION_SOUTHEAST' : 1,
'CANAL_DIRECTION_SOUTH' : 5,
'CANAL_DIRECTION_SOUTHWEST' : 2,
'CANAL_DIRECTION_WEST' : 6,
'CANAL_DIRECTION_NORTHWEST' : 3,
'CORNER_W' : 0,
'CORNER_S' : 1,
'CORNER_E' : 2,
'CORNER_N' : 3,
'IS_STEEP_SLOPE' : 4,
'SLOPE_FLAT' : 0,
'SLOPE_W' : 1,
'SLOPE_S' : 2,
'SLOPE_E' : 4,
'SLOPE_N' : 8,
'SLOPE_NW' : 9,
'SLOPE_SW' : 3,
'SLOPE_SE' : 6,
'SLOPE_NE' : 12,
'SLOPE_EW' : 5,
'SLOPE_NS' : 10,
'SLOPE_NWS' : 11,
'SLOPE_WSE' : 7,
'SLOPE_SEN' : 14,
'SLOPE_ENW' : 13,
'SLOPE_STEEP_W' : 27,
'SLOPE_STEEP_S' : 23,
'SLOPE_STEEP_E' : 30,
'SLOPE_STEEP_N' : 29,
#loading stages
'LOADING_STAGE_INITIALIZE' : 0x0000,
'LOADING_STAGE_RESERVE' : 0x0101,
'LOADING_STAGE_ACTIVATE' : 0x0201,
'LOADING_STAGE_TEST' : 0x0401,
#palettes
'PALETTE_DOS' : 0,
'PALETTE_DEFAULT' : 0,
'PALETTE_WIN' : 1,
'PALETTE_LEGACY' : 1,
#game mode
'GAMEMODE_MENU' : 0,
'GAMEMODE_GAME' : 1,
'GAMEMODE_EDITOR' : 2,
#difficulty
'DIFFICULTY_EASY' : 0,
'DIFFICULTY_MEDIUM' : 1,
'DIFFICULTY_HARD' : 2,
'DIFFICULTY_CUSTOM' : 3,
#display options
'DISPLAY_TOWN_NAMES' : 0,
'DISPLAY_STATION_NAMES' : 1,
'DISPLAY_SIGNS' : 2,
'DISPLAY_ANIMATION' : 3,
'DISPLAY_FULL_DETAIL' : 5,
#map types (ttdp variable 0x13)
'MAP_TYPE_X_BIGGER' : 0, #bit 0 and 1 clear
'MAP_TYPE_RECTANGULAR' : 1, #bit 0 set
'MAP_TYPE_Y_BIGGER' : 2, #bit 0 clear, bit 1 set
# Platform types (platform_xx station variables)
'PLATFORM_SAME_STATION' : 0,
'PLATFORM_SAME_SECTION' : 1,
'PLATFORM_SAME_DIRECTION' : 2,
# vehicle type (base station var 'had_vehicle_of_type')
'HAD_VEHICLE_OF_TYPE_TRAIN' : 0,
'HAD_VEHICLE_OF_TYPE_BUS' : 1,
'HAD_VEHICLE_OF_TYPE_TRUCK' : 2,
'HAD_VEHICLE_OF_TYPE_AIRCRAFT' : 3,
'HAD_VEHICLE_OF_TYPE_SHIP' : 4,
# station facilities (base station var 'facilities')
'FACILITY_TRAIN' : 0,
'FACILITY_TRUCK_STOP' : 1,
'FACILITY_BUS_STOP' : 2,
'FACILITY_AIRPORT' : 3,
'FACILITY_DOCK' : 4,
#Random triggers
'TRIGGER_ALL_NEEDED' : 7,
'TRIGGER_VEHICLE_NEW_LOAD' : 0,
'TRIGGER_VEHICLE_SERVICE' : 1,
'TRIGGER_VEHICLE_UNLOAD_ALL' : 2,
'TRIGGER_VEHICLE_ANY_LOAD' : 3,
'TRIGGER_VEHICLE_32_CALLBACK' : 4,
'TRIGGER_STATION_NEW_CARGO' : 0,
'TRIGGER_STATION_NO_MORE_CARGO' : 1,
'TRIGGER_STATION_TRAIN_ARRIVES' : 2,
'TRIGGER_STATION_TRAIN_LEAVES' : 3,
'TRIGGER_STATION_TRAIN_LOADS_UNLOADS' : 4,
'TRIGGER_STATION_TRAIN_RESERVES' : 5,
'TRIGGER_HOUSE_TILELOOP' : 0,
'TRIGGER_HOUSE_TOP_TILELOOP' : 1,
'TRIGGER_INDUSTRYTILE_TILELOOP' : 0,
'TRIGGER_INDUSTRYTILE_256_TICKS' : 1,
'TRIGGER_INDUSTRYTILE_CARGO_DELIVERY' : 2,
#Tile classes
'TILE_CLASS_GROUND' : 0x00,
'TILE_CLASS_RAIL' : 0x01,
'TILE_CLASS_ROAD' : 0x02,
'TILE_CLASS_HOUSE' : 0x03,
'TILE_CLASS_TREES' : 0x04,
'TILE_CLASS_STATION' : 0x05,
'TILE_CLASS_WATER' : 0x06,
'TILE_CLASS_VOID' : 0x07,
'TILE_CLASS_INDUSTRY' : 0x08,
'TILE_CLASS_TUNNEL_BRIDGE' : 0x09,
'TILE_CLASS_OBJECTS' : 0x0A,
#Land shape flags for industry tiles
'LSF_CANNOT_LOWER_NW_EDGE' : 0,
'LSF_CANNOT_LOWER_NE_EDGE' : 1,
'LSF_CANNOT_LOWER_SW_EDGE' : 2,
'LSF_CANNOT_LOWER_SE_EDGE' : 3,
'LSF_ONLY_ON_FLAT_LAND' : 4,
'LSF_ALLOW_ON_WATER' : 5,
# Animation triggers
'ANIM_TRIGGER_INDTILE_CONSTRUCTION_STATE' : 0,
'ANIM_TRIGGER_INDTILE_TILE_LOOP' : 1,
'ANIM_TRIGGER_INDTILE_INDUSTRY_LOOP' : 2,
'ANIM_TRIGGER_INDTILE_RECEIVED_CARGO' : 3,
'ANIM_TRIGGER_INDTILE_DISTRIBUTES_CARGO' : 4,
'ANIM_TRIGGER_OBJ_BUILT' : 0,
'ANIM_TRIGGER_OBJ_TILELOOP' : 1,
'ANIM_TRIGGER_OBJ_256_TICKS' : 2,
'ANIM_TRIGGER_APT_BUILT' : 0,
'ANIM_TRIGGER_APT_TILELOOP' : 1,
'ANIM_TRIGGER_APT_NEW_CARGO' : 2,
'ANIM_TRIGGER_APT_CARGO_TAKEN' : 3,
'ANIM_TRIGGER_APT_250_TICKS' : 4,
# Animation looping
'ANIMATION_NON_LOOPING' : 0,
'ANIMATION_LOOPING' : 1,
# Animation callback results
'CB_RESULT_STOP_ANIMATION' : 0xFF, # callback 0x25, 0x26
'CB_RESULT_START_ANIMATION' : 0xFE, # callback 0x25
'CB_RESULT_NEXT_FRAME' : 0xFE, # callback 0x26
'CB_RESULT_DO_NOTHING' : 0xFD, # callback 0x25
'CB_RESULT_FOUNDATIONS' : 0x01, # callback 0x30
'CB_RESULT_NO_FOUNDATIONS' : 0x00, # callback 0x30
'CB_RESULT_AUTOSLOPE' : 0x00, # callback 0x3C
'CB_RESULT_NO_AUTOSLOPE' : 0x01, # callback 0x3C
# Recolour modes for layout sprites
'RECOLOUR_NONE' : 0,
'RECOLOUR_TRANSPARENT' : 1,
'RECOLOUR_REMAP' : 2,
# Possible values for palette
'PALETTE_USE_DEFAULT' : 0,
'PALETTE_TILE_RED_PULSATING' : 771,
'PALETTE_SEL_TILE_RED' : 772,
'PALETTE_SEL_TILE_BLUE' : 773,
'PALETTE_IDENTITY' : 775,
'PALETTE_CC_FIRST' : 775,
'PALETTE_CC_DARK_BLUE' : 775, # = first
'PALETTE_CC_PALE_GREEN' : 776,
'PALETTE_CC_PINK' : 777,
'PALETTE_CC_YELLOW' : 778,
'PALETTE_CC_RED' : 778,
'PALETTE_CC_LIGHT_BLUE' : 780,
'PALETTE_CC_GREEN' : 781,
'PALETTE_CC_DARK_GREEN' : 782,
'PALETTE_CC_BLUE' : 783,
'PALETTE_CC_CREAM' : 784,
'PALETTE_CC_MAUVE' : 785,
'PALETTE_CC_PURPLE' : 786,
'PALETTE_CC_ORANGE' : 787,
'PALETTE_CC_BROWN' : 788,
'PALETTE_CC_GREY' : 789,
'PALETTE_CC_WHITE' : 790,
'PALETTE_BARE_LAND' : 791,
'PALETTE_STRUCT_BLUE' : 795,
'PALETTE_STRUCT_BROWN' : 796,
'PALETTE_STRUCT_WHITE' : 797,
'PALETTE_STRUCT_RED' : 798,
'PALETTE_STRUCT_GREEN' : 799,
'PALETTE_STRUCT_CONCRETE' : 800,
'PALETTE_STRUCT_YELLOW' : 801,
'PALETTE_TRANSPARENT' : 802,
'PALETTE_STRUCT_GREY' : 803,
'PALETTE_CRASH' : 804,
'PALETTE_CHURCH_RED' : 1438,
'PALETTE_CHURCH_CREAM' : 1439,
# Company colours
'COLOUR_DARK_BLUE' : 0,
'COLOUR_PALE_GREEN' : 1,
'COLOUR_PINK' : 2,
'COLOUR_YELLOW' : 3,
'COLOUR_RED' : 4,
'COLOUR_LIGHT_BLUE' : 5,
'COLOUR_GREEN' : 6,
'COLOUR_DARK_GREEN' : 7,
'COLOUR_BLUE' : 8,
'COLOUR_CREAM' : 9,
'COLOUR_MAUVE' : 10,
'COLOUR_PURPLE' : 11,
'COLOUR_ORANGE' : 12,
'COLOUR_BROWN' : 13,
'COLOUR_GREY' : 14,
'COLOUR_WHITE' : 15,
# Town growth effect of cargo
'TOWNGROWTH_PASSENGERS' : 0x00,
'TOWNGROWTH_MAIL' : 0x02,
'TOWNGROWTH_GOODS' : 0x05,
'TOWNGROWTH_WATER' : 0x09,
'TOWNGROWTH_FOOD' : 0x0B,
'TOWNGROWTH_NONE' : 0xFF,
# Cargo callbacks
'CARGO_CB_PROFIT' : 0x01,
'CARGO_CB_STATION_RATING' : 0x02,
#CMP and UCMP results
'CMP_LESS' : 0,
'CMP_EQUAL' : 1,
'CMP_GREATER' : 2,
# TTD Strings
'TTD_STR_CARGO_PLURAL_NOTHING' : 0x000E,
'TTD_STR_CARGO_PLURAL_PASSENGERS' : 0x000F,
'TTD_STR_CARGO_PLURAL_COAL' : 0x0010,
'TTD_STR_CARGO_PLURAL_MAIL' : 0x0011,
'TTD_STR_CARGO_PLURAL_OIL' : 0x0012,
'TTD_STR_CARGO_PLURAL_LIVESTOCK' : 0x0013,
'TTD_STR_CARGO_PLURAL_GOODS' : 0x0014,
'TTD_STR_CARGO_PLURAL_GRAIN' : 0x0015,
'TTD_STR_CARGO_PLURAL_WOOD' : 0x0016,
'TTD_STR_CARGO_PLURAL_IRON_ORE' : 0x0017,
'TTD_STR_CARGO_PLURAL_STEEL' : 0x0018,
'TTD_STR_CARGO_PLURAL_VALUABLES' : 0x0019,
'TTD_STR_CARGO_PLURAL_COPPER_ORE' : 0x001A,
'TTD_STR_CARGO_PLURAL_MAIZE' : 0x001B,
'TTD_STR_CARGO_PLURAL_FRUIT' : 0x001C,
'TTD_STR_CARGO_PLURAL_DIAMONDS' : 0x001D,
'TTD_STR_CARGO_PLURAL_FOOD' : 0x001E,
'TTD_STR_CARGO_PLURAL_PAPER' : 0x001F,
'TTD_STR_CARGO_PLURAL_GOLD' : 0x0020,
'TTD_STR_CARGO_PLURAL_WATER' : 0x0021,
'TTD_STR_CARGO_PLURAL_WHEAT' : 0x0022,
'TTD_STR_CARGO_PLURAL_RUBBER' : 0x0023,
'TTD_STR_CARGO_PLURAL_SUGAR' : 0x0024,
'TTD_STR_CARGO_PLURAL_TOYS' : 0x0025,
'TTD_STR_CARGO_PLURAL_CANDY' : 0x0026,
'TTD_STR_CARGO_PLURAL_COLA' : 0x0027,
'TTD_STR_CARGO_PLURAL_COTTON_CANDY' : 0x0028,
'TTD_STR_CARGO_PLURAL_BUBBLES' : 0x0029,
'TTD_STR_CARGO_PLURAL_TOFFEE' : 0x002A,
'TTD_STR_CARGO_PLURAL_BATTERIES' : 0x002B,
'TTD_STR_CARGO_PLURAL_PLASTIC' : 0x002C,
'TTD_STR_CARGO_PLURAL_FIZZY_DRINKS' : 0x002D,
'TTD_STR_CARGO_SINGULAR_NOTHING' : 0x002E,
'TTD_STR_CARGO_SINGULAR_PASSENGER' : 0x002F,
'TTD_STR_CARGO_SINGULAR_COAL' : 0x0030,
'TTD_STR_CARGO_SINGULAR_MAIL' : 0x0031,
'TTD_STR_CARGO_SINGULAR_OIL' : 0x0032,
'TTD_STR_CARGO_SINGULAR_LIVESTOCK' : 0x0033,
'TTD_STR_CARGO_SINGULAR_GOODS' : 0x0034,
'TTD_STR_CARGO_SINGULAR_GRAIN' : 0x0035,
'TTD_STR_CARGO_SINGULAR_WOOD' : 0x0036,
'TTD_STR_CARGO_SINGULAR_IRON_ORE' : 0x0037,
'TTD_STR_CARGO_SINGULAR_STEEL' : 0x0038,
'TTD_STR_CARGO_SINGULAR_VALUABLES' : 0x0039,
'TTD_STR_CARGO_SINGULAR_COPPER_ORE' : 0x003A,
'TTD_STR_CARGO_SINGULAR_MAIZE' : 0x003B,
'TTD_STR_CARGO_SINGULAR_FRUIT' : 0x003C,
'TTD_STR_CARGO_SINGULAR_DIAMOND' : 0x003D,
'TTD_STR_CARGO_SINGULAR_FOOD' : 0x003E,
'TTD_STR_CARGO_SINGULAR_PAPER' : 0x003F,
'TTD_STR_CARGO_SINGULAR_GOLD' : 0x0040,
'TTD_STR_CARGO_SINGULAR_WATER' : 0x0041,
'TTD_STR_CARGO_SINGULAR_WHEAT' : 0x0042,
'TTD_STR_CARGO_SINGULAR_RUBBER' : 0x0043,
'TTD_STR_CARGO_SINGULAR_SUGAR' : 0x0044,
'TTD_STR_CARGO_SINGULAR_TOY' : 0x0045,
'TTD_STR_CARGO_SINGULAR_CANDY' : 0x0046,
'TTD_STR_CARGO_SINGULAR_COLA' : 0x0047,
'TTD_STR_CARGO_SINGULAR_COTTON_CANDY' : 0x0048,
'TTD_STR_CARGO_SINGULAR_BUBBLE' : 0x0049,
'TTD_STR_CARGO_SINGULAR_TOFFEE' : 0x004A,
'TTD_STR_CARGO_SINGULAR_BATTERY' : 0x004B,
'TTD_STR_CARGO_SINGULAR_PLASTIC' : 0x004C,
'TTD_STR_CARGO_SINGULAR_FIZZY_DRINK' : 0x004D,
'TTD_STR_PASSENGERS' : 0x004F,
'TTD_STR_TONS' : 0x0050,
'TTD_STR_BAGS' : 0x0051,
'TTD_STR_LITERS' : 0x0052,
'TTD_STR_ITEMS' : 0x0053,
'TTD_STR_CRATES' : 0x0054,
'TTD_STR_QUANTITY_NOTHING' : 0x006E,
'TTD_STR_QUANTITY_PASSENGERS' : 0x006F,
'TTD_STR_QUANTITY_COAL' : 0x0070,
'TTD_STR_QUANTITY_MAIL' : 0x0071,
'TTD_STR_QUANTITY_OIL' : 0x0072,
'TTD_STR_QUANTITY_LIVESTOCK' : 0x0073,
'TTD_STR_QUANTITY_GOODS' : 0x0074,
'TTD_STR_QUANTITY_GRAIN' : 0x0075,
'TTD_STR_QUANTITY_WOOD' : 0x0076,
'TTD_STR_QUANTITY_IRON_ORE' : 0x0077,
'TTD_STR_QUANTITY_STEEL' : 0x0078,
'TTD_STR_QUANTITY_VALUABLES' : 0x0079,
'TTD_STR_QUANTITY_COPPER_ORE' : 0x007A,
'TTD_STR_QUANTITY_MAIZE' : 0x007B,
'TTD_STR_QUANTITY_FRUIT' : 0x007C,
'TTD_STR_QUANTITY_DIAMONDS' : 0x007D,
'TTD_STR_QUANTITY_FOOD' : 0x007E,
'TTD_STR_QUANTITY_PAPER' : 0x007F,
'TTD_STR_QUANTITY_GOLD' : 0x0080,
'TTD_STR_QUANTITY_WATER' : 0x0081,
'TTD_STR_QUANTITY_WHEAT' : 0x0082,
'TTD_STR_QUANTITY_RUBBER' : 0x0083,
'TTD_STR_QUANTITY_SUGAR' : 0x0084,
'TTD_STR_QUANTITY_TOYS' : 0x0085,
'TTD_STR_QUANTITY_SWEETS' : 0x0086,
'TTD_STR_QUANTITY_COLA' : 0x0087,
'TTD_STR_QUANTITY_CANDYFLOSS' : 0x0088,
'TTD_STR_QUANTITY_BUBBLES' : 0x0089,
'TTD_STR_QUANTITY_TOFFEE' : 0x008A,
'TTD_STR_QUANTITY_BATTERIES' : 0x008B,
'TTD_STR_QUANTITY_PLASTIC' : 0x008C,
'TTD_STR_QUANTITY_FIZZY_DRINKS' : 0x008D,
'TTD_STR_ABBREV_NOTHING' : 0x008E,
'TTD_STR_ABBREV_PASSENGERS' : 0x008F,
'TTD_STR_ABBREV_COAL' : 0x0090,
'TTD_STR_ABBREV_MAIL' : 0x0091,
'TTD_STR_ABBREV_OIL' : 0x0092,
'TTD_STR_ABBREV_LIVESTOCK' : 0x0093,
'TTD_STR_ABBREV_GOODS' : 0x0094,
'TTD_STR_ABBREV_GRAIN' : 0x0095,
'TTD_STR_ABBREV_WOOD' : 0x0096,
'TTD_STR_ABBREV_IRON_ORE' : 0x0097,
'TTD_STR_ABBREV_STEEL' : 0x0098,
'TTD_STR_ABBREV_VALUABLES' : 0x0099,
'TTD_STR_ABBREV_COPPER_ORE' : 0x009A,
'TTD_STR_ABBREV_MAIZE' : 0x009B,
'TTD_STR_ABBREV_FRUIT' : 0x009C,
'TTD_STR_ABBREV_DIAMONDS' : 0x009D,
'TTD_STR_ABBREV_FOOD' : 0x009E,
'TTD_STR_ABBREV_PAPER' : 0x009F,
'TTD_STR_ABBREV_GOLD' : 0x00A0,
'TTD_STR_ABBREV_WATER' : 0x00A1,
'TTD_STR_ABBREV_WHEAT' : 0x00A2,
'TTD_STR_ABBREV_RUBBER' : 0x00A3,
'TTD_STR_ABBREV_SUGAR' : 0x00A4,
'TTD_STR_ABBREV_TOYS' : 0x00A5,
'TTD_STR_ABBREV_SWEETS' : 0x00A6,
'TTD_STR_ABBREV_COLA' : 0x00A7,
'TTD_STR_ABBREV_CANDYFLOSS' : 0x00A8,
'TTD_STR_ABBREV_BUBBLES' : 0x00A9,
'TTD_STR_ABBREV_TOFFEE' : 0x00AA,
'TTD_STR_ABBREV_BATTERIES' : 0x00AB,
'TTD_STR_ABBREV_PLASTIC' : 0x00AC,
'TTD_STR_ABBREV_FIZZY_DRINKS' : 0x00AD,
'TTD_STR_TOWN_BUILDING_NAME_TALL_OFFICE_BLOCK_1' : 0x200F,
'TTD_STR_TOWN_BUILDING_NAME_OFFICE_BLOCK_1' : 0x2010,
'TTD_STR_TOWN_BUILDING_NAME_SMALL_BLOCK_OF_FLATS_1' : 0x2011,
'TTD_STR_TOWN_BUILDING_NAME_CHURCH_1' : 0x2012,
'TTD_STR_TOWN_BUILDING_NAME_LARGE_OFFICE_BLOCK_1' : 0x2013,
'TTD_STR_TOWN_BUILDING_NAME_TOWN_HOUSES_1' : 0x2014,
'TTD_STR_TOWN_BUILDING_NAME_HOTEL_1' : 0x2015,
'TTD_STR_TOWN_BUILDING_NAME_STATUE_1' : 0x2016,
'TTD_STR_TOWN_BUILDING_NAME_FOUNTAIN_1' : 0x2017,
'TTD_STR_TOWN_BUILDING_NAME_PARK_1' : 0x2018,
'TTD_STR_TOWN_BUILDING_NAME_OFFICE_BLOCK_2' : 0x2019,
'TTD_STR_TOWN_BUILDING_NAME_SHOPS_AND_OFFICES_1' : 0x201A,
'TTD_STR_TOWN_BUILDING_NAME_MODERN_OFFICE_BUILDING_1' : 0x201B,
'TTD_STR_TOWN_BUILDING_NAME_WAREHOUSE_1' : 0x201C,
'TTD_STR_TOWN_BUILDING_NAME_OFFICE_BLOCK_3' : 0x201D,
'TTD_STR_TOWN_BUILDING_NAME_STADIUM_1' : 0x201E,
'TTD_STR_TOWN_BUILDING_NAME_OLD_HOUSES_1' : 0x201F,
'TTD_STR_TOWN_BUILDING_NAME_COTTAGES_1' : 0x2036,
'TTD_STR_TOWN_BUILDING_NAME_HOUSES_1' : 0x2037,
'TTD_STR_TOWN_BUILDING_NAME_FLATS_1' : 0x2038,
'TTD_STR_TOWN_BUILDING_NAME_TALL_OFFICE_BLOCK_2' : 0x2039,
'TTD_STR_TOWN_BUILDING_NAME_SHOPS_AND_OFFICES_2' : 0x203A,
'TTD_STR_TOWN_BUILDING_NAME_SHOPS_AND_OFFICES_3' : 0x203B,
'TTD_STR_TOWN_BUILDING_NAME_THEATER_1' : 0x203C,
'TTD_STR_TOWN_BUILDING_NAME_STADIUM_2' : 0x203D,
'TTD_STR_TOWN_BUILDING_NAME_OFFICES_1' : 0x203E,
'TTD_STR_TOWN_BUILDING_NAME_HOUSES_2' : 0x203F,
'TTD_STR_TOWN_BUILDING_NAME_CINEMA_1' : 0x2040,
'TTD_STR_TOWN_BUILDING_NAME_SHOPPING_MALL_1' : 0x2041,
'TTD_STR_TOWN_BUILDING_NAME_IGLOO_1' : 0x2059,
'TTD_STR_TOWN_BUILDING_NAME_TEPEES_1' : 0x205A,
'TTD_STR_TOWN_BUILDING_NAME_TEAPOT_HOUSE_1' : 0x205B,
'TTD_STR_TOWN_BUILDING_NAME_PIGGY_BANK_1' : 0x205C,
'TTD_STR_INDUSTRY_NAME_COAL_MINE' : 0x4802,
'TTD_STR_INDUSTRY_NAME_POWER_STATION' : 0x4803,
'TTD_STR_INDUSTRY_NAME_SAWMILL' : 0x4804,
'TTD_STR_INDUSTRY_NAME_FOREST' : 0x4805,
'TTD_STR_INDUSTRY_NAME_OIL_REFINERY' : 0x4806,
'TTD_STR_INDUSTRY_NAME_OIL_RIG' : 0x4807,
'TTD_STR_INDUSTRY_NAME_FACTORY' : 0x4808,
'TTD_STR_INDUSTRY_NAME_PRINTING_WORKS' : 0x4809,
'TTD_STR_INDUSTRY_NAME_STEEL_MILL' : 0x480A,
'TTD_STR_INDUSTRY_NAME_FARM' : 0x480B,
'TTD_STR_INDUSTRY_NAME_COPPER_ORE_MINE' : 0x480C,
'TTD_STR_INDUSTRY_NAME_OIL_WELLS' : 0x480D,
'TTD_STR_INDUSTRY_NAME_BANK' : 0x480E,
'TTD_STR_INDUSTRY_NAME_FOOD_PROCESSING_PLANT' : 0x480F,
'TTD_STR_INDUSTRY_NAME_PAPER_MILL' : 0x4810,
'TTD_STR_INDUSTRY_NAME_GOLD_MINE' : 0x4811,
'TTD_STR_INDUSTRY_NAME_BANK_TROPIC_ARCTIC' : 0x4812,
'TTD_STR_INDUSTRY_NAME_DIAMOND_MINE' : 0x4813,
'TTD_STR_INDUSTRY_NAME_IRON_ORE_MINE' : 0x4814,
'TTD_STR_INDUSTRY_NAME_FRUIT_PLANTATION' : 0x4815,
'TTD_STR_INDUSTRY_NAME_RUBBER_PLANTATION' : 0x4816,
'TTD_STR_INDUSTRY_NAME_WATER_SUPPLY' : 0x4817,
'TTD_STR_INDUSTRY_NAME_WATER_TOWER' : 0x4818,
'TTD_STR_INDUSTRY_NAME_FACTORY_2' : 0x4819,
'TTD_STR_INDUSTRY_NAME_FARM_2' : 0x481A,
'TTD_STR_INDUSTRY_NAME_LUMBER_MILL' : 0x481B,
'TTD_STR_INDUSTRY_NAME_COTTON_CANDY_FOREST' : 0x481C,
'TTD_STR_INDUSTRY_NAME_CANDY_FACTORY' : 0x481D,
'TTD_STR_INDUSTRY_NAME_BATTERY_FARM' : 0x481E,
'TTD_STR_INDUSTRY_NAME_COLA_WELLS' : 0x481F,
'TTD_STR_INDUSTRY_NAME_TOY_SHOP' : 0x4820,
'TTD_STR_INDUSTRY_NAME_TOY_FACTORY' : 0x4821,
'TTD_STR_INDUSTRY_NAME_PLASTIC_FOUNTAINS' : 0x4822,
'TTD_STR_INDUSTRY_NAME_FIZZY_DRINK_FACTORY' : 0x4823,
'TTD_STR_INDUSTRY_NAME_BUBBLE_GENERATOR' : 0x4824,
'TTD_STR_INDUSTRY_NAME_TOFFEE_QUARRY' : 0x4825,
'TTD_STR_INDUSTRY_NAME_SUGAR_MINE' : 0x4826,
'TTD_STR_NEWS_INDUSTRY_CONSTRUCTION' : 0x482D,
'TTD_STR_NEWS_INDUSTRY_PLANTED' : 0x482E,
'TTD_STR_NEWS_INDUSTRY_CLOSURE_GENERAL' : 0x4832,
'TTD_STR_NEWS_INDUSTRY_CLOSURE_SUPPLY_PROBLEMS' : 0x4833,
'TTD_STR_NEWS_INDUSTRY_CLOSURE_LACK_OF_TREES' : 0x4834,
'TTD_STR_NEWS_INDUSTRY_PRODUCTION_INCREASE_GENERAL' : 0x4835,
'TTD_STR_NEWS_INDUSTRY_PRODUCTION_INCREASE_COAL' : 0x4836,
'TTD_STR_NEWS_INDUSTRY_PRODUCTION_INCREASE_OIL' : 0x4837,
'TTD_STR_NEWS_INDUSTRY_PRODUCTION_INCREASE_FARM' : 0x4838,
'TTD_STR_NEWS_INDUSTRY_PRODUCTION_DECREASE_GENERAL' : 0x4839,
'TTD_STR_NEWS_INDUSTRY_PRODUCTION_DECREASE_FARM' : 0x483A,
'TTD_STR_ERROR_CAN_T_CONSTRUCT_THIS_INDUSTRY' : 0x4830,
'TTD_STR_ERROR_FOREST_CAN_ONLY_BE_PLANTED' : 0x4831,
'TTD_STR_ERROR_CAN_ONLY_BE_POSITIONED' : 0x483B,
}
def signextend(param, info):
#r = (x ^ m) - m; with m being (1 << (num_bits -1))
m = expression.ConstantNumeric(1 << (info['size'] * 8 - 1))
return expression.BinOp(nmlop.SUB, expression.BinOp(nmlop.XOR, param, m, param.pos), m, param.pos)
def global_param_write(info, expr, pos):
if not ('writable' in info and info['writable']): raise generic.ScriptError("Target parameter is not writable.", pos)
return expression.Parameter(expression.ConstantNumeric(info['num']), pos), expr
def global_param_read(info, pos):
param = expression.Parameter(expression.ConstantNumeric(info['num']), pos)
if info['size'] == 1:
mask = expression.ConstantNumeric(0xFF)
param = expression.BinOp(nmlop.AND, param, mask)
else:
assert info['size'] == 4
if 'function' in info: return info['function'](param, info)
return param
def param_from_info(info, pos):
return expression.SpecialParameter(generic.reverse_lookup(global_parameters, info), info, global_param_write, global_param_read, False, pos)
global_parameters = {
'climate' : {'num': 0x83, 'size': 1},
'loading_stage' : {'num': 0x84, 'size': 4},
'ttdpatch_version' : {'num': 0x8B, 'size': 4},
'current_palette' : {'num': 0x8D, 'size': 1},
'traininfo_y_offset' : {'num': 0x8E, 'size': 1, 'writable': 1, 'function': signextend},
'game_mode' : {'num': 0x92, 'size': 1},
'ttd_platform' : {'num': 0x9D, 'size': 4},
'openttd_version' : {'num': 0xA1, 'size': 4},
'difficulty_level' : {'num': 0xA2, 'size': 4},
'date_loaded' : {'num': 0xA3, 'size': 4},
'year_loaded' : {'num': 0xA4, 'size': 4},
}
def misc_bit_write(info, expr, pos):
param = expression.Parameter(expression.ConstantNumeric(info['param'], pos), pos)
#param = (expr != 0) ? param | (1 << bit) : param & ~(1 << bit)
expr = expression.BinOp(nmlop.CMP_NEQ, expr, expression.ConstantNumeric(0, pos), pos)
or_expr = expression.BinOp(nmlop.OR, param, expression.ConstantNumeric(1 << info['bit'], pos), pos)
and_expr = expression.BinOp(nmlop.AND, param, expression.ConstantNumeric(~(1 << info['bit']), pos), pos)
expr = expression.TernaryOp(expr, or_expr, and_expr, pos)
return (param, expr)
def misc_bit_read(info, pos):
return expression.BinOp(nmlop.HASBIT, expression.Parameter(expression.ConstantNumeric(info['param'], pos), pos), expression.ConstantNumeric(info['bit'], pos), pos)
def misc_grf_bit(info, pos):
return expression.SpecialParameter(generic.reverse_lookup(misc_grf_bits, info), info, misc_bit_write, misc_bit_read, True, pos)
misc_grf_bits = {
'traffic_side' : {'param': 0x86, 'bit': 4},
'desert_paved_roads' : {'param': 0x9E, 'bit': 1},
'train_width_32_px' : {'param': 0x9E, 'bit': 3},
'second_rocky_tileset' : {'param': 0x9E, 'bit': 6},
}
def add_1920(expr, info):
"""
Create a new expression that adds 1920 to a given expression.
@param expr: The expression to add 1920 to.
@type expr: L{Expression}
@param info: Ignored.
@return: A new expression that adds 1920 to the given expression.
@rtype: L{Expression}
"""
return expression.BinOp(nmlop.ADD, expr, expression.ConstantNumeric(1920, expr.pos), expr.pos)
def map_exponentiate(expr, info):
"""
Given a exponent, add an offset to it and compute the exponentiation with base 2.
@param expr: The exponent.
@type expr: L{Expression}
@param info: Table with extra information, most notable 'log_offset', the value we need to
add to the given expression before computing the exponentiation.
@type info: C{dict}
@return: An expression computing 2**(expr + info['log_offset']).
@rtype: L{Expression}
"""
#map (log2(x) - a) to x, i.e. do 1 << (x + a)
expr = expression.BinOp(nmlop.ADD, expr, expression.ConstantNumeric(info['log_offset'], expr.pos), expr.pos)
return expression.BinOp(nmlop.SHIFT_LEFT, expression.ConstantNumeric(1, expr.pos), expr, expr.pos)
def patch_variable_read(info, pos):
"""
Helper function to read special patch variables.
@param info: Generic information about the parameter to read, like parameter number and size.
@type info: C{dict}
@param pos: Position information in the source file.
@type pos: L{Position} or C{None}
@return: An expression that reads the special variables.
@rtype: L{Expression}
"""
expr = expression.PatchVariable(info['num'], pos)
if info['start'] != 0:
expr = expression.BinOp(nmlop.SHIFT_RIGHT, expr, expression.ConstantNumeric(info['start'], pos), pos)
if info['size'] != 32:
expr = expression.BinOp(nmlop.AND, expr, expression.ConstantNumeric((1 << info['size']) - 1, pos), pos)
if 'function' in info:
expr = info['function'](expr, info)
return expr
def patch_variable(info, pos):
return expression.SpecialParameter(generic.reverse_lookup(patch_variables, info), info, None, patch_variable_read, False, pos)
patch_variables = {
'starting_year' : {'num': 0x0B, 'start': 0, 'size': 32, 'function': add_1920},
'freight_trains' : {'num': 0x0E, 'start': 0, 'size': 32},
'plane_speed' : {'num': 0x10, 'start': 0, 'size': 32},
'base_sprite_2cc' : {'num': 0x11, 'start': 0, 'size': 32},
'map_type' : {'num': 0x13, 'start': 24, 'size': 2},
'map_min_edge' : {'num': 0x13, 'start': 20, 'size': 4, 'log_offset': 6, 'function': map_exponentiate},
'map_max_edge' : {'num': 0x13, 'start': 16, 'size': 4, 'log_offset': 6, 'function': map_exponentiate},
'map_x_edge' : {'num': 0x13, 'start': 12, 'size': 4, 'log_offset': 6, 'function': map_exponentiate},
'map_y_edge' : {'num': 0x13, 'start': 8, 'size': 4, 'log_offset': 6, 'function': map_exponentiate},
'map_size' : {'num': 0x13, 'start': 0, 'size': 8, 'log_offset': 12, 'function': map_exponentiate},
'max_height_level' : {'num': 0x14, 'start': 0, 'size': 32},
'base_sprite_foundations' : {'num': 0x15, 'start': 0, 'size': 32},
'base_sprite_shores' : {'num': 0x16, 'start': 0, 'size': 32},
}
def config_flag_read(bit, pos):
return expression.SpecialCheck((0x01, r'\70'), 0x85, (0, 1), bit, "PatchFlag({})".format(bit), varsize = 1, pos = pos)
def config_flag(info, pos):
return expression.SpecialParameter(generic.reverse_lookup(config_flags, info), info, None, config_flag_read, True, pos)
config_flags = {
'long_bridges' : 0x0F,
'gradual_loading' : 0x2C,
'bridge_speed_limits' : 0x34,
'newtrains' : 0x37,
'newrvs' : 0x38,
'newships' : 0x39,
'newplanes' : 0x3A,
'signals_on_traffic_side' : 0x3B,
'electrified_railways' : 0x3C,
'newhouses' : 0x59,
'wagon_speed_limits' : 0x5D,
'newindustries' : 0x67,
'temperate_snowline' : 0x6A,
'newcargos' : 0x6B,
'dynamic_engines' : 0x78,
'variable_runningcosts' : 0x7E,
}
def unified_maglev_read(info, pos):
bit0 = expression.BinOp(nmlop.HASBIT, expression.Parameter(expression.ConstantNumeric(0x85), pos), expression.ConstantNumeric(0x32), pos)
bit1 = expression.BinOp(nmlop.HASBIT, expression.Parameter(expression.ConstantNumeric(0x85), pos), expression.ConstantNumeric(0x33), pos)
shifted_bit1 = expression.BinOp(nmlop.SHIFT_LEFT, bit1, expression.ConstantNumeric(1))
return expression.BinOp(nmlop.OR, shifted_bit1, bit0)
def unified_maglev(info, pos):
return expression.SpecialParameter(generic.reverse_lookup(unified_maglev_var, info), info, None, unified_maglev_read, False, pos)
unified_maglev_var = {
'unified_maglev' : 0,
}
def setting_from_info(info, pos):
return expression.SpecialParameter(generic.reverse_lookup(settings, info), info, global_param_write, global_param_read, False, pos)
def item_to_id(item, pos):
if not isinstance(item.id, expression.ConstantNumeric):
raise generic.ScriptError("Referencing item '{}' with a non-constant id is not possible.".format(item.name), pos)
return expression.ConstantNumeric(item.id.value, pos)
def param_from_name(info, pos):
return expression.Parameter(expression.ConstantNumeric(info), pos)
def create_spritegroup_ref(info, pos):
return expression.SpriteGroupRef(expression.Identifier(info), [], pos)
cargo_numbers = {}
is_default_railtype_table = True
railtype_table = {'RAIL': 0, 'ELRL': 1, 'MONO': 1, 'MGLV': 2}
item_names = {}
settings = {}
named_parameters = {}
spritegroups = {'CB_FAILED': 'CB_FAILED'}
const_list = [
constant_numbers,
(global_parameters, param_from_info),
(misc_grf_bits, misc_grf_bit),
(patch_variables, patch_variable),
(named_parameters, param_from_name),
cargo_numbers,
railtype_table,
(item_names, item_to_id),
(settings, setting_from_info),
(config_flags, config_flag),
(unified_maglev_var, unified_maglev),
(spritegroups, create_spritegroup_ref),
]
def print_stats():
"""
Print statistics about used ids.
"""
if len(cargo_numbers) > 0:
# Ids FE and FF have special meanings in Action3, so we do not consider them valid ids.
generic.print_info("Cargo translation table: {}/{}".format(len(cargo_numbers), 0xFE))
if not is_default_railtype_table:
generic.print_info("Railtype translation table: {}/{}".format(len(railtype_table), 0x100))
nml-0.4.5/nml/output_dep.py 0000644 0005672 0005672 00000002333 13315644406 017007 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
# -*- coding: utf-8 -*-
import codecs
from nml import output_base
class OutputDEP(output_base.TextOutputBase):
"""
Class for output to a dependency file in makefile format.
"""
def __init__(self, filename, grf_filename):
output_base.TextOutputBase.__init__(self, filename)
self.grf_filename = grf_filename
def open_file(self):
return codecs.open(self.filename, 'w', 'utf-8')
def write(self, text):
self.file.write(self.grf_filename + ': ' + text + '\n')
def skip_sprite_checks(self):
return True
nml-0.4.5/nml/output_nfo.py 0000644 0005672 0005672 00000013616 13315644406 017027 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
# -*- coding: utf-8 -*-
import codecs, io
from nml import generic, grfstrings, output_base
from nml.actions import real_sprite
zoom_levels = {
0 : 'normal',
1 : 'zi4',
2 : 'zi2',
3 : 'zo2',
4 : 'zo4',
5 : 'zo8',
}
bit_depths = {
8 : '8bpp',
32 : '32bpp',
}
class OutputNFO(output_base.SpriteOutputBase):
def __init__(self, filename, start_sprite_num):
output_base.SpriteOutputBase.__init__(self, filename)
self.sprite_num = start_sprite_num
def open(self):
self.file = io.StringIO()
def open_file(self):
handle = codecs.open(self.filename, 'w', encoding='utf-8')
handle.write('// Automatically generated by GRFCODEC. Do not modify!\n'
'// (Info version 32)\n'
'// Escapes: 2+ 2- 2< 2> 2u< 2u> 2/ 2% 2u/ 2u% 2* 2& 2| 2^ 2sto = 2s 2rst = 2r 2psto 2ror = 2rot 2cmp 2ucmp 2<< 2u>> 2>>\n'
'// Escapes: 71 70 7= 7! 7< 7> 7G 7g 7gG 7GG 7gg 7c 7C\n'
'// Escapes: D= = DR D+ = DF D- = DC Du* = DM D* = DnF Du<< = DnC D<< = DO D& D| Du/ D/ Du% D%\n'
'// Format: spritenum imagefile depth xpos ypos xsize ysize xrel yrel zoom flags\n\n')
return handle
def print_byte(self, value):
value = self.prepare_byte(value)
self.file.write("\\b" + str(value) + " ")
def print_bytex(self, value, pretty_print = None):
value = self.prepare_byte(value)
if pretty_print is not None:
self.file.write(pretty_print + " ")
return
self.file.write("{:02X} ".format(value))
def print_word(self, value):
value = self.prepare_word(value)
self.file.write("\\w{:d} ".format(value))
def print_wordx(self, value):
value = self.prepare_word(value)
self.file.write("\\wx{:04X} ".format(value))
def print_dword(self, value):
value = self.prepare_dword(value)
self.file.write("\\d{:d} ".format(value))
def print_dwordx(self, value):
value = self.prepare_dword(value)
self.file.write("\\dx{:08X} ".format(value))
def print_string(self, value, final_zero = True, force_ascii = False):
assert self.in_sprite
self.file.write('"')
if not grfstrings.is_ascii_string(value):
if force_ascii:
raise generic.ScriptError("Expected ascii string but got a unicode string")
self.file.write('Þ') # b'\xC3\x9E'.decode('utf-8')
self.file.write(value.replace('"', '\\"'))
self.byte_count += grfstrings.get_string_size(value, final_zero, force_ascii)
self.file.write('" ')
if final_zero:
self.print_bytex(0)
# get_string_size already includes the final 0 byte
# but print_bytex also increases byte_count, so decrease
# it here by one to correct it.
self.byte_count -= 1
def print_decimal(self, value):
assert self.in_sprite
self.file.write(str(value) + " ")
def newline(self, msg = "", prefix = "\t"):
if msg != "": msg = prefix + "// " + msg
self.file.write(msg + "\n")
def comment(self, msg):
self.file.write("// " + msg + "\n")
def start_sprite(self, size, is_real_sprite = False):
output_base.SpriteOutputBase.start_sprite(self, size)
self.print_decimal(self.sprite_num)
self.sprite_num += 1
if not is_real_sprite:
self.file.write("* ")
self.print_decimal(size)
def print_sprite(self, sprite_list):
"""
@param sprite_list: List of non-empty real sprites for various bit depths / zoom levels
@type sprite_list: C{list} of L{RealSprite}
"""
self.start_sprite(0, True)
for i, sprite_info in enumerate(sprite_list):
self.file.write(sprite_info.file.value + " ")
self.file.write(bit_depths[sprite_info.bit_depth] + " ")
self.print_decimal(sprite_info.xpos.value)
self.print_decimal(sprite_info.ypos.value)
self.print_decimal(sprite_info.xsize.value)
self.print_decimal(sprite_info.ysize.value)
self.print_decimal(sprite_info.xrel.value)
self.print_decimal(sprite_info.yrel.value)
self.file.write(zoom_levels[sprite_info.zoom_level] + " ")
if (sprite_info.flags.value & real_sprite.FLAG_NOCROP) != 0:
self.file.write("nocrop ")
if sprite_info.mask_file is not None:
self.newline()
self.file.write("|\t")
self.file.write(sprite_info.mask_file.value)
self.file.write(" mask ")
mask_x, mask_y = sprite_info.mask_pos if sprite_info.mask_pos is not None else (sprite_info.xpos, sprite_info.ypos)
self.print_decimal(mask_x.value)
self.print_decimal(mask_y.value)
if i + 1 < len(sprite_list):
self.newline()
self.file.write("|\t")
self.end_sprite()
def print_empty_realsprite(self):
self.start_sprite(1)
self.print_bytex(0)
self.end_sprite()
def print_named_filedata(self, filename):
self.start_sprite(0, True)
self.file.write("** " + filename)
self.end_sprite()
nml-0.4.5/nml/unit.py 0000644 0005672 0005672 00000007777 13315644406 015617 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
# Available units, mapping of unit name to L{Unit} objects.
units = {}
def get_unit(name):
"""
Get a unit by name.
@param name: Name of the unit.
@type name: C{str}
@return: The requested unit.
@rtype: L{Unit}
"""
return units[name]
class Unit(object):
def __init__(self, name, type, convert, ottd_mul, ottd_shift):
"""
Unit definition.
Conversion factor works like this:
1 reference_unit = convert other_unit
So nfo_value = property_value / convert * property_specific_conversion_factor
To avoid using fractions, rational numbers (as 2-tuples) are used instead.
ottd_mul and ottd_shift are the values taken from OpenTTD's src/strings.cpp and
are used to calculate the displayed value by OpenTTD. If possible, adjust_values
increases or decreases the NFO value so that the desired display value is actually
achieved.
@ivar name: Name of the unit.
@type name: C{str}
@ivar type: Kind of unit.
@type type: C{str}
@ivar convert: Conversion factor of the unit.
@type convert: Either C{int} or a rational tuple (C{int}, C{int})
@ivar ottd_mul: OpenTTD multiplication factor for displaying a value of this unit.
@type ottd_mul: C{int}
@ivar ottd_shift: OpenTTD shift factor for displaying a value of this unit.
@type ottd_shift: C{int}
"""
self.name = name
self.type = type
self.convert = convert
self.ottd_mul = ottd_mul
self.ottd_shift = ottd_shift
def __str__(self):
return self.name
def add_unit(name, type, convert, ottd_mul, ottd_shift):
"""
Construct new unit, and add it to L{units}.
@param name: Name of the unit.
@type name: C{str}
@param type: Kind of unit.
@type type: C{str}
@param convert: Conversion factor of the unit.
@type convert: Either C{int} or a rational tuple (C{int}, C{int})
@param ottd_mul: OpenTTD multiplication factor for displaying a value of this unit.
@type ottd_mul: C{int}
@param ottd_shift: OpenTTD shift factor for displaying a value of this unit.
@type ottd_shift: C{int}
"""
unit = Unit(name, type, convert, ottd_mul, ottd_shift)
units[name] = unit
# name type convert mul shift
add_unit('nfo', 'nfo', 1, 1, 0) #don't convert, take value literal
#Speed (reference: m/s)
# name type convert mul shift
add_unit( 'mph', 'speed', (3125, 1397), 1, 0)
add_unit( 'km/h', 'speed', ( 18, 5), 103, 6)
add_unit( 'm/s', 'speed', 1, 1831, 12)
#Power (reference: hpI (imperial hp))
# name type convert mul shift
add_unit( 'hp', 'power', 1, 1, 0) # Default to imperial hp
add_unit( 'kW', 'power', (2211, 2965), 6109, 13)
add_unit( 'hpM', 'power', ( 731, 721), 4153, 12)
add_unit( 'hpI', 'power', 1, 1, 0)
#Weight (reference: ton)
# name type convert mul shift
add_unit( 'ton', 'weight', 1, 1, 0)
add_unit( 'tons', 'weight', 1, 1, 0)
add_unit( 'kg', 'weight', 1000, 1000, 0)
#Snowline height
# name type convert mul shift
add_unit('snow%', 'snowline', ( 255, 100), 1, 0)
nml-0.4.5/nml/ast/ 0000755 0005672 0005672 00000000000 13315644467 015042 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/nml/ast/tilelayout.py 0000644 0005672 0005672 00000012616 13315644406 017606 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, expression, global_constants
from nml.actions import action0properties
from nml.ast import base_statement, assignment
class TileLayout(base_statement.BaseStatement):
"""
'tile_layout' AST node. A TileLayout is a list of x,y-offset/tileID pairs.
The x and y offsets are from the northernmost tile of the industry/airport.
Additionally some extra properties can be stored in the TileLayout, like
the orientation of the airport.
@ivar name: The name of this layout by which it can be referenced later.
@type name: C{str}
@ivar tile_prop_list: List of offset/tileID and properties.
@type tile_prop_list: C{list} of L{LayoutTile} and L{Assignment}
@ivar tile_list: List of tile-offsets/tileIDs.
@type tile_list: C{list} of C{LayoutTile} with constant x and y values.
@ivar properties: table of all properties. Unknown property names are accepted and ignored.
@type properties: C{dict} with C{str} keys and L{ConstantNumeric} values
"""
def __init__(self, name, tile_list, pos):
base_statement.BaseStatement.__init__(self, "tile layout", pos, False, False)
self.name = name.value
self.tile_prop_list = tile_list
self.tile_list = []
self.properties = {}
def pre_process(self):
for tileprop in self.tile_prop_list:
if isinstance(tileprop, assignment.Assignment):
name = tileprop.name.value
if name in self.properties:
raise generic.ScriptError("Duplicate property {} in tile layout".format(name), tileprop.name.pos)
self.properties[name] = tileprop.value.reduce_constant(global_constants.const_list)
else:
assert isinstance(tileprop, LayoutTile)
x = tileprop.x.reduce_constant()
y = tileprop.y.reduce_constant()
tile = tileprop.tiletype.reduce(unknown_id_fatal = False)
if isinstance(tile, expression.Identifier) and tile.value == 'clear':
tile = expression.ConstantNumeric(0xFF)
self.tile_list.append(LayoutTile(x, y, tile))
if self.name in action0properties.tilelayout_names:
raise generic.ScriptError("A tile layout with name '{}' has already been defined.".format(self.name), self.pos)
action0properties.tilelayout_names[self.name] = self
def debug_print(self, indentation):
generic.print_dbg(indentation, 'TileLayout')
for tile in self.tile_list:
generic.print_dbg(indentation + 2, 'At {:d},{:d}:'.format(tile.x, tile.y))
tile.tiletype.debug_print(indentation + 4)
def get_action_list(self):
return []
def __str__(self):
return 'tilelayout {} {{\n\t{}\n}}\n'.format(self.name, '\n\t'.join(str(x) for x in self.tile_prop_list))
def get_size(self):
size = 2
for tile in self.tile_list:
size += 3
if not isinstance(tile.tiletype, expression.ConstantNumeric):
size += 2
return size
def write(self, file):
for tile in self.tile_list:
file.print_bytex(tile.x.value)
file.print_bytex(tile.y.value)
if isinstance(tile.tiletype, expression.ConstantNumeric):
file.print_bytex(tile.tiletype.value)
else:
if not isinstance(tile.tiletype, expression.Identifier):
raise generic.ScriptError("Invalid expression type for layout tile", tile.tiletype.pos)
if tile.tiletype.value not in global_constants.item_names:
raise generic.ScriptError("Unknown tile name", tile.tiletype.pos)
file.print_bytex(0xFE)
tile_id = global_constants.item_names[tile.tiletype.value].id
if not isinstance(tile_id, expression.ConstantNumeric):
raise generic.ScriptError("Tile '{}' cannot be used in a tilelayout, as its ID is not a constant.".format(tile.tiletype.value), tile.tiletype.pos)
file.print_wordx(tile_id.value)
file.newline()
file.print_bytex(0)
file.print_bytex(0x80)
file.newline()
class LayoutTile(object):
"""
Single tile that is part of a L{TileLayout}.
@ivar x: X-offset from the northernmost tile of the industry/airport.
@type x: L{Expression}
@ivar y: Y-offset from the northernmost tile of the industry/airport.
@type y: L{Expression}
@ivar tiletype: TileID of the tile to draw on the given offset.
@type tiletype: L{Expression}
"""
def __init__(self, x, y, tiletype):
self.x = x
self.y = y
self.tiletype = tiletype
def __str__(self):
return '{}, {}: {};'.format(self.x, self.y, self.tiletype)
nml-0.4.5/nml/ast/assignment.py 0000644 0005672 0005672 00000006007 13315644406 017560 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
class Assignment(object):
"""
Simple storage container for a name / value pair.
This class does not enforce any type information.
Create a subclass to do this, or hard-code it in the parser.
@ivar name: Name of the parameter / property / whatever to be set.
@type name: Anything
@ivar value: Value to assign to
@type value: Anything
@ivar pos: Position information of the assignment
@type pos: L{Position}
"""
def __init__(self, name, value, pos):
self.name = name
self.value = value
self.pos = pos
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Assignment')
generic.print_dbg(indentation + 2, 'Name:')
self.name.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Value:')
self.value.debug_print(indentation + 4)
def __str__(self):
return "{}: {};".format(self.name, self.value)
class UnitAssignment(Assignment):
"""
Storage container where the value can have a unit.
@ivar unit: Unit of the value, or not C{None}
@type unit: L{Unit}
"""
def __init__(self, name, value, unit, pos):
Assignment.__init__(self, name, value, pos)
self.unit = unit
def debug_print(self, indentation):
Assignment.debug_print(self, indentation)
generic.print_dbg(indentation + 2, 'Unit:')
if self.unit is None:
generic.print_dbg(indentation + 4, 'None')
else:
generic.print_dbg(indentation + 4, self.unit)
def __str__(self):
if self.unit is None:
return Assignment.__str__(self)
else:
return "{}: {} {};".format(self.name, self.value, self.unit.name)
class Range(object):
"""
Storage container for a range of values (inclusive). This Contains
a minimum value and optionally also a maximum value. If the maximum
values is omitted, the minimum is also used as maximum.
@ivar min: The minimum value of this range.
@type min: L{Expression}
@ivar max: The maximum value of this range.
@type max: L{Expression} or C{None}
"""
def __init__(self, min, max):
self.min = min
self.max = max
def __str__(self):
if self.max is None:
return str(self.min)
return "{} .. {}".format(self.min, self.max)
nml-0.4.5/nml/ast/skipall.py 0000644 0005672 0005672 00000002262 13315644406 017046 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions import action7
from nml.ast import base_statement
from nml import generic
class SkipAll(base_statement.BaseStatement):
"""
Skip everything after this statement.
"""
def __init__(self, pos):
base_statement.BaseStatement.__init__(self, "exit-statement", pos)
def get_action_list(self):
return [action7.UnconditionalSkipAction(9, 0)]
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Skip all')
def __str__(self):
return "exit;\n"
nml-0.4.5/nml/ast/font.py 0000644 0005672 0005672 00000006340 13315644406 016356 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, expression
from nml.actions import action12
from nml.ast import base_statement, sprite_container
class FontGlyphBlock(base_statement.BaseStatement, sprite_container.SpriteContainer):
"""
AST class for a font_glyph block
Syntax: font_glyph(
@ivar font_size: Size of the font to provide characters for (NORMAL/SMALL/LARGE/MONO)
@type font_size: L{Expression}
@ivar base_char: First character to replace
@type base_char: L{Expression}
@ivar image_file: Default file to use for the contained real sprites (none if N/A)
@type image_file: L{StringLiteral} or C{None}
@ivar sprite_list: List of real sprites
@type sprite_list: C{list} of L{RealSprite}
"""
def __init__(self, param_list, sprite_list, name, pos):
base_statement.BaseStatement.__init__(self, "font_glyph-block", pos)
sprite_container.SpriteContainer.__init__(self, "font_glyph-block", name)
if not (2 <= len(param_list) <= 3):
raise generic.ScriptError("font_glyph-block requires 2 or 3 parameters, encountered " + str(len(param_list)), pos)
self.font_size = param_list[0]
self.base_char = param_list[1]
self.image_file = param_list[2].reduce() if len(param_list) >= 3 else None
if self.image_file is not None and not isinstance(self.image_file, expression.StringLiteral):
raise generic.ScriptError("font_glyph-block parameter 3 'file' must be a string literal", self.image_file.pos)
self.sprite_list = sprite_list
self.add_sprite_data(self.sprite_list, self.image_file, pos)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Load font glyphs, starting at', self.base_char)
generic.print_dbg(indentation + 2, 'Font size: ', self.font_size)
generic.print_dbg(indentation + 2, 'Source: ', self.image_file.value if self.image_file is not None else 'None')
generic.print_dbg(indentation + 2, 'Sprites:')
for sprite in self.sprite_list:
sprite.debug_print(indentation + 4)
def get_action_list(self):
return action12.parse_action12(self)
def __str__(self):
name = str(self.block_name) if self.block_name is not None else ""
params = [self.font_size, self.base_char]
if self.image_file is not None:
params.append(self.image_file)
ret = "font_glyph {}({}) {{\n".format(name, ", ".join(str(param) for param in params))
for sprite in self.sprite_list:
ret += "\t{}\n".format(sprite)
ret += "}\n"
return ret
nml-0.4.5/nml/ast/sprite_container.py 0000644 0005672 0005672 00000006471 13315644406 020765 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
class SpriteContainer(object):
"""
Base class for all AST Nodes that contain real sprites
Note that this does not inherit from BaseStatement,
to (amongst other things) avoid various multiple inheritance issues with Spritesets
@ivar block_type: Type of block (e.g. 'spriteset' ,'replace')
@type block_type: C{str}
@ivar block_name: Block-specific name
@type block_name: L{Identifier}, or C{None} if N/A
@ivar sprite_data: Mapping of (zoom level, bit-depth) to (sprite list, default file)
@type sprite_data: C{dict} that maps (C{tuple} of (C{int}, C{int})) to (C{tuple} of (C{list} of (L{RealSprite}, L{RecolourSprite} or L{TemplateUsage}), L{StringLiteral} or C{None}, L{Position}))
"""
sprite_blocks = {}
def __init__(self, block_type, block_name):
self.block_type = block_type
self.block_name = block_name
self.sprite_data = {}
if block_name is not None:
if block_name.value in SpriteContainer.sprite_blocks:
raise generic.ScriptError("Block with name '{}' is already defined.".format(block_name.value), block_name.pos)
SpriteContainer.sprite_blocks[block_name.value] = self
def add_sprite_data(self, sprite_list, default_file, pos, zoom_level = 0, bit_depth = 8, default_mask_file = None):
assert zoom_level in range(0, 6)
assert bit_depth in (8, 32)
key = (zoom_level, bit_depth)
if key in self.sprite_data:
msg = ("Sprites are already defined for {} '{}' for this zoom " +
"level / bit depth combination. This data will be overridden.")
msg = msg.format(self.block_type, self.block_name.value)
generic.print_warning(msg, pos)
self.sprite_data[key] = (sprite_list, default_file, default_mask_file, pos)
def get_all_sprite_data(self):
"""
Get all sprite data.
Sorting makes sure that the order is consistent, and that the normal zoom, 8bpp sprites appear first.
@return: List of 6-tuples (sprite_list, default_file, default_mask_file, position, zoom_level, bit_depth).
@rtype: C{list} of C{tuple} of (C{list} of (L{RealSprite}, L{RecolourSprite} or L{TemplateUsage}), L{StringLiteral} or C{None}, L{Position}, C{int}, C{int})
"""
return [val + key for key, val in sorted(self.sprite_data.items())]
@classmethod
def resolve_sprite_block(cls, block_name):
if block_name.value in cls.sprite_blocks:
return cls.sprite_blocks[block_name.value]
raise generic.ScriptError("Undeclared block identifier '{}' encountered".format(block_name.value), block_name.pos)
nml-0.4.5/nml/ast/snowline.py 0000644 0005672 0005672 00000015206 13315644406 017247 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import datetime
from nml import generic, expression, nmlop
from nml.actions import action0
from nml.ast import base_statement
class Snowline(base_statement.BaseStatement):
"""
Snowline curve throughout the year.
@ivar type: Type of snowline.
@type type: C{str}
@ivar date_heights: Height of the snow line at given days in the year.
@type date_heights: C{list} of L{UnitAssignment}
"""
def __init__(self, line_type, height_data, pos):
base_statement.BaseStatement.__init__(self, "snowline-block", pos)
if line_type.value not in ('equal', 'linear'):
raise generic.ScriptError('Unknown type of snow line (only "equal" and "linear" are supported)', line_type.pos)
self.type = line_type.value
self.date_heights = height_data
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Snowline (type={})'.format(self.type))
for dh in self.date_heights:
dh.debug_print(indentation + 2)
def __str__(self):
return 'snowline ({}) {{\n\t{}\n}}\n'.format(str(self.type), '\n\t'.join(str(x) for x in self.date_heights))
def get_action_list(self):
return action0.get_snowlinetable_action(compute_table(self))
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 = expression.BinOp(nmlop.MUL, height, expression.ConstantNumeric(mul, height.pos), height.pos)
height = expression.BinOp(nmlop.ADD, height, expression.ConstantNumeric(int(div / 2), height.pos), height.pos)
height = expression.BinOp(nmlop.DIV, height, expression.ConstantNumeric(div, height.pos), height.pos)
# 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
nml-0.4.5/nml/ast/conditional.py 0000644 0005672 0005672 00000005467 13315644406 017724 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import global_constants, generic
from nml.actions import action7
from nml.ast import base_statement
class ConditionalList(base_statement.BaseStatementList):
"""
Wrapper for a complete if/else if/else if/else block.
"""
def __init__(self, conditionals):
assert len(conditionals) > 0
base_statement.BaseStatementList.__init__(self, "if/else-block", conditionals[0].pos,
base_statement.BaseStatementList.LIST_TYPE_SKIP, conditionals, in_item = True)
def get_action_list(self):
return action7.parse_conditional_block(self)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Conditional')
base_statement.BaseStatementList.debug_print(self, indentation + 2)
def __str__(self):
ret = ''
ret += ' else '.join([str(stmt) for stmt in self.statements])
ret += '\n'
return ret
class Conditional(base_statement.BaseStatementList):
"""
Condition along with the code that has to be executed if the condition
evaluates to some value not equal to 0.
@ivar expr: The expression where the execution of code in this block depends on.
@type expr: L{Expression}
"""
def __init__(self, expr, block, pos):
base_statement.BaseStatementList.__init__(self, "if/else-block", pos,
base_statement.BaseStatementList.LIST_TYPE_SKIP, block, in_item = True)
self.expr = expr
def pre_process(self):
if self.expr is not None:
self.expr = self.expr.reduce(global_constants.const_list)
base_statement.BaseStatementList.pre_process(self)
def debug_print(self, indentation):
if self.expr is not None:
generic.print_dbg(indentation, 'Expression:')
self.expr.debug_print(indentation + 2)
generic.print_dbg(indentation, 'Block:')
base_statement.BaseStatementList.debug_print(self, indentation + 2)
def __str__(self):
ret = ''
if self.expr is not None:
ret += 'if ({})'.format(self.expr)
ret += ' {\n'
ret += base_statement.BaseStatementList.__str__(self)
ret += '}\n'
return ret
nml-0.4.5/nml/ast/general.py 0000644 0005672 0005672 00000005102 13315644406 017020 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from nml.ast import base_statement
feature_ids = {
'FEAT_TRAINS': 0x00,
'FEAT_ROADVEHS': 0x01,
'FEAT_SHIPS': 0x02,
'FEAT_AIRCRAFT': 0x03,
'FEAT_STATIONS': 0x04,
'FEAT_CANALS': 0x05,
'FEAT_BRIDGES': 0x06,
'FEAT_HOUSES': 0x07,
'FEAT_GLOBALVARS': 0x08,
'FEAT_INDUSTRYTILES': 0x09,
'FEAT_INDUSTRIES': 0x0A,
'FEAT_CARGOS': 0x0B,
'FEAT_SOUNDEFFECTS': 0x0C,
'FEAT_AIRPORTS': 0x0D,
'FEAT_SIGNALS': 0x0E,
'FEAT_OBJECTS': 0x0F,
'FEAT_RAILTYPES': 0x10,
'FEAT_AIRPORTTILES': 0x11,
}
def feature_name(feature):
"""
Given a feature number, return the identifier as normally used for that
feature in nml files.
@param feature: The feature number to convert to a string.
@type feature: C{int}
@return: String with feature as used in nml files.
@rtype: C{str}
"""
for name, num in list(feature_ids.items()):
if num == feature:
return name
assert False, "Invalid feature number"
def parse_feature(expr):
"""
Parse an expression into a valid feature number.
@param expr: Expression to parse
@type expr: L{Expression}
@return: A constant number representing the parsed feature
@rtype: L{ConstantNumeric}
"""
expr = expr.reduce_constant([feature_ids])
if expr.value not in list(feature_ids.values()):
raise generic.ScriptError("Invalid feature '{:02X}' encountered.".format(expr.value), expr.pos)
return expr
class MainScript(base_statement.BaseStatementList):
def __init__(self, statements):
assert len(statements) > 0
base_statement.BaseStatementList.__init__(self, "main script", statements[0].pos,
base_statement.BaseStatementList.LIST_TYPE_NONE, statements, False, False, False, False)
def __str__(self):
res = ""
for stmt in self.statements:
res += str(stmt) + '\n'
return res
nml-0.4.5/nml/ast/sort_vehicles.py 0000644 0005672 0005672 00000004355 13315644406 020265 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, global_constants
from nml.ast import base_statement, general
from nml.actions import action0
class SortVehicles(base_statement.BaseStatement):
"""
AST-node representing a sort-vehicles block.
@ivar feature: Feature of the item
@type feature: L{ConstantNumeric}
@ivar vehid_list: List of vehicle ids.
@type vehid_list: L{Array}.
"""
def __init__(self, params, pos):
base_statement.BaseStatement.__init__(self, "sort-block", pos)
if len(params) != 2:
raise generic.ScriptError("Sort-block requires exactly two parameters, got {:d}".format(len(params)), self.pos)
self.feature = general.parse_feature(params[0])
self.vehid_list = params[1]
def pre_process(self):
self.vehid_list = self.vehid_list.reduce(global_constants.const_list)
if not isinstance(self.vehid_list, expression.Array) or not all([isinstance(x, expression.ConstantNumeric) for x in self.vehid_list.values]):
raise generic.ScriptError("Second parameter is not an array of one of the items in it could not be reduced to a constant number", self.pos)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Sort, feature', hex(self.feature.value))
for id in self.vehid_list.values:
generic.print_dbg(indentation + 2, 'Vehicle id:', id)
def get_action_list(self):
return action0.parse_sort_block(self.feature.value, self.vehid_list.values)
def __str__(self):
return 'sort({:d}, {});\n'.format(self.feature.value, self.vehid_list)
nml-0.4.5/nml/ast/alt_sprites.py 0000644 0005672 0005672 00000013416 13315644406 017743 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic
from nml.ast import base_statement, sprite_container
"""
Store if there are any 32bpp sprites,
if so ask to enable the 32bpp blitter via action14
"""
any_32bpp_sprites = False
zoom_levels = {
'ZOOM_LEVEL_NORMAL' : 0,
'ZOOM_LEVEL_IN_4X' : 1,
'ZOOM_LEVEL_IN_2X' : 2,
'ZOOM_LEVEL_OUT_2X' : 3,
'ZOOM_LEVEL_OUT_4X' : 4,
'ZOOM_LEVEL_OUT_8X' : 5,
}
bit_depths = {
'BIT_DEPTH_8BPP' : 8,
'BIT_DEPTH_32BPP' : 32.
}
class AltSpritesBlock(base_statement.BaseStatement):
"""
AST Node for alternative graphics. These are normally 32bpp graphics, possible
for a higher zoom-level than the default sprites.
Syntax: alternative_sprites(name, zoom_level, bit_depth[, image_file])
@ivar name: The name of the replace/font_glyph/replace_new/spriteset/base_graphics-block this
block contains alternative graphics for.
@type name: L{expression.Identifier}
@ivar zoom_level: The zoomlevel these graphics are for.
@type zoom_level: C{int}
@ivar bit_depth: Bit depth these graphics are for
@type bit_depth: C{int}
@ivar image_file: Default graphics file for the sprites in this block.
@type image_file: L{expression.StringLiteral} or C{None}
@ivar mask_file: Default graphics file for the mask sprites in this block.
@type mask_file: L{expression.StringLiteral} or C{None}
@ivar sprite_list: List of real sprites or templates expanding to real sprites.
@type sprite_list: Heterogeneous C{list} of L{RealSprite}, L{TemplateUsage}
"""
def __init__(self, param_list, sprite_list, pos):
base_statement.BaseStatement.__init__(self, "alt_sprites-block", pos)
if not (3 <= len(param_list) <= 5):
raise generic.ScriptError("alternative_sprites-block requires 3 or 4 parameters, encountered " + str(len(param_list)), pos)
self.name = param_list[0]
if not isinstance(self.name, expression.Identifier):
raise generic.ScriptError("alternative_sprites parameter 1 'name' must be an identifier", self.name.pos)
if isinstance(param_list[1], expression.Identifier) and param_list[1].value in zoom_levels:
self.zoom_level = zoom_levels[param_list[1].value]
else:
raise generic.ScriptError("value for alternative_sprites parameter 2 'zoom level' is not a valid zoom level", param_list[1].pos)
if isinstance(param_list[2], expression.Identifier) and param_list[2].value in bit_depths:
self.bit_depth = bit_depths[param_list[2].value]
else:
raise generic.ScriptError("value for alternative_sprites parameter 3 'bit depth' is not a valid bit depthl", param_list[2].pos)
global any_32bpp_sprites
if self.bit_depth == 32: any_32bpp_sprites = True
if len(param_list) >= 4:
self.image_file = param_list[3].reduce()
if not isinstance(self.image_file, expression.StringLiteral):
raise generic.ScriptError("alternative_sprites-block parameter 4 'file' must be a string literal", self.image_file.pos)
else:
self.image_file = None
if len(param_list) >= 5:
self.mask_file = param_list[4].reduce()
if not isinstance(self.mask_file, expression.StringLiteral):
raise generic.ScriptError("alternative_sprites-block parameter 5 'mask_file' must be a string literal", self.mask_file.pos)
if not self.bit_depth == 32:
raise generic.ScriptError("A mask file may only be specified for 32 bpp sprites.", self.mask_file.pos)
else:
self.mask_file = None
self.sprite_list = sprite_list
def pre_process(self):
block = sprite_container.SpriteContainer.resolve_sprite_block(self.name)
block.add_sprite_data(self.sprite_list, self.image_file, self.pos, self.zoom_level, self.bit_depth, self.mask_file)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Alternative sprites')
generic.print_dbg(indentation + 2, 'Replacement for sprite:', self.name)
generic.print_dbg(indentation + 2, 'Zoom level:', self.zoom_level)
generic.print_dbg(indentation + 2, 'Bit depth:', self.bit_depth)
generic.print_dbg(indentation + 2, 'Source:', self.image_file.value if self.image_file is not None else 'None')
generic.print_dbg(indentation + 2, 'Mask source:', self.mask_file.value if self.mask_file is not None else 'None')
generic.print_dbg(indentation + 2, 'Sprites:')
for sprite in self.sprite_list:
sprite.debug_print(indentation + 4)
def get_action_list(self):
return []
def __str__(self):
params = [self.name, generic.reverse_lookup(zoom_levels, self.zoom_level), generic.reverse_lookup(bit_depths, self.bit_depth)]
if self.image_file is not None: params.append(self.image_file)
if self.mask_file is not None: params.append(self.mask_file)
ret = "alternative_sprites({}) {{\n".format(", ".join(str(p) for p in params))
for sprite in self.sprite_list:
ret += "\t{}\n".format(sprite)
ret += "}\n"
return ret
nml-0.4.5/nml/ast/deactivate.py 0000644 0005672 0005672 00000003032 13315644406 017514 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions import actionE
from nml.ast import base_statement
from nml import expression, generic
class DeactivateBlock(base_statement.BaseStatement):
def __init__(self, grfid_list, pos):
base_statement.BaseStatement.__init__(self, "deactivate()", pos)
self.grfid_list = grfid_list
def pre_process(self):
# Parse (string-)expressions to integers
self.grfid_list = [expression.parse_string_to_dword(grfid.reduce()) for grfid in self.grfid_list]
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Deactivate other newgrfs:')
for grfid in self.grfid_list:
grfid.debug_print(indentation + 2)
def get_action_list(self):
return actionE.parse_deactivate_block(self)
def __str__(self):
return 'deactivate({});\n'.format(', '.join(str(grfid) for grfid in self.grfid_list))
nml-0.4.5/nml/ast/grf.py 0000644 0005672 0005672 00000030377 13315644406 016175 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, grfstrings, global_constants
from nml.actions import action8, action14
from nml.ast import base_statement
palette_node = None
blitter_node = None
"""
Statistics about registers used for parameters.
The 1st field is the largest parameter register used.
The 2nd field is the maximum amount of parameter registers available. This is where L{action6.free_parameters} begins.
"""
param_stats = [0, 0x40]
def print_stats():
"""
Print statistics about used ids.
"""
if param_stats[0] > 0:
generic.print_info("GRF parameter registers: {}/{}".format(param_stats[0], param_stats[1]))
def set_palette_used(pal):
"""
Set the used palette in the action 14 node, if applicable
@param pal: Palette to use
@type pal: C{str} of length 1
"""
if palette_node:
palette_node.pal = pal
def set_preferred_blitter(blitter):
"""
Set the preferred blitter in the action14 node, if applicable
@param blitter: The blitter to set
@type blitter: C{str} of length 1
"""
if blitter_node:
blitter_node.blitter = blitter
class GRF(base_statement.BaseStatement):
"""
AST Node for a grf block, that supplies (static) information about the GRF
This is equivalent to actions 8 and 14
@ivar name: Name of the GRF (short)
@type name: L{Expression}, should be L{String} else user error
@ivar desc: Description of the GRF (longer)
@type name: L{Expression}, should be L{String} else user error
@ivar grfid: Globally unique identifier of the GRF
@type grfid: L{Expression}, should be L{StringLiteral} of 4 bytes else user error
@ivar version: Version of this GRF
@type version: L{Expression}
@ivar min_compatible_version: Minimum (older) version of the same GRF that it is compatible with
@type min_compatible_version: L{Expression}
@ivar params: List of user-configurable GRF parameters
@type params: C{list} of L{ParameterDescription}
"""
def __init__(self, alist, pos):
base_statement.BaseStatement.__init__(self, "grf-block", pos, False, False)
self.name = None
self.desc = None
self.url = None
self.grfid = None
self.version = None
self.min_compatible_version = None
self.params = []
for assignment in alist:
if isinstance(assignment, ParameterDescription):
self.params.append(assignment)
elif assignment.name.value == "name": self.name = assignment.value
elif assignment.name.value == "desc": self.desc = assignment.value
elif assignment.name.value == "url": self.url = assignment.value
elif assignment.name.value == "grfid": self.grfid = assignment.value
elif assignment.name.value == "version": self.version = assignment.value
elif assignment.name.value == "min_compatible_version": self.min_compatible_version = assignment.value
else: raise generic.ScriptError("Unknown item in GRF-block: " + str(assignment.name), assignment.name.pos)
def register_names(self):
generic.OnlyOnce.enforce(self, "GRF-block")
def pre_process(self):
if None in (self.name, self.desc, self.grfid, self.version, self.min_compatible_version):
raise generic.ScriptError("A GRF-block requires the 'name', 'desc', 'grfid', 'version' and 'min_compatible_version' properties to be set.", self.pos)
self.grfid = self.grfid.reduce()
global_constants.constant_numbers['GRFID'] = expression.parse_string_to_dword(self.grfid)
self.name = self.name.reduce()
if not isinstance(self.name, expression.String):
raise generic.ScriptError("GRF-name must be a string", self.name.pos)
grfstrings.validate_string(self.name)
self.desc = self.desc.reduce()
if not isinstance(self.desc, expression.String):
raise generic.ScriptError("GRF-description must be a string", self.desc.pos)
grfstrings.validate_string(self.desc)
if self.url is not None:
self.url = self.url.reduce()
if not isinstance(self.url, expression.String):
raise generic.ScriptError("URL must be a string", self.url.pos)
grfstrings.validate_string(self.url)
self.version = self.version.reduce_constant()
self.min_compatible_version = self.min_compatible_version.reduce_constant()
global param_stats
param_num = 0
for param in self.params:
param.pre_process(expression.ConstantNumeric(param_num))
param_num = param.num.value + 1
if param_num > param_stats[1]:
raise generic.ScriptError("No free parameters available. Consider assigning manually and combine multiple bool parameters into a single bitmask parameter using .", self.pos)
if param_num > param_stats[0]:
param_stats[0] = param_num
def debug_print(self, indentation):
generic.print_dbg(indentation, 'GRF')
generic.print_dbg(indentation + 2, 'grfid:')
self.grfid.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Name:')
self.name.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Description:')
self.desc.debug_print(indentation + 4)
if self.url is not None:
generic.print_dbg(indentation + 2, 'URL:')
self.url.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Version:')
self.version.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Minimal compatible version:')
self.min_compatible_version.debug_print(indentation + 4)
def get_action_list(self):
global palette_node, blitter_node
palette_node = action14.UsedPaletteNode("A")
blitter_node = action14.BlitterNode("8")
action14_root = action14.BranchNode("INFO")
action14.grf_name_desc_actions(action14_root, self.name, self.desc, self.url, self.version, self.min_compatible_version)
action14.param_desc_actions(action14_root, self.params)
action14_root.subnodes.append(palette_node)
action14_root.subnodes.append(blitter_node)
return action14.get_actions(action14_root) + [action8.Action8(self.grfid, self.name, self.desc)]
def __str__(self):
ret = 'grf {\n'
ret += '\tgrfid: {};\n'.format(str(self.grfid))
ret += '\tname: {};\n'.format(str(self.name))
ret += '\tdesc: {};\n'.format(str(self.desc))
if self.url is not None:
ret += '\turl: {};\n'.format(self.url)
ret += '\tversion: {};\n'.format(self.version)
ret += '\tmin_compatible_version: {};\n'.format(self.min_compatible_version)
for param in self.params:
ret += str(param)
ret += '}\n'
return ret
class ParameterSetting(object):
def __init__(self, name, value_list):
self.name = name
self.value_list = value_list
self.type = 'int'
self.name_string = None
self.desc_string = None
self.min_val = None
self.max_val = None
self.def_val = None
self.bit_num = None
self.val_names = []
self.properties_set = set()
def pre_process(self):
for set_val in self.value_list:
self.set_property(set_val.name.value, set_val.value)
def __str__(self):
ret = "\t\t{} {{\n".format(self.name)
for val in self.value_list:
if val.name.value == 'names':
ret += "\t\t\tnames: {\n"
for name in val.value.values:
ret += "\t\t\t\t{}: {};\n".format(name.name, name.value)
ret += "\t\t\t};\n"
else:
ret += "\t\t\t{}: {};\n".format(val.name, val.value)
ret += "\t\t}\n"
return ret
def set_property(self, name, value):
"""
Set a single parameter property
@param name: Name of the property to be set
@type name: C{str}
@param value: Value of the property (note: may be an array)
@type value: L{Expression}
"""
if name in self.properties_set:
raise generic.ScriptError("You cannot set the same property twice in a parameter description block", value.pos)
self.properties_set.add(name)
if name == 'names':
for name_value in value.values:
num = name_value.name.reduce_constant().value
desc = name_value.value
if not isinstance(desc, expression.String):
raise generic.ScriptError("setting name description must be a string", desc.pos)
self.val_names.append((num, desc))
return
value = value.reduce(unknown_id_fatal = False)
if name == 'type':
if not isinstance(value, expression.Identifier) or (value.value != 'int' and value.value != 'bool'):
raise generic.ScriptError("setting-type must be either 'int' or 'bool'", value.pos)
self.type = value.value
elif name == 'name':
if not isinstance(value, expression.String):
raise generic.ScriptError("setting-name must be a string", value.pos)
self.name_string = value
elif name == 'desc':
if not isinstance(value, expression.String):
raise generic.ScriptError("setting-description must be a string", value.pos)
self.desc_string = value
elif name == 'bit':
if self.type != 'bool':
raise generic.ScriptError("setting-bit is only valid for 'bool' settings", value.pos)
self.bit_num = value.reduce_constant()
elif name == 'min_value':
if self.type != 'int':
raise generic.ScriptError("setting-min_value is only valid for 'int' settings", value.pos)
self.min_val = value.reduce_constant()
elif name == 'max_value':
if self.type != 'int':
raise generic.ScriptError("setting-max_value is only valid for 'int' settings", value.pos)
self.max_val = value.reduce_constant()
elif name == 'def_value':
self.def_val = value.reduce_constant()
if self.type == 'bool' and self.def_val.value != 0 and self.def_val.value != 1:
raise generic.ScriptError("setting-def_value must be either 0 or 1 for 'bool' settings", value.pos)
else:
raise generic.ScriptError("Unknown setting-property " + name, value.pos)
class ParameterDescription(object):
def __init__(self, setting_list, num = None, pos = None):
self.setting_list = setting_list
self.num = num
self.pos = pos
def __str__(self):
ret = "\tparam"
if self.num:
ret += " " + str(self.num)
ret += " {\n"
for setting in self.setting_list:
ret += str(setting)
ret += "\t}\n"
return ret
def pre_process(self, num):
if self.num is None: self.num = num
self.num = self.num.reduce_constant()
for setting in self.setting_list:
setting.pre_process()
for setting in self.setting_list:
if setting.type == 'int':
if len(self.setting_list) > 1:
raise generic.ScriptError("When packing multiple settings in one parameter only bool settings are allowed", self.pos)
global_constants.settings[setting.name.value] = {'num': self.num.value, 'size': 4}
else:
bit = 0 if setting.bit_num is None else setting.bit_num.value
global_constants.misc_grf_bits[setting.name.value] = {'param': self.num.value, 'bit': bit}
nml-0.4.5/nml/ast/spriteblock.py 0000644 0005672 0005672 00000025663 13315644406 017742 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, global_constants
from nml.actions import action2, action2layout, action2real, real_sprite
from nml.ast import base_statement, sprite_container
class TemplateDeclaration(base_statement.BaseStatement):
def __init__(self, name, param_list, sprite_list, pos):
base_statement.BaseStatement.__init__(self, "template declaration", pos, False, False)
self.name = name
self.param_list = param_list
self.sprite_list = sprite_list
def pre_process(self):
#check that all templates that are referred to exist at this point
#This prevents circular dependencies
for sprite in self.sprite_list:
if isinstance(sprite, real_sprite.TemplateUsage):
if sprite.name.value == self.name.value:
raise generic.ScriptError("Sprite template '{}' includes itself.".format(sprite.name.value), self.pos)
elif sprite.name.value not in real_sprite.sprite_template_map:
raise generic.ScriptError("Encountered unknown template identifier: " + sprite.name.value, sprite.pos)
#Register template
if self.name.value not in real_sprite.sprite_template_map:
real_sprite.sprite_template_map[self.name.value] = self
else:
raise generic.ScriptError("Template named '{}' is already defined, first definition at {}".format(self.name.value, real_sprite.sprite_template_map[self.name.value].pos), self.pos)
def get_labels(self):
labels = {}
offset = 0
for sprite in self.sprite_list:
sprite_labels, num_sprites = sprite.get_labels()
for lbl, lbl_offset in list(sprite_labels.items()):
if lbl in labels:
raise generic.ScriptError("Duplicate label encountered; '{}' already exists.".format(lbl), self.pos)
labels[lbl] = lbl_offset + offset
offset += num_sprites
return labels, offset
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Template declaration:', self.name.value)
generic.print_dbg(indentation + 2, 'Parameters:')
for param in self.param_list:
param.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Sprites:')
for sprite in self.sprite_list:
sprite.debug_print(indentation + 4)
def get_action_list(self):
return []
def __str__(self):
ret = "template {}({}) {{\n".format(str(self.name), ", ".join([str(param) for param in self.param_list]))
for sprite in self.sprite_list:
ret += "\t{}\n".format(sprite)
ret += "}\n"
return ret
spriteset_base_class = action2.make_sprite_group_class(True, True, False, cls_is_relocatable = True)
class SpriteSet(spriteset_base_class, sprite_container.SpriteContainer):
def __init__(self, param_list, sprite_list, pos):
base_statement.BaseStatement.__init__(self, "spriteset", pos, False, False)
if not (1 <= len(param_list) <= 2):
raise generic.ScriptError("Spriteset requires 1 or 2 parameters, encountered " + str(len(param_list)), pos)
name = param_list[0]
if not isinstance(name, expression.Identifier):
raise generic.ScriptError("Spriteset parameter 1 'name' should be an identifier", name.pos)
sprite_container.SpriteContainer.__init__(self, "spriteset", name)
self.initialize(name)
if len(param_list) >= 2:
self.image_file = param_list[1].reduce()
if not isinstance(self.image_file, expression.StringLiteral):
raise generic.ScriptError("Spriteset-block parameter 2 'file' must be a string literal", self.image_file.pos)
else:
self.image_file = None
self.sprite_list = sprite_list
self.action1_num = None #set number in action1
self.labels = {} #mapping of real sprite labels to offsets
self.add_sprite_data(self.sprite_list, self.image_file, pos)
def pre_process(self):
spriteset_base_class.pre_process(self)
offset = 0
for sprite in self.sprite_list:
sprite_labels, num_sprites = sprite.get_labels()
for lbl, lbl_offset in list(sprite_labels.items()):
if lbl in self.labels:
raise generic.ScriptError("Duplicate label encountered; '{}' already exists.".format(lbl), self.pos)
self.labels[lbl] = lbl_offset + offset
offset += num_sprites
def collect_references(self):
return []
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Sprite set:', self.name.value)
generic.print_dbg(indentation + 2, 'Source: ', self.image_file.value if self.image_file is not None else 'None')
generic.print_dbg(indentation + 2, 'Sprites:')
for sprite in self.sprite_list:
sprite.debug_print(indentation + 4)
def get_action_list(self):
# Actions are created when parsing the action2s, not here
return []
def __str__(self):
filename = (", " + str(self.image_file)) if self.image_file is not None else ""
ret = "spriteset({}{}) {{\n".format(self.name, filename)
for sprite in self.sprite_list:
ret += "\t{}\n".format(str(sprite))
ret += "}\n"
return ret
spritegroup_base_class = action2.make_sprite_group_class(False, True, False)
class SpriteGroup(spritegroup_base_class):
def __init__(self, name, spriteview_list, pos = None):
base_statement.BaseStatement.__init__(self, "spritegroup", pos, False, False)
self.initialize(name)
self.spriteview_list = spriteview_list
def pre_process(self):
for spriteview in self.spriteview_list:
spriteview.pre_process()
spritegroup_base_class.pre_process(self)
def collect_references(self):
return []
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Sprite group:', self.name.value)
for spriteview in self.spriteview_list:
spriteview.debug_print(indentation + 2)
def get_action_list(self):
action_list = []
if self.prepare_act2_output():
for feature in sorted(self.feature_set):
action_list.extend(action2real.get_real_action2s(self, feature))
return action_list
def __str__(self):
ret = "spritegroup {} {{\n".format(self.name)
for spriteview in self.spriteview_list:
ret += "\t{}\n".format(spriteview)
ret += "}\n"
return ret
class SpriteView(object):
def __init__(self, name, spriteset_list, pos):
self.name = name
self.spriteset_list = spriteset_list
self.pos = pos
def pre_process(self):
self.spriteset_list = [x.reduce(global_constants.const_list) for x in self.spriteset_list]
for sg_ref in self.spriteset_list:
if not (isinstance(sg_ref, expression.SpriteGroupRef)
and action2.resolve_spritegroup(sg_ref.name).is_spriteset()):
raise generic.ScriptError("Expected a sprite set reference", sg_ref.pos)
if len(sg_ref.param_list) != 0:
raise generic.ScriptError("Spritesets referenced from a spritegroup may not have parameters.", sg_ref.pos)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Sprite view:', self.name.value)
generic.print_dbg(indentation + 2, 'Sprite sets:')
for spriteset in self.spriteset_list:
spriteset.debug_print(indentation + 4)
def __str__(self):
return "{}: [{}];".format(self.name, ", ".join([str(spriteset) for spriteset in self.spriteset_list]))
spritelayout_base_class = action2.make_sprite_group_class(False, True, False)
class SpriteLayout(spritelayout_base_class):
def __init__(self, name, param_list, layout_sprite_list, pos = None):
base_statement.BaseStatement.__init__(self, "spritelayout", pos, False, False)
self.initialize(name, None, len(param_list))
self.param_list = param_list
self.register_map = {} # Set during action generation for easier referencing
self.layout_sprite_list = layout_sprite_list
# Do not reduce expressions here as they may contain variables
# And the feature is not known yet
def pre_process(self):
# Check parameter names
seen_names = set()
for param in self.param_list:
if not isinstance(param, expression.Identifier):
raise generic.ScriptError("spritelayout parameter names must be identifiers.", param.pos)
if param.value in seen_names:
raise generic.ScriptError("Duplicate parameter name '{}' encountered.".format(param.value), param.pos)
seen_names.add(param.value)
spritelayout_base_class.pre_process(self)
def collect_references(self):
return []
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Sprite layout:', self.name.value)
generic.print_dbg(indentation + 2, 'Parameters:')
for param in self.param_list:
param.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Sprites:')
for layout_sprite in self.layout_sprite_list:
layout_sprite.debug_print(indentation + 4)
def __str__(self):
params = "" if not self.param_list else "({})".format(", ".join(str(x) for x in self.param_list))
return 'spritelayout {}{} {{\n{}\n}}\n'.format(str(self.name), params, '\n'.join(str(x) for x in self.layout_sprite_list))
def get_action_list(self):
action_list = []
if self.prepare_act2_output():
for feature in sorted(self.feature_set):
action_list.extend(action2layout.get_layout_action2s(self, feature, self.pos))
return action_list
class LayoutSprite(object):
def __init__(self, ls_type, param_list, pos):
self.type = ls_type
self.param_list = param_list
self.pos = pos
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Tile layout sprite of type:', self.type)
for layout_param in self.param_list:
layout_param.debug_print(indentation + 2)
def __str__(self):
return '\t{} {{\n\t\t{}\n\t}}'.format(self.type, '\n\t\t'.join(str(layout_param) for layout_param in self.param_list))
nml-0.4.5/nml/ast/disable_item.py 0000644 0005672 0005672 00000005430 13315644406 020030 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, global_constants
from nml.actions import action0
from nml.ast import base_statement, general
class DisableItem(base_statement.BaseStatement):
"""
Class representing a 'disable_item' statement in the AST.
@ivar feature: Feature of the items to disable
@type feature: L{ConstantNumeric}
@ivar first_id: First item ID to disable
@type first_id: L{ConstantNumeric}, or C{None} if not set
@ivar last_id: Last item ID to disable
@type last_id: L{ConstantNumeric}, or C{None} if not set
"""
def __init__(self, param_list, pos):
base_statement.BaseStatement.__init__(self, "disable_item()", pos)
if not (1 <= len(param_list) <= 3):
raise generic.ScriptError("disable_item() requires between 1 and 3 parameters, encountered {:d}.".format(len(param_list)), pos)
self.feature = general.parse_feature(param_list[0])
if len(param_list) > 1:
self.first_id = param_list[1].reduce_constant(global_constants.const_list)
else:
self.first_id = None
if len(param_list) > 2:
self.last_id = param_list[2].reduce_constant(global_constants.const_list)
if self.last_id.value < self.first_id.value:
raise generic.ScriptError("Last id to disable may not be lower than the first id.", pos)
else:
self.last_id = None
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Disable items, feature=' + str(self.feature.value))
if self.first_id is not None:
generic.print_dbg(indentation + 2, 'First ID:')
self.first_id.debug_print(indentation + 4)
if self.last_id is not None:
generic.print_dbg(indentation + 2, 'Last ID:')
self.last_id.debug_print(indentation + 4)
def __str__(self):
ret = str(self.feature)
if self.first_id is not None: ret += ', ' + str(self.first_id)
if self.last_id is not None: ret += ', ' + str(self.last_id)
return 'disable_item({});\n'.format(ret)
def get_action_list(self):
return action0.get_disable_actions(self)
nml-0.4.5/nml/ast/base_statement.py 0000644 0005672 0005672 00000013752 13315644406 020413 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
class BaseStatement(object):
"""
Base class for a statement (AST node) in NML.
Note: All instance variables (except 'pos') are prefixed with "bs_" to avoid naming conflicts
@ivar bs_name: Name of the statement type
@type bs_name: C{str}
@ivar pos: Position information
@type pos: L{Position}
@ivar bs_skipable: Whether the statement may be skipped using an if-block (default: True)
@type bs_skipable: C{bool}
@ivar bs_loopable: Whether the statement may be executed multiple times using a while-block (default: True)
@type bs_loopable: C{bool}
@pre: bs_skipable or not bs_loopable
@ivar bs_in_item: Whether the statement may be part of an item block (default: False)
@type bs_in_item: C{bool}
@ivar bs_out_item: Whether the statement may appear outside of item blocks (default: True)
@type bs_out_item: C{bool}
"""
def __init__(self, name, pos, skipable = True, loopable = True, in_item = False, out_item = True):
assert skipable or not loopable
self.bs_name = name
self.pos = pos
self.bs_skipable = skipable
self.bs_loopable = loopable
self.bs_in_item = in_item
self.bs_out_item = out_item
def validate(self, scope_list):
"""
Validate that this statement is in a valid location
@param scope_list: List of nested blocks containing this statement
@type scope_list: C{list} of L{BaseStatementList}
"""
seen_item = False
for scope in scope_list:
if scope.list_type == BaseStatementList.LIST_TYPE_SKIP:
if not self.bs_skipable: raise generic.ScriptError("{} may not appear inside a conditional block.".format(self.bs_name), self.pos)
if scope.list_type == BaseStatementList.LIST_TYPE_LOOP:
if not self.bs_loopable: raise generic.ScriptError("{} may not appear inside a loop.".format(self.bs_name), self.pos)
if scope.list_type == BaseStatementList.LIST_TYPE_ITEM:
seen_item = True
if not self.bs_in_item: raise generic.ScriptError("{} may not appear inside an item block.".format(self.bs_name), self.pos)
if not (seen_item or self.bs_out_item):
raise generic.ScriptError("{} must appear inside an item block.".format(self.bs_name), self.pos)
def register_names(self):
"""
Called to register identifiers, that must be available before their definition.
"""
pass
def pre_process(self):
"""
Called to do any pre-processing before the actual action generation.
For example, to remove identifiesr
"""
pass
def debug_print(self, indentation):
"""
Print all AST information to the standard output
@param indentation: Print all lines with at least C{indentation} spaces
@type indentation: C{int}
"""
raise NotImplementedError('debug_print must be implemented in BaseStatement-subclass {!r}'.format(type(self)))
def get_action_list(self):
"""
Generate a list of NFO actions associated with this statement
@return: A list of action
@rtype: C{list} of L{BaseAction}
"""
raise NotImplementedError('get_action_list must be implemented in BaseStatement-subclass {!r}'.format(type(self)))
def __str__(self):
"""
Generate a string representing this statement in valid NML-code.
@return: An NML string representing this action
@rtype: C{str}
"""
raise NotImplementedError('__str__ must be implemented in BaseStatement-subclass {!r}'.format(type(self)))
class BaseStatementList(BaseStatement):
"""
Base class for anything that contains a list of statements
@ivar list_type: Type of this list, used for validation logic
@type list_type: C{int}, see constants below
@pre list_type in (LIST_TYPE_NONE, LIST_TYPE_SKIP, LIST_TYPE_LOOP, LIST_TYPE_ITEM)
@ivar statements: List of sub-statements in this block
@type statements: C{list} of L{BaseStatement}
"""
LIST_TYPE_NONE = 0
LIST_TYPE_SKIP = 1
LIST_TYPE_LOOP = 2
LIST_TYPE_ITEM = 3
def __init__(self, name, pos, list_type, statements, skipable = True, loopable = True, in_item = False, out_item = True):
BaseStatement.__init__(self, name, pos, skipable, loopable, in_item, out_item)
assert list_type in (self.LIST_TYPE_NONE, self.LIST_TYPE_SKIP, self.LIST_TYPE_LOOP, self.LIST_TYPE_ITEM)
self.list_type = list_type
self.statements = statements
def validate(self, scope_list):
new_list = scope_list + [self]
for stmt in self.statements:
stmt.validate(new_list)
def register_names(self):
for stmt in self.statements:
stmt.register_names()
def pre_process(self):
for stmt in self.statements:
stmt.pre_process()
def debug_print(self, indentation):
for stmt in self.statements:
stmt.debug_print(indentation)
def get_action_list(self):
action_list = []
for stmt in self.statements:
action_list.extend(stmt.get_action_list())
return action_list
def __str__(self):
res = ""
for stmt in self.statements:
res += '\t' + str(stmt).replace('\n', '\n\t')[0:-1]
return res
nml-0.4.5/nml/ast/switch.py 0000644 0005672 0005672 00000033600 13315644406 016710 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, global_constants
from nml.actions import action2, action2var, action2random
from nml.ast import base_statement, general
var_ranges = {
'SELF' : 0x89,
'PARENT' : 0x8A
}
# Used by Switch and RandomSwitch
switch_base_class = action2.make_sprite_group_class(False, True, True)
class Switch(switch_base_class):
def __init__(self, param_list, body, pos):
base_statement.BaseStatement.__init__(self, "switch-block", pos, False, False)
if len(param_list) != 4:
raise generic.ScriptError("Switch-block requires 4 parameters, encountered " + str(len(param_list)), pos)
if not isinstance(param_list[1], expression.Identifier):
raise generic.ScriptError("Switch-block parameter 2 'variable range' must be an identifier.", param_list[1].pos)
if param_list[1].value in var_ranges:
self.var_range = var_ranges[param_list[1].value]
else:
raise generic.ScriptError("Unrecognized value for switch parameter 2 'variable range': '{}'".format(param_list[1].value), param_list[1].pos)
if not isinstance(param_list[2], expression.Identifier):
raise generic.ScriptError("Switch-block parameter 3 'name' must be an identifier.", param_list[2].pos)
self.initialize(param_list[2], general.parse_feature(param_list[0]).value)
self.expr = param_list[3]
self.body = body
def pre_process(self):
var_feature = action2var.get_feature(self) # Feature of the accessed variables
self.expr = action2var.reduce_varaction2_expr(self.expr, var_feature)
self.body.reduce_expressions(var_feature)
switch_base_class.pre_process(self)
def collect_references(self):
all_refs = []
for result in [r.result for r in self.body.ranges] + [self.body.default]:
if result is not None and isinstance(result.value, expression.SpriteGroupRef):
all_refs.append(result.value)
return all_refs
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Switch, Feature = {:d}, name = {}'.format(next(iter(self.feature_set)), self.name.value))
generic.print_dbg(indentation + 2, 'Expression:')
self.expr.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Body:')
self.body.debug_print(indentation + 4)
def get_action_list(self):
if self.prepare_act2_output():
return action2var.parse_varaction2(self)
return []
def __str__(self):
var_range = 'SELF' if self.var_range == 0x89 else 'PARENT'
return 'switch({}, {}, {}, {}) {{\n{}}}\n'.format(str(next(iter(self.feature_set))), var_range, str(self.name), str(self.expr), str(self.body))
class SwitchBody(object):
"""
AST-node representing the body of a switch block
This contains the various ranges as well as the default value
@ivar ranges: List of ranges
@type ranges: C{list} of L{SwitchRange}
@ivar default: Default result to use if no range matches
@type default: L{SwitchValue} or C{None} if N/A
"""
def __init__(self, ranges, default):
self.ranges = ranges
self.default = default
def reduce_expressions(self, var_feature):
for r in self.ranges[:]:
if r.min is r.max and isinstance(r.min, expression.Identifier) and r.min.value == 'default':
if self.default is not None:
raise generic.ScriptError("Switch-block has more than one default, which is impossible.", r.result.pos)
self.default = r.result
self.ranges.remove(r)
else:
r.reduce_expressions(var_feature)
if self.default is not None and self.default.value is not None:
self.default.value = action2var.reduce_varaction2_expr(self.default.value, var_feature)
def debug_print(self, indentation):
for r in self.ranges:
r.debug_print(indentation)
if self.default is not None:
generic.print_dbg(indentation, 'Default:')
self.default.debug_print(indentation + 2)
def __str__(self):
ret = ''.join('\t{}\n'.format(r) for r in self.ranges)
if self.default is not None:
ret += '\t{}\n'.format(str(self.default))
return ret
class SwitchRange(object):
def __init__(self, min, max, result, unit = None):
self.min = min
self.max = max
self.result = result
self.unit = unit
def reduce_expressions(self, var_feature):
self.min = self.min.reduce(global_constants.const_list)
self.max = self.max.reduce(global_constants.const_list)
if self.result.value is not None:
self.result.value = action2var.reduce_varaction2_expr(self.result.value, var_feature)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Min:')
self.min.debug_print(indentation + 2)
generic.print_dbg(indentation, 'Max:')
self.max.debug_print(indentation + 2)
generic.print_dbg(indentation, 'Result:')
self.result.debug_print(indentation + 2)
def __str__(self):
ret = str(self.min)
if not isinstance(self.min, expression.ConstantNumeric) or not isinstance(self.max, expression.ConstantNumeric) or self.max.value != self.min.value:
ret += '..' + str(self.max)
ret += ': ' + str(self.result)
return ret
class SwitchValue(object):
"""
Class representing a single returned value or sprite group in a switch-block
Also used for random-switch and graphics blocks
@ivar value: Value to return
@type value: L{Expression} or C{None}
@ivar is_return: Whether the return keyword was present
@type is_return: C{bool}
@ivar pos: Position information
@type pos: L{Position}
"""
def __init__(self, value, is_return, pos):
self.value = value
self.is_return = is_return
self.pos = pos
def debug_print(self, indentation):
if self.value is None:
assert self.is_return
generic.print_dbg(indentation, 'Return computed value')
else:
generic.print_dbg(indentation, 'Return value:' if self.is_return else 'Go to block:')
self.value.debug_print(indentation + 2)
def __str__(self):
if self.value is None:
assert self.is_return
return 'return;'
elif self.is_return:
return 'return {};'.format(self.value)
else:
return '{};'.format(self.value)
class RandomSwitch(switch_base_class):
def __init__(self, param_list, choices, pos):
base_statement.BaseStatement.__init__(self, "random_switch-block", pos, False, False)
if not (3 <= len(param_list) <= 4):
raise generic.ScriptError("random_switch requires 3 or 4 parameters, encountered {:d}".format(len(param_list)), pos)
#feature
feature = general.parse_feature(param_list[0]).value
#type
self.type = param_list[1]
# Extract type name and possible argument
if isinstance(self.type, expression.Identifier):
self.type_count = None
elif isinstance(self.type, expression.FunctionCall):
if len(self.type.params) == 0:
self.type_count = None
elif len(self.type.params) == 1:
self.type_count = action2var.reduce_varaction2_expr(self.type.params[0], feature)
else:
raise generic.ScriptError("Value for random_switch parameter 2 'type' can have only one parameter.", self.type.pos)
self.type = self.type.name
else:
raise generic.ScriptError("random_switch parameter 2 'type' should be an identifier, possibly with a parameter.", self.type.pos)
#name
if not isinstance(param_list[2], expression.Identifier):
raise generic.ScriptError("random_switch parameter 3 'name' should be an identifier", pos)
name = param_list[2]
#triggers
self.triggers = param_list[3] if len(param_list) == 4 else expression.ConstantNumeric(0)
#body
self.choices = []
self.dependent = []
self.independent = []
for choice in choices:
if isinstance(choice.probability, expression.Identifier):
if choice.probability.value == 'dependent':
self.dependent.append(choice.result)
continue
elif choice.probability.value == 'independent':
self.independent.append(choice.result)
continue
self.choices.append(choice)
if len(self.choices) == 0:
raise generic.ScriptError("random_switch requires at least one possible choice", pos)
self.initialize(name, feature)
self.random_act2 = None # Set during action generation to resolve dependent/independent chains
def pre_process(self):
for choice in self.choices:
choice.reduce_expressions(next(iter(self.feature_set)))
for dep_list in (self.dependent, self.independent):
for i, dep in enumerate(dep_list[:]):
if dep.is_return:
raise generic.ScriptError("Expected a random_switch identifier after (in)dependent, not a return.", dep.pos)
dep_list[i] = dep.value.reduce(global_constants.const_list)
# Make sure, all [in]dependencies refer to existing random switch blocks
if (not isinstance(dep_list[i], expression.SpriteGroupRef)) or len(dep_list[i].param_list) > 0:
raise generic.ScriptError("Value for (in)dependent should be an identifier", dep_list[i].pos)
spritegroup = action2.resolve_spritegroup(dep_list[i].name)
if not isinstance(spritegroup, RandomSwitch):
raise generic.ScriptError("Value of (in)dependent '{}' should refer to a random_switch.".format(dep_list[i].name.value), dep_list[i].pos)
self.triggers = self.triggers.reduce_constant(global_constants.const_list)
if not (0 <= self.triggers.value <= 255):
raise generic.ScriptError("random_switch parameter 4 'triggers' out of range 0..255, encountered " + str(self.triggers.value), self.triggers.pos)
switch_base_class.pre_process(self)
def collect_references(self):
all_refs = []
for choice in self.choices:
if isinstance(choice.result.value, expression.SpriteGroupRef):
all_refs.append(choice.result.value)
return all_refs
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Random')
generic.print_dbg(indentation + 2, 'Feature:', next(iter(self.feature_set)))
generic.print_dbg(indentation + 2, 'Type:')
self.type.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Name:', self.name.value)
generic.print_dbg(indentation + 2, 'Triggers:')
self.triggers.debug_print(indentation + 4)
for dep in self.dependent:
generic.print_dbg(indentation + 2, 'Dependent on:')
dep.debug_print(indentation + 4)
for indep in self.independent:
generic.print_dbg(indentation + 2, 'Independent from:')
indep.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Choices:')
for choice in self.choices:
choice.debug_print(indentation + 4)
def get_action_list(self):
if self.prepare_act2_output():
return action2random.parse_randomswitch(self)
return []
def __str__(self):
ret = 'random_switch({}, {}, {}, {}) {{\n'.format(str(next(iter(self.feature_set))), str(self.type), str(self.name), str(self.triggers))
for dep in self.dependent:
ret += 'dependent: {};\n'.format(dep)
for indep in self.independent:
ret += 'independent: {};\n'.format(indep)
for choice in self.choices:
ret += str(choice) + '\n'
ret += '}\n'
return ret
class RandomChoice(object):
"""
Class to hold one of the possible choices in a random_switch
@ivar probability: Relative chance for this choice to be chosen
@type probability: L{Expression}
@ivar result: Result of this choice, either another action2 or a return value
@type result: L{SwitchValue}
"""
def __init__ (self, probability, result):
self.probability = probability
if result.value is None:
raise generic.ScriptError("Returning the computed value is not possible in a random_switch, as there is no computed value.", result.pos)
self.result = result
def reduce_expressions(self, var_feature):
self.probability = self.probability.reduce_constant(global_constants.const_list)
if self.probability.value <= 0:
raise generic.ScriptError("Random probability must be higher than 0", self.probability.pos)
self.result.value = action2var.reduce_varaction2_expr(self.result.value, var_feature)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Probability:')
self.probability.debug_print(indentation + 2)
generic.print_dbg(indentation, 'Result:')
self.result.debug_print(indentation + 2)
def __str__(self):
return '{}: {}'.format(self.probability, self.result)
nml-0.4.5/nml/ast/base_graphics.py 0000644 0005672 0005672 00000007115 13315644406 020203 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic
from nml.actions import real_sprite
from nml.ast import base_statement, sprite_container
class BaseGraphics(base_statement.BaseStatement, sprite_container.SpriteContainer):
"""
AST node for a 'base_graphics' block.
NML syntax: base_graphics [block_name]([[sprite_num ,]default_file]) { ..real sprites.. }
@ivar image_file: Default image file to use for sprites.
@type image_file: C{None} if not specified, else L{StringLiteral}
@ivar sprite_num: Sprite number of the first sprite (if provided explicitly)
@type sprite_num: L{Expression} or C{None}
@ivar sprite_list: List of real sprites to use
@type sprite_list: Heterogeneous C{list} of L{RealSprite}, L{TemplateUsage}
"""
def __init__(self, param_list, sprite_list, name, pos):
base_statement.BaseStatement.__init__(self, "base_graphics-block", pos)
sprite_container.SpriteContainer.__init__(self, "base_graphics-block", name)
num_params = len(param_list)
if not (0 <= num_params <= 2):
raise generic.ScriptError("base_graphics-block requires 0 to 2 parameters, encountered {:d}".format(num_params), pos)
if num_params >= 2:
self.sprite_num = param_list[0].reduce_constant()
else:
self.sprite_num = None
if num_params >= 1:
self.image_file = param_list[-1].reduce()
if not isinstance(self.image_file, expression.StringLiteral):
raise generic.ScriptError("The last base_graphics-block parameter 'file' must be a string literal", self.image_file.pos)
else:
self.image_file = None
self.sprite_list = sprite_list
self.add_sprite_data(self.sprite_list, self.image_file, pos)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'base_graphics-block')
generic.print_dbg(indentation + 2, 'Source:', self.image_file.value if self.image_file is not None else 'None')
if self.block_name:
generic.print_dbg(indentation + 2, 'Name:', self.block_name)
if self.sprite_num is not None:
generic.print_dbg(indentation + 2, 'Sprite number:', self.sprite_num)
generic.print_dbg(indentation + 2, 'Sprites:')
for sprite in self.sprite_list:
sprite.debug_print(indentation + 4)
def get_action_list(self):
actions = real_sprite.parse_sprite_data(self)
actions[0].sprite_num = self.sprite_num
return actions
def __str__(self):
name = str(self.block_name) if self.block_name is not None else ""
params = [] if self.sprite_num is None else [self.sprite_num]
if self.image_file is not None: params.append(self.image_file)
ret = "base_graphics {}({}) {{\n".format(name, ", ".join(str(param) for param in params))
for sprite in self.sprite_list:
ret += "\t{}\n".format(sprite)
ret += "}\n"
return ret
nml-0.4.5/nml/ast/cargotable.py 0000644 0005672 0005672 00000003667 13315644406 017524 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, global_constants, expression
from nml.actions import action0
from nml.ast import base_statement
class CargoTable(base_statement.BaseStatement):
def __init__(self, cargo_list, pos):
base_statement.BaseStatement.__init__(self, "cargo table", pos, False, False)
self.cargo_list = cargo_list
def register_names(self):
generic.OnlyOnce.enforce(self, "cargo table")
for i, cargo in enumerate(self.cargo_list):
if isinstance(cargo, expression.Identifier):
self.cargo_list[i] = expression.StringLiteral(cargo.value, cargo.pos)
expression.parse_string_to_dword(self.cargo_list[i]) # we don't care about the result, only validate the input
global_constants.cargo_numbers[self.cargo_list[i].value] = i
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Cargo table')
for cargo in self.cargo_list:
generic.print_dbg(indentation, 'Cargo:', cargo.value)
def get_action_list(self):
return action0.get_cargolist_action(self.cargo_list)
def __str__(self):
ret = 'cargotable {\n'
ret += ', '.join([expression.identifier_to_print(cargo.value) for cargo in self.cargo_list])
ret += '\n}\n'
return ret
nml-0.4.5/nml/ast/override.py 0000644 0005672 0005672 00000004573 13315644406 017235 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, expression, global_constants
from nml.ast import base_statement
from nml.actions import action0
class EngineOverride(base_statement.BaseStatement):
"""
AST Node for an engine override.
@ivar grfid: GRFid of the grf to override the engines from.
@type grfid: C{int{
@ivar source_grfid: GRFid of the grf that overrides the engines.
@type source_grfid: C{int}
"""
def __init__(self, args, pos):
base_statement.BaseStatement.__init__(self, "engine_override()", pos)
self.args = args
def pre_process(self):
if len(self.args) not in (1, 2):
raise generic.ScriptError("engine_override expects 1 or 2 parameters", self.pos)
if len(self.args) == 1:
try:
self.source_grfid = expression.Identifier('GRFID').reduce(global_constants.const_list).value
assert isinstance(self.source_grfid, int)
except generic.ScriptError:
raise generic.ScriptError("GRFID of this grf is required, but no grf-block is defined.", self.pos)
else:
self.source_grfid = expression.parse_string_to_dword(self.args[0].reduce(global_constants.const_list))
self.grfid = expression.parse_string_to_dword(self.args[-1].reduce(global_constants.const_list))
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Engine override')
generic.print_dbg(indentation, 'Source:', self.source_grfid)
generic.print_dbg(indentation, 'Target:', self.grfid)
def get_action_list(self):
return action0.get_engine_override_action(self)
def __str__(self):
return "engine_override({});\n".format(', '.join(str(x) for x in self.args))
nml-0.4.5/nml/ast/loop.py 0000644 0005672 0005672 00000003627 13315644406 016366 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions import action7
from nml.ast import base_statement
from nml import global_constants, generic
class Loop(base_statement.BaseStatementList):
"""
AST node for a while-loop.
@ivar expr: The conditional to check whether the loop continues.
@type expr: L{Expression}
"""
def __init__(self, expr, block, pos):
base_statement.BaseStatementList.__init__(self, "while-loop", pos,
base_statement.BaseStatementList.LIST_TYPE_LOOP, block, in_item = True)
self.expr = expr
def pre_process(self):
self.expr = self.expr.reduce(global_constants.const_list)
base_statement.BaseStatementList.pre_process(self)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'While loop')
generic.print_dbg(indentation + 2, 'Expression:')
self.expr.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Block:')
base_statement.BaseStatementList.debug_print(self, indentation + 4)
def get_action_list(self):
return action7.parse_loop_block(self)
def __str__(self):
ret = 'while({}) {{\n'.format(self.expr)
ret += base_statement.BaseStatementList.__str__(self)
ret += '}\n'
return ret
nml-0.4.5/nml/ast/railtypetable.py 0000644 0005672 0005672 00000005477 13315644406 020263 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, global_constants, expression
from nml.ast import assignment
from nml.actions import action0
from nml.ast import base_statement
class RailtypeTable(base_statement.BaseStatement):
def __init__(self, railtype_list, pos):
base_statement.BaseStatement.__init__(self, "rail type table", pos, False, False)
self.railtype_list = railtype_list
def register_names(self):
generic.OnlyOnce.enforce(self, "rail type table")
global_constants.is_default_railtype_table = False
global_constants.railtype_table.clear()
for i, railtype in enumerate(self.railtype_list):
if isinstance(railtype, assignment.Assignment):
name = railtype.name
val_list = []
for rt in railtype.value:
if isinstance(rt, expression.Identifier):
val_list.append(expression.StringLiteral(rt.value, rt.pos))
else:
val_list.append(rt)
expression.parse_string_to_dword(val_list[-1]) # we don't care about the result, only validate the input
self.railtype_list[i] = val_list if len(val_list) > 1 else val_list[0]
else:
name = railtype
if isinstance(railtype, expression.Identifier):
self.railtype_list[i] = expression.StringLiteral(railtype.value, railtype.pos)
expression.parse_string_to_dword(self.railtype_list[i]) # we don't care about the result, only validate the input
global_constants.railtype_table[name.value] = i
def pre_process(self):
pass
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Railtype table')
for railtype in self.railtype_list:
generic.print_dbg(indentation + 2, 'Railtype: ', railtype.value)
def get_action_list(self):
return action0.get_railtypelist_action(self.railtype_list)
def __str__(self):
ret = 'railtypetable {\n'
ret += ', '.join([expression.identifier_to_print(railtype.value) for railtype in self.railtype_list])
ret += '\n}\n'
return ret
nml-0.4.5/nml/ast/error.py 0000644 0005672 0005672 00000007666 13315644406 016555 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, expression
from nml.actions import actionB
from nml.ast import base_statement
class Error(base_statement.BaseStatement):
"""
An error has occured while parsing the GRF. This can be anything ranging from
an imcompatible GRF file that was found or a game setting that is set to the
wrong value to a wrong combination of parameters. The action taken by the host
depends on the severity level of the error.
NML equivalent: error(level, message[, extra_text[, parameter1[, parameter2]]]).
@ivar params: Extra expressions whose value can be used in the error string.
@type params: C{list} of L{Expression}
@ivar severity: Severity level of this error, value between 0 and 3.
@type severity: L{Expression}
@ivar msg: The string to be used for this error message. This can be either
one of the predifined error strings or a custom string from the
language file.
@type msg: L{Expression}
@ivar data: Optional extra message that is inserted in place of the second
{STRING}-code of msg.
@type data: C{None} or L{String} or L{StringLiteral}
"""
def __init__(self, param_list, pos):
base_statement.BaseStatement.__init__(self, "error()", pos)
if not 2 <= len(param_list) <= 5:
raise generic.ScriptError("'error' expects between 2 and 5 parameters, got " + str(len(param_list)), self.pos)
self.severity = param_list[0]
self.msg = param_list[1]
self.data = param_list[2] if len(param_list) >= 3 else None
self.params = param_list[3:]
def pre_process(self):
self.severity = self.severity.reduce([actionB.error_severity])
self.msg = self.msg.reduce([actionB.default_error_msg])
if self.data:
self.data = self.data.reduce()
self.params = [x.reduce() for x in self.params]
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Error message')
generic.print_dbg(indentation + 2, 'Message:')
self.msg.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Severity:')
self.severity.debug_print(indentation + 4)
generic.print_dbg(indentation, 'Data: ')
if self.data is not None: self.data.debug_print(indentation + 4)
if len(self.params) > 0:
generic.print_dbg(indentation + 2, 'Param1: ')
self.params[0].debug_print(indentation + 4)
if len(self.params) > 1:
generic.print_dbg(indentation + 2, 'Param2: ')
self.params[1].debug_print(indentation + 4)
def get_action_list(self):
return actionB.parse_error_block(self)
def __str__(self):
sev = str(self.severity)
if isinstance(self.severity, expression.ConstantNumeric):
for s in actionB.error_severity:
if self.severity.value == actionB.error_severity[s]:
sev = s
break
res = 'error({}, {}'.format(sev, self.msg)
if self.data is not None:
res += ', {}'.format(self.data)
if len(self.params) > 0:
res += ', {}'.format(self.params[0])
if len(self.params) > 1:
res += ', {}'.format(self.params[1])
res += ');\n'
return res
nml-0.4.5/nml/ast/__init__.py 0000644 0005672 0005672 00000001243 13315644406 017144 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
nml-0.4.5/nml/ast/townnames.py 0000644 0005672 0005672 00000040301 13315644406 017416 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import heapq
from nml import expression, generic, grfstrings
from nml.actions import actionF
from nml.ast import base_statement
townname_serial = 1
class TownNames(base_statement.BaseStatement):
"""
'town_names' ast node.
@ivar name: Name ID of the town_name.
@type name: C{None}, L{Identifier}, or L{ConstantNumeric}
@ivar id_number: Allocated ID number for this town_name action F node.
@type id_number: C{None} or C{int}
@ivar style_name: Name of the translated string containing the name of the style, if any.
@type style_name: C{None} or L{String}
@ivar actFs: Action F instance needed before this one.
@type actFs: C{list} of L{ActionF}
@ivar parts: Parts of the names.
@type parts: C{list} of L{TownNamesPart}
@ivar param_list: Stored parameter list.
@type param_list: C{list} of (L{TownNamesPart} or L{TownNamesParam})
"""
def __init__(self, name, param_list, pos):
base_statement.BaseStatement.__init__(self, "town_names-block", pos, False, False)
self.name = name
self.param_list = param_list
self.id_number = None
self.style_name = None
self.actFs = []
self.parts = []
def debug_print(self, indentation):
if isinstance(self.name, str):
name_text = "name = " + repr(self.name)
if self.id_number is not None: name_text += " (allocated number is 0x{:x})".format(self.id_number)
elif self.id_number is not None:
name_text = "number = 0x{:x}".format(self.id_number)
else:
name_text = "(unnamed)"
generic.print_dbg(indentation, 'Town name ' + name_text)
if self.style_name is not None:
generic.print_dbg(indentation, " style name string:", self.style_name)
for part in self.parts:
generic.print_dbg(indentation, "-name part:")
part.debug_print(indentation + 2)
def pre_process(self):
self.actFs = []
self.parts = []
for param in self.param_list:
if isinstance(param, TownNamesPart):
actFs, part = param.make_actions()
self.actFs.extend(actFs)
self.parts.append(part)
else:
if param.key.value != 'styles':
raise generic.ScriptError("Expected 'styles' keyword.", param.pos)
if len(param.value.params) > 0:
raise generic.ScriptError("Parameters of the 'styles' were not expected.", param.pos)
if self.style_name is not None:
raise generic.ScriptError("'styles' is already defined.", self.pos)
self.style_name = param.value
if len(self.parts) == 0:
raise generic.ScriptError("Missing name parts in a town_names item.", self.pos)
# 'name' is actually a number.
# Allocate it now, before the self.prepare_output() call (to prevent names to grab it).
if self.name is not None and not isinstance(self.name, expression.Identifier):
value = self.name.reduce_constant()
if not isinstance(value, expression.ConstantNumeric):
raise generic.ScriptError("ID should be an integer number.", self.pos)
self.id_number = value.value
if self.id_number < 0 or self.id_number > 0x7f:
raise generic.ScriptError("ID must be a number between 0 and 0x7f (inclusive)", self.pos)
if self.id_number not in actionF.free_numbers:
raise generic.ScriptError("town names ID 0x{:x} is already used.".format(self.id_number), self.pos)
actionF.free_numbers.remove(self.id_number)
def __str__(self):
ret = 'town_names'
if self.name is not None:
ret += '({})'.format(self.name)
ret += '{{\n{}}}\n'.format(''.join(str(x) for x in self.param_list))
return ret
def get_action_list(self):
return self.actFs + [actionF.ActionF(self.name, self.id_number, self.style_name, self.parts, self.pos)]
class TownNamesPart(object):
"""
A class containing a town name part.
@ivar pieces: Pieces of the town name part.
@type pieces: C{list} of (L{TownNamesEntryDefinition} or L{TownNamesEntryText})
@ivar pos: Position information of the parts block.
@type pos: L{Position}
@ivar startbit: First bit to use for this part, if defined.
@type startbit: C{int} or C{None}
@ivar num_bits: Number of bits to use, if defined.
@type num_bits: C{int} or C{None}
"""
def __init__(self, pieces, pos):
self.pos = pos
self.pieces = pieces
self.startbit = None
self.num_bits = None
def make_actions(self):
"""
Construct new actionF instances to store all pieces of this part, if needed.
@return: Action F that should be defined before, and the processed part.
@rtype: C{list} of L{ActionF}, L{TownNamesPart}
"""
new_pieces = []
for piece in self.pieces:
piece.pre_process()
if piece.probability.value == 0:
generic.print_warning("Dropping town name piece with 0 probability.", piece.pos)
else:
new_pieces.append(piece)
self.pieces = new_pieces
actFs = self.move_pieces()
if len(self.pieces) == 0:
raise generic.ScriptError("Expected names and/or town_name references in the part.", self.pos)
if len(self.pieces) > 255:
raise generic.ScriptError("Too many values in a part, found {:d}, maximum is 255".format(len(self.pieces)), self.pos)
return actFs, self
def move_pieces(self):
"""
Move pieces to new action F instances to make it fit, if needed.
@return: Created action F instances.
@rtype: C{list} of L{ActionF}
@note: Function may change L{pieces}.
"""
global townname_serial
if len(self.pieces) <= 255:
return [] # Trivially correct.
# There are too many pieces.
number_action_f = (len(self.pieces) + 254) // 255
pow2 = 1
while pow2 < number_action_f: pow2 = pow2 * 2
if pow2 < 255: number_action_f = pow2
heap = [] # Heap of (summed probability, subset-of-pieces)
i = 0
while i < number_action_f:
# Index 'i' is added to have a unique sorting when lists have equal total probabilities.
heapq.heappush(heap, (0, i, []))
i = i + 1
finished_actions = []
# Index 'idx' is added to have a unique sorting when pieces have equal probabilities.
rev_pieces = sorted(((p.probability.value, idx, p) for idx, p in enumerate(self.pieces)), reverse = True)
for prob, _idx, piece in rev_pieces:
while True:
sub = heapq.heappop(heap)
if len(sub[2]) < 255:
break
# If a subset already has the max number of parts, consider it finished.
finished_actions.append(sub)
sub[2].append(piece)
sub = (sub[0] + prob, sub[1], sub[2])
heapq.heappush(heap, sub)
finished_actions.extend(heap)
# To ensure the chances do not get messed up due to one part needing less bits for its
# selection, all parts are forced to use the same number of bits.
max_prob = max(sub[0] for sub in finished_actions)
num_bits = 1
while (1 << num_bits) < max_prob:
num_bits = num_bits + 1
# Assign to action F
actFs = []
for _prob, _idx, sub in finished_actions:
actF_name = expression.Identifier("**townname #{:d}**".format(townname_serial), None)
townname_serial = townname_serial + 1
town_part = TownNamesPart(sub, self.pos)
town_part.set_numbits(num_bits)
actF = actionF.ActionF(actF_name, None, None, [town_part], self.pos)
actFs.append(actF)
# Remove pieces of 'sub' from self.pieces
counts = len(self.pieces), len(sub)
sub_set = set(sub)
self.pieces = [piece for piece in self.pieces if piece not in sub_set]
assert len(self.pieces) == counts[0] - counts[1]
self.pieces.append(TownNamesEntryDefinition(actF_name, expression.ConstantNumeric(1), self.pos))
# update self.parts
return actFs
def assign_bits(self, startbit):
"""
Assign bits for this piece.
@param startbit: First bit free for use.
@type startbit: C{int}
@return: Number of bits needed for this piece.
@rtype: C{int}
"""
assert len(self.pieces) <= 255
total = sum(piece.probability.value for piece in self.pieces)
self.startbit = startbit
if self.num_bits is None:
n = 1
while total > (1 << n): n = n + 1
self.num_bits = n
assert (1 << self.num_bits) >= total
return self.num_bits
def set_numbits(self, numbits):
"""
Set the number of bits that this part should use.
"""
assert self.num_bits is None
self.num_bits = numbits
def debug_print(self, indentation):
total = sum(piece.probability.value for piece in self.pieces)
generic.print_dbg(indentation, 'Town names part (total {:d})'.format(total))
for piece in self.pieces:
piece.debug_print(indentation + 2, total)
def __str__(self):
return '{{\n\t{}\n}}\n'.format('\n\t'.join(str(piece) for piece in self.pieces))
def get_length(self):
size = 3 # textcount, firstbit, bitcount bytes.
size += sum(piece.get_length() for piece in self.pieces)
return size
def resolve_townname_id(self):
'''
Resolve the reference numbers to previous C{town_names} blocks.
@return: Set of referenced C{town_names} block numbers.
'''
blocks = set()
for piece in self.pieces:
block = piece.resolve_townname_id()
if block is not None: blocks.add(block)
return blocks
def write(self, file):
file.print_bytex(len(self.pieces))
file.print_bytex(self.startbit)
file.print_bytex(self.num_bits)
for piece in self.pieces:
piece.write(file)
file.newline()
class TownNamesParam(object):
"""
Class containing a parameter of a town name.
Currently known key/values:
- 'styles' / string expression
"""
def __init__(self, key, value, pos):
self.key = key
self.value = value
self.pos = pos
def __str__(self):
return '{}: {};\n'.format(self.key, self.value)
class TownNamesEntryDefinition(object):
"""
An entry in a part referring to a non-final town name, with a given probability.
@ivar def_number: Name or number referring to a previous town_names node.
@type def_number: L{Identifier} or L{ConstantNumeric}
@ivar number: Actual ID to use.
@type number: C{None} or C{int}
@ivar probability: Probability of picking this reference.
@type probability: C{ConstantNumeric}
@ivar pos: Position information of the parts block.
@type pos: L{Position}
"""
def __init__(self, def_number, probability, pos):
self.def_number = def_number
self.number = None
self.probability = probability
self.pos = pos
def pre_process(self):
self.number = None
if not isinstance(self.def_number, expression.Identifier):
self.def_number = self.def_number.reduce_constant()
if not isinstance(self.def_number, expression.ConstantNumeric):
raise generic.ScriptError("Reference to other town name ID should be an integer number.", self.pos)
if self.def_number.value < 0 or self.def_number.value > 0x7f:
raise generic.ScriptError("Reference number out of range (must be between 0 and 0x7f inclusive).", self.pos)
self.probability = self.probability.reduce_constant()
if not isinstance(self.probability, expression.ConstantNumeric):
raise generic.ScriptError("Probability should be an integer number.", self.pos)
if self.probability.value < 0 or self.probability.value > 0x7f:
raise generic.ScriptError("Probability out of range (must be between 0 and 0x7f inclusive).", self.pos)
def debug_print(self, indentation, total):
if isinstance(self.def_number, expression.Identifier):
name_text = "name '" + self.def_number.value + "'"
else:
name_text = "number 0x{:x}".format(self.def_number.value)
generic.print_dbg(indentation, 'Insert town_name ID {} with probability {:d}/{:d}'.format(name_text, self.probability.value, total))
def __str__(self):
return 'town_names({}, {:d}),'.format(str(self.def_number), self.probability.value)
def get_length(self):
return 2
def resolve_townname_id(self):
'''
Resolve the reference number to a previous C{town_names} block.
@return: Number of the referenced C{town_names} block.
'''
if isinstance(self.def_number, expression.Identifier):
self.number = actionF.named_numbers.get(self.def_number.value)
if self.number is None:
raise generic.ScriptError('Town names name "{}" is not defined or points to a next town_names node'.format(self.def_number.value), self.pos)
else:
self.number = self.def_number.value
if self.number not in actionF.numbered_numbers:
raise generic.ScriptError('Town names number "{}" is not defined or points to a next town_names node'.format(self.number), self.pos)
return self.number
def write(self, file):
file.print_bytex(self.probability.value | 0x80)
file.print_bytex(self.number)
class TownNamesEntryText(object):
"""
An entry in a part, a text-string with a given probability.
@ivar pos: Position information of the parts block.
@type pos: L{Position}
"""
def __init__(self, id, text, probability, pos):
self.id = id
self.text = text
self.probability = probability
self.pos = pos
def pre_process(self):
if self.id.value != 'text':
raise generic.ScriptError("Expected 'text' prefix.", self.pos)
if not isinstance(self.text, expression.StringLiteral):
raise generic.ScriptError("Expected string literal for the name.", self.pos)
self.probability = self.probability.reduce_constant()
if not isinstance(self.probability, expression.ConstantNumeric):
raise generic.ScriptError("Probability should be an integer number.", self.pos)
if self.probability.value < 0 or self.probability.value > 0x7f:
raise generic.ScriptError("Probability out of range (must be between 0 and 0x7f inclusive).", self.pos)
def debug_print(self, indentation, total):
generic.print_dbg(indentation, 'Text {} with probability {:d}/{:d}'.format(self.text.value, self.probability.value, total))
def __str__(self):
return 'text({}, {:d}),'.format(self.text, self.probability.value)
def get_length(self):
return 1 + grfstrings.get_string_size(self.text.value) # probability, text
def resolve_townname_id(self):
'''
Resolve the reference number to a previous C{town_names} block.
@return: C{None}, as being the block number of a referenced previous C{town_names} block.
'''
return None
def write(self, file):
file.print_bytex(self.probability.value)
file.print_string(self.text.value, final_zero = True)
nml-0.4.5/nml/ast/basecost.py 0000644 0005672 0005672 00000017174 13315644406 017222 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, global_constants, nmlop
from nml.ast import assignment, base_statement
from nml.actions import action0
class BaseCost(base_statement.BaseStatement):
"""
AST Node for a base costs table.
@ivar costs: List of base cost values to set.
@type costs: C{list} of L{Assignment}
"""
def __init__(self, costs, pos):
base_statement.BaseStatement.__init__(self, "basecost-block", pos)
self.costs = costs
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 = expression.BinOp(nmlop.ADD, cost.value, expression.ConstantNumeric(8), cost.value.pos).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 list(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 debug_print(self, indentation):
generic.print_dbg(indentation, 'Base costs')
for cost in self.costs:
cost.debug_print(indentation + 2)
def get_action_list(self):
return action0.get_basecost_action(self)
def __str__(self):
ret = "basecost {\n"
for cost in self.costs:
ret += "\t{}: {};\n".format(cost.name, cost.value)
ret += "}\n"
return ret
base_cost_table = {
'PR_STATION_VALUE' : (0, ''),
'PR_BUILD_RAIL' : (1, 'PR_CONSTRUCTION'),
'PR_BUILD_ROAD' : (2, 'PR_CONSTRUCTION'),
'PR_BUILD_SIGNALS' : (3, 'PR_CONSTRUCTION'),
'PR_BUILD_BRIDGE' : (4, 'PR_CONSTRUCTION'),
'PR_BUILD_DEPOT_TRAIN' : (5, 'PR_CONSTRUCTION'),
'PR_BUILD_DEPOT_ROAD' : (6, 'PR_CONSTRUCTION'),
'PR_BUILD_DEPOT_SHIP' : (7, 'PR_CONSTRUCTION'),
'PR_BUILD_TUNNEL' : (8, 'PR_CONSTRUCTION'),
'PR_BUILD_STATION_RAIL' : (9, 'PR_CONSTRUCTION'),
'PR_BUILD_STATION_RAIL_LENGTH' : (10, 'PR_CONSTRUCTION'),
'PR_BUILD_STATION_AIRPORT' : (11, 'PR_CONSTRUCTION'),
'PR_BUILD_STATION_BUS' : (12, 'PR_CONSTRUCTION'),
'PR_BUILD_STATION_TRUCK' : (13, 'PR_CONSTRUCTION'),
'PR_BUILD_STATION_DOCK' : (14, 'PR_CONSTRUCTION'),
'PR_BUILD_VEHICLE_TRAIN' : (15, 'PR_BUILD_VEHICLE'),
'PR_BUILD_VEHICLE_WAGON' : (16, 'PR_BUILD_VEHICLE'),
'PR_BUILD_VEHICLE_AIRCRAFT' : (17, 'PR_BUILD_VEHICLE'),
'PR_BUILD_VEHICLE_ROAD' : (18, 'PR_BUILD_VEHICLE'),
'PR_BUILD_VEHICLE_SHIP' : (19, 'PR_BUILD_VEHICLE'),
'PR_BUILD_TREES' : (20, 'PR_CONSTRUCTION'),
'PR_TERRAFORM' : (21, 'PR_CONSTRUCTION'),
'PR_CLEAR_GRASS' : (22, 'PR_CONSTRUCTION'),
'PR_CLEAR_ROUGH' : (23, 'PR_CONSTRUCTION'),
'PR_CLEAR_ROCKS' : (24, 'PR_CONSTRUCTION'),
'PR_CLEAR_FIELDS' : (25, 'PR_CONSTRUCTION'),
'PR_CLEAR_TREES' : (26, 'PR_CONSTRUCTION'),
'PR_CLEAR_RAIL' : (27, 'PR_CONSTRUCTION'),
'PR_CLEAR_SIGNALS' : (28, 'PR_CONSTRUCTION'),
'PR_CLEAR_BRIDGE' : (29, 'PR_CONSTRUCTION'),
'PR_CLEAR_DEPOT_TRAIN' : (30, 'PR_CONSTRUCTION'),
'PR_CLEAR_DEPOT_ROAD' : (31, 'PR_CONSTRUCTION'),
'PR_CLEAR_DEPOT_SHIP' : (32, 'PR_CONSTRUCTION'),
'PR_CLEAR_TUNNEL' : (33, 'PR_CONSTRUCTION'),
'PR_CLEAR_WATER' : (34, 'PR_CONSTRUCTION'),
'PR_CLEAR_STATION_RAIL' : (35, 'PR_CONSTRUCTION'),
'PR_CLEAR_STATION_AIRPORT' : (36, 'PR_CONSTRUCTION'),
'PR_CLEAR_STATION_BUS' : (37, 'PR_CONSTRUCTION'),
'PR_CLEAR_STATION_TRUCK' : (38, 'PR_CONSTRUCTION'),
'PR_CLEAR_STATION_DOCK' : (39, 'PR_CONSTRUCTION'),
'PR_CLEAR_HOUSE' : (40, 'PR_CONSTRUCTION'),
'PR_CLEAR_ROAD' : (41, 'PR_CONSTRUCTION'),
'PR_RUNNING_TRAIN_STEAM' : (42, 'PR_RUNNING'),
'PR_RUNNING_TRAIN_DIESEL' : (43, 'PR_RUNNING'),
'PR_RUNNING_TRAIN_ELECTRIC' : (44, 'PR_RUNNING'),
'PR_RUNNING_AIRCRAFT' : (45, 'PR_RUNNING'),
'PR_RUNNING_ROADVEH' : (46, 'PR_RUNNING'),
'PR_RUNNING_SHIP' : (47, 'PR_RUNNING'),
'PR_BUILD_INDUSTRY' : (48, 'PR_CONSTRUCTION'),
'PR_CLEAR_INDUSTRY' : (49, 'PR_CONSTRUCTION'),
'PR_BUILD_UNMOVABLE' : (50, 'PR_CONSTRUCTION'),
'PR_CLEAR_UNMOVABLE' : (51, 'PR_CONSTRUCTION'),
'PR_BUILD_WAYPOINT_RAIL' : (52, 'PR_CONSTRUCTION'),
'PR_CLEAR_WAYPOINT_RAIL' : (53, 'PR_CONSTRUCTION'),
'PR_BUILD_WAYPOINT_BUOY' : (54, 'PR_CONSTRUCTION'),
'PR_CLEAR_WAYPOINT_BUOY' : (55, 'PR_CONSTRUCTION'),
'PR_TOWN_ACTION' : (56, 'PR_CONSTRUCTION'),
'PR_BUILD_FOUNDATION' : (57, 'PR_CONSTRUCTION'),
'PR_BUILD_INDUSTRY_RAW' : (58, 'PR_CONSTRUCTION'),
'PR_BUILD_TOWN' : (59, 'PR_CONSTRUCTION'),
'PR_BUILD_CANAL' : (60, 'PR_CONSTRUCTION'),
'PR_CLEAR_CANAL' : (61, 'PR_CONSTRUCTION'),
'PR_BUILD_AQUEDUCT' : (62, 'PR_CONSTRUCTION'),
'PR_CLEAR_AQUEDUCT' : (63, 'PR_CONSTRUCTION'),
'PR_BUILD_LOCK' : (64, 'PR_CONSTRUCTION'),
'PR_CLEAR_LOCK' : (65, 'PR_CONSTRUCTION'),
'PR_MAINTENANCE_RAIL' : (66, 'PR_RUNNING'),
'PR_MAINTENANCE_ROAD' : (67, 'PR_RUNNING'),
'PR_MAINTENANCE_CANAL' : (68, 'PR_RUNNING'),
'PR_MAINTENANCE_STATION' : (69, 'PR_RUNNING'),
'PR_MAINTENANCE_AIRPORT' : (70, 'PR_RUNNING'),
}
generic_base_costs = ['PR_CONSTRUCTION', 'PR_RUNNING', 'PR_BUILD_VEHICLE']
nml-0.4.5/nml/ast/replace.py 0000644 0005672 0005672 00000014664 13315644406 017033 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, global_constants
from nml.actions import actionA, action5
from nml.ast import base_statement, sprite_container
class ReplaceSprite(base_statement.BaseStatement, sprite_container.SpriteContainer):
"""
AST node for a 'replace' block.
NML syntax: replace [name] (start_id[, default_file]) { ..real sprites.. }
@ivar start_id: First sprite to replace. Extracted from C{param_list} during pre-processing.
@type start_id: C{Expression}
@ivar image_file: Default image file to use for sprites.
@type image_file: C{None} if not specified, else L{StringLiteral}
@ivar sprite_list: List of real sprites to use
@type sprite_list: Heterogeneous C{list} of L{RealSprite}, L{TemplateUsage}
"""
def __init__(self, param_list, sprite_list, name, pos):
base_statement.BaseStatement.__init__(self, "replace-block", pos)
sprite_container.SpriteContainer.__init__(self, "replace-block", name)
num_params = len(param_list)
if not (1 <= num_params <= 2):
raise generic.ScriptError("replace-block requires 1 or 2 parameters, encountered " + str(num_params), pos)
self.start_id = param_list[0]
if num_params >= 2:
self.image_file = param_list[1].reduce()
if not isinstance(self.image_file, expression.StringLiteral):
raise generic.ScriptError("replace-block parameter 2 'file' must be a string literal", self.image_file.pos)
else:
self.image_file = None
self.sprite_list = sprite_list
self.add_sprite_data(self.sprite_list, self.image_file, pos)
def pre_process(self):
self.start_id = self.start_id.reduce(global_constants.const_list)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Replace sprites starting at')
self.start_id.debug_print(indentation + 2)
generic.print_dbg(indentation + 2, 'Source:', self.image_file.value if self.image_file is not None else 'None')
if self.block_name is not None:
generic.print_dbg(indentation + 2, 'Name:', self.block_name.value)
generic.print_dbg(indentation + 2, 'Sprites:')
for sprite in self.sprite_list:
sprite.debug_print(indentation + 4)
def get_action_list(self):
return actionA.parse_actionA(self)
def __str__(self):
name = str(self.block_name) if self.block_name is not None else ""
def_file = "" if self.image_file is None else ", " + str(self.image_file)
ret = "replace {}({}{}) {{\n".format(name, self.start_id, def_file)
for sprite in self.sprite_list:
ret += "\t{}\n".format(sprite)
ret += "}\n"
return ret
class ReplaceNewSprite(base_statement.BaseStatement, sprite_container.SpriteContainer):
"""
AST node for a 'replacenew' block.
NML syntax: replacenew [name](type[, default_file[, offset]]) { ..real sprites.. }
@ivar type: Type of sprites to replace.
@type type: L{Identifier}
@ivar image_file: Default image file to use for sprites.
@type image_file: C{None} if not specified, else L{StringLiteral}
@ivar offset: Offset into the block of sprites.
@type offset: C{int}
@ivar sprite_list: List of real sprites to use
@type sprite_list: Heterogeneous C{list} of L{RealSprite}, L{TemplateUsage}
"""
def __init__(self, param_list, sprite_list, name, pos):
base_statement.BaseStatement.__init__(self, "replacenew-block", pos)
sprite_container.SpriteContainer.__init__(self, "replacenew-block", name)
num_params = len(param_list)
if not (1 <= num_params <= 3):
raise generic.ScriptError("replacenew-block requires 1 to 3 parameters, encountered " + str(num_params), pos)
self.type = param_list[0]
if not isinstance(self.type, expression.Identifier):
raise generic.ScriptError("replacenew parameter 'type' must be an identifier of a sprite replacement type", self.type.pos)
if num_params >= 2:
self.image_file = param_list[1].reduce()
if not isinstance(self.image_file, expression.StringLiteral):
raise generic.ScriptError("replacenew-block parameter 2 'file' must be a string literal", self.image_file.pos)
else:
self.image_file = None
if num_params >= 3:
self.offset = param_list[2].reduce_constant().value
generic.check_range(self.offset, 0, 0xFFFF, "replacenew-block parameter 3 'offset'", param_list[2].pos)
else:
self.offset = 0
self.sprite_list = sprite_list
self.add_sprite_data(self.sprite_list, self.image_file, pos)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Replace sprites for new features of type', self.type)
generic.print_dbg(indentation + 2, 'Offset: ', self.offset)
generic.print_dbg(indentation + 2, 'Source: ', self.image_file.value if self.image_file is not None else 'None')
if self.block_name is not None:
generic.print_dbg(indentation + 2, 'Name:', self.block_name.value)
generic.print_dbg(indentation + 2, 'Sprites:')
for sprite in self.sprite_list:
sprite.debug_print(indentation + 4)
def get_action_list(self):
return action5.parse_action5(self)
def __str__(self):
name = str(self.block_name) if self.block_name is not None else ""
params = [self.type]
if self.image_file is not None:
params.append(self.image_file)
if self.offset != 0:
params.append(self.offset)
ret = "replacenew {}({}) {{\n".format(name, ", ".join(str(param) for param in params))
for sprite in self.sprite_list:
ret += "\t{}\n".format(sprite)
ret += "}\n"
return ret
nml-0.4.5/nml/ast/item.py 0000644 0005672 0005672 00000026723 13315644406 016355 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, global_constants, unit
from nml.ast import base_statement, general
from nml.actions import action0, action2, action2var, action3
item_feature = None
item_id = None
item_size = None
class Item(base_statement.BaseStatementList):
"""
AST-node representing an item block
@ivar feature: Feature of the item
@type feature: L{ConstantNumeric}
@ivar name: Name of the item
@type name: L{Identifier} or C{None} if N/A.
@ivar id: Numeric ID of the item
@type id: L{ConstantNumeric} or C{None} if N/A
@ivar size: Size, used by houses only
@type size: L{ConstantNumeric} or C{None} if N/A
"""
def __init__(self, params, body, pos):
base_statement.BaseStatementList.__init__(self, "item-block", pos, base_statement.BaseStatementList.LIST_TYPE_ITEM, body)
if not (1 <= len(params) <= 4):
raise generic.ScriptError("Item block requires between 1 and 4 parameters, found {:d}.".format(len(params)), self.pos)
self.feature = general.parse_feature(params[0])
if self.feature.value in (0x08, 0x0C, 0x0E):
raise generic.ScriptError("Defining item blocks for this feature is not allowed.", self.pos)
self.name = params[1] if len(params) >= 2 else None
self.id = params[2].reduce_constant(global_constants.const_list) if len(params) >= 3 else None
if isinstance(self.id, expression.ConstantNumeric) and self.id.value == -1:
self.id = None # id == -1 means default
if len(params) >= 4:
if self.feature.value != 0x07:
raise generic.ScriptError("item-block parameter 4 'size' may only be set for houses", params[3].pos)
self.size = params[3].reduce_constant(global_constants.const_list)
if self.size.value not in list(action0.house_sizes.keys()):
raise generic.ScriptError("item-block parameter 4 'size' does not have a valid value", self.size.pos)
else:
self.size = None
def register_names(self):
if self.name:
if not isinstance(self.name, expression.Identifier):
raise generic.ScriptError("Item parameter 2 'name' should be an identifier", self.pos)
if self.name.value in global_constants.item_names:
existing_id = global_constants.item_names[self.name.value].id
if self.id is not None and existing_id.value != self.id.value:
raise generic.ScriptError("Duplicate item with name '{}'. This item has already been assigned to id {:d}, cannot reassign to id {:d}".format(self.name.value, existing_id.value, self.id.value), self.pos)
self.id = existing_id
# We may have to reserve multiple item IDs for houses
num_ids = action0.house_sizes[self.size.value] if self.size is not None else 1
if self.id is None:
self.id = expression.ConstantNumeric(action0.get_free_id(self.feature.value, num_ids, self.pos))
elif not action0.check_id_range(self.feature.value, self.id.value, num_ids, self.id.pos):
action0.mark_id_used(self.feature.value, self.id.value, num_ids)
if self.name is not None:
global_constants.item_names[self.name.value] = self
base_statement.BaseStatementList.register_names(self)
def pre_process(self):
global item_feature, item_id, item_size
item_id = self.id
item_feature = self.feature.value
item_size = self.size
base_statement.BaseStatementList.pre_process(self)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Item, feature', hex(self.feature.value))
base_statement.BaseStatementList.debug_print(self, indentation + 2)
def get_action_list(self):
global item_feature, item_id, item_size
item_id = self.id
item_feature = self.feature.value
item_size = self.size
return base_statement.BaseStatementList.get_action_list(self)
def __str__(self):
ret = 'item({:d}'.format(self.feature.value)
if self.name is not None:
ret += ', {}'.format(self.name)
ret += ', {}'.format(str(self.id) if self.id is not None else "-1")
if self.size is not None:
sizes =["1X1", None, "2X1", "1X2", "2X2"]
ret += ', HOUSE_SIZE_{}'.format(sizes[self.size.value])
ret += ') {\n'
ret += base_statement.BaseStatementList.__str__(self)
ret += '}\n'
return ret
class Property(object):
"""
AST-node representing a single property. These are only valid
insde a PropertyBlock.
@ivar name: The name (or number) of this property.
@type name: L{Identifier} or L{ConstantNumeric}.
@ivar value: The value that will be assigned to this property.
@type value: L{Expression}.
@ivar unit: The unit of the value.
@type unit: L{Unit}
@ivar pos: Position information.
@type pos: L{Position}
"""
def __init__(self, name, value, unit, pos):
self.pos = pos
self.name = name
self.value = value
self.unit = unit
def pre_process(self):
self.value = self.value.reduce(global_constants.const_list, unknown_id_fatal = False)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Property:', self.name.value)
self.value.debug_print(indentation + 2)
def __str__(self):
unit = '' if self.unit is None else ' ' + str(self.unit)
return '\t{}: {}{};'.format(self.name, self.value, unit)
class PropertyBlock(base_statement.BaseStatement):
"""
Block that contains a list of property/value pairs to be assigned
to the current item.
@ivar prop_list: List of properties.
@type prop_list: C{list} of L{Property}
"""
def __init__(self, prop_list, pos):
base_statement.BaseStatement.__init__(self, "property-block", pos, in_item = True, out_item = False)
self.prop_list = prop_list
def pre_process(self):
for prop in self.prop_list:
prop.pre_process()
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Property block:')
for prop in self.prop_list:
prop.debug_print(indentation + 2)
def get_action_list(self):
return action0.parse_property_block(self.prop_list, item_feature, item_id, item_size)
def __str__(self):
ret = 'property {\n'
for prop in self.prop_list:
ret += '{}\n'.format(prop)
ret += '}\n'
return ret
class LiveryOverride(base_statement.BaseStatement):
def __init__(self, wagon_id, graphics_block, pos):
base_statement.BaseStatement.__init__(self, "livery override", pos, in_item = True, out_item = False)
self.graphics_block = graphics_block
self.wagon_id = wagon_id
def pre_process(self):
self.graphics_block.pre_process()
pass
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Livery override, wagon id:')
self.wagon_id.debug_print(indentation + 2)
for graphics in self.graphics_block.graphics_list:
graphics.debug_print(indentation + 2)
generic.print_dbg(indentation + 2, 'Default graphics:', self.graphics_block.default_graphics)
def get_action_list(self):
wagon_id = self.wagon_id.reduce_constant([(global_constants.item_names, global_constants.item_to_id)])
return action3.parse_graphics_block(self.graphics_block, item_feature, wagon_id, item_size, True)
def __str__(self):
ret = 'livery_override({}) {{\n'.format(self.wagon_id)
for graphics in self.graphics_block.graphics_list:
ret += "\t{}\n".format(graphics)
if self.graphics_block.default_graphics is not None:
ret += '\t{}\n'.format(self.graphics_block.default_graphics)
ret += '}\n'
return ret
graphics_base_class = action2.make_sprite_group_class(False, False, True)
class GraphicsBlock(graphics_base_class):
def __init__(self, graphics_list, default_graphics, pos):
base_statement.BaseStatement.__init__(self, "graphics-block", pos, in_item = True, out_item = False)
self.graphics_list = graphics_list
self.default_graphics = default_graphics
def pre_process(self):
for graphics_def in self.graphics_list:
graphics_def.reduce_expressions(item_feature)
if self.default_graphics is not None:
if self.default_graphics.value is None:
raise generic.ScriptError("Returning the computed value is not possible in a graphics-block, as there is no computed value.", self.pos)
self.default_graphics.value = action2var.reduce_varaction2_expr(self.default_graphics.value, item_feature)
# initialize base class and pre_process it as well (in that order)
self.initialize(None, item_feature)
graphics_base_class.pre_process(self)
def collect_references(self):
all_refs = []
for result in [g.result for g in self.graphics_list] + ([self.default_graphics] if self.default_graphics is not None else []):
if isinstance(result.value, expression.SpriteGroupRef):
all_refs.append(result.value)
return all_refs
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Graphics block:')
for graphics in self.graphics_list:
graphics.debug_print(indentation + 2)
if self.default_graphics is not None:
generic.print_dbg(indentation + 2, 'Default graphics:')
self.default_graphics.debug_print(indentation + 4)
def get_action_list(self):
if self.prepare_act2_output():
return action3.parse_graphics_block(self, item_feature, item_id, item_size)
return []
def __str__(self):
ret = 'graphics {\n'
for graphics in self.graphics_list:
ret += "\t{}\n".format(graphics)
if self.default_graphics is not None:
ret += '\t{}\n'.format(self.default_graphics)
ret += '}\n'
return ret
class GraphicsDefinition(object):
def __init__(self, cargo_id, result, unit = None):
self.cargo_id = cargo_id
self.result = result
self.unit = unit
def reduce_expressions(self, var_feature):
# Do not reduce cargo-id (yet)
if self.result.value is None:
raise generic.ScriptError("Returning the computed value is not possible in a graphics-block, as there is no computed value.", self.result.pos)
self.result.value = action2var.reduce_varaction2_expr(self.result.value, var_feature)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Cargo ID:')
self.cargo_id.debug_print(indentation + 2)
generic.print_dbg(indentation, 'Result:')
self.result.debug_print(indentation + 2)
def __str__(self):
return '{}: {}'.format(self.cargo_id, self.result)
nml-0.4.5/nml/ast/produce.py 0000644 0005672 0005672 00000006117 13315644406 017053 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic
from nml.actions import action2, action2var, action2production
from nml.ast import base_statement
produce_base_class = action2.make_sprite_group_class(False, True, True)
class Produce(produce_base_class):
"""
AST node for a 'produce'-block, which is basically equivalent to the production callback.
Syntax: produce(name, sub1, sub2, sub3, add1, add2[, again])
@ivar param_list: List of parameters supplied to the produce-block.
- 0..2: Amounts of cargo to subtract from input
- 3..4: Amounts of cargo to add to output
- 5: Run the production CB again if nonzero
@type param_list: C{list} of L{Expression}
"""
def __init__(self, param_list, pos):
base_statement.BaseStatement.__init__(self, "produce-block", pos, False, False)
if not (6 <= len(param_list) <= 7):
raise generic.ScriptError("produce-block requires 6 or 7 parameters, encountered {:d}".format(len(param_list)), self.pos)
name = param_list[0]
if not isinstance(name, expression.Identifier):
raise generic.ScriptError("produce parameter 1 'name' should be an identifier.", name.pos)
self.initialize(name, 0x0A)
self.param_list = param_list[1:]
if len(self.param_list) < 6:
self.param_list.append(expression.ConstantNumeric(0))
def pre_process(self):
for i, param in enumerate(self.param_list):
self.param_list[i] = action2var.reduce_varaction2_expr(param, 0x0A)
produce_base_class.pre_process(self)
def collect_references(self):
return []
def __str__(self):
return 'produce({});\n'.format(', '.join(str(x) for x in [self.name] + self.param_list))
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Produce, name =', self.name)
generic.print_dbg(indentation + 2, 'Subtract from input:')
for expr in self.param_list[0:3]:
expr.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Add to output:')
for expr in self.param_list[3:5]:
expr.debug_print(indentation + 4)
generic.print_dbg(indentation + 2, 'Again:')
self.param_list[5].debug_print(indentation + 4)
def get_action_list(self):
if self.prepare_act2_output():
return action2production.get_production_actions(self)
return []
nml-0.4.5/nml/_lz77.c 0000644 0005672 0005672 00000013326 13315644406 015357 0 ustar jenkins jenkins 0000000 0000000 /* NML is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* NML is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with NML; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/**
* @file
* Cython accelerator module.
*
* Loaded by lz77.py if available, to replace the universal python code if possible.
*/
#include
/**
* Resizeable buffer/array.
*/
typedef struct {
char *buf; ///< Plain data.
int used; ///< Number of bytes used.
int allocated; ///< Number of bytes allocated.
} buffer_t;
/**
* Append a byte to a buffer.
* If the buffer is too small, it is enlarged.
* @param data Buffer.
* @param b Byte to add.
*/
static inline void append_byte(buffer_t *data, char b)
{
if (data->used + 1 > data->allocated) {
data->allocated += 0x10000;
data->buf = realloc(data->buf, data->allocated);
}
data->buf[data->used++] = b;
}
/**
* Append a byte array to a buffer.
* If the buffer is too small, it is enlarged.
* @param data Buffer.
* @param b Data to add.
* @param l Size of \a b.
*/
static inline void append_bytes(buffer_t *data, const char *b, int l)
{
if (data->used + l > data->allocated) {
data->allocated = (data->used + l + 0xFFFF) & ~0xFFFF;
data->buf = realloc(data->buf, data->allocated);
}
memcpy(data->buf + data->used, b, l);
data->used += l;
}
/**
* Locate a byte sequence in a buffer.
* @param pat_data Pattern to look for.
* @param pat_size Size of \a pat_data.
* @param data Data to search in.
* @param data__size Size of \a data.
* @return Offset of first occurence, or -1 if no match.
*/
static inline int find(const char *pat_data, int pat_size, const char *data, int data_size)
{
int i;
for (i = 0; i + pat_size <= data_size; ++i) {
/* Apparently this loop is optimised quite well by gcc.
* Usage of __builtin_bcmp performed terrible, since I didn't manage to get it inlined.
* It it worth noting though, that pat_size is very small (<= 16). */
int j = 0;
while (j < pat_size && pat_data[j] == data[i + j]) ++j;
if (j == pat_size) return i;
}
return -1;
}
/**
* GRF compression algorithm.
* @param input_data Uncompressed data.
* @param input_size Size of \a input_data.
* @param output Compressed data.
*/
static void encode(const char *input_data, int input_size, buffer_t *output)
{
char literal[0x80];
int literal_size = 0;
int position = 0;
while (position < input_size) {
int start_pos = position - (1 << 11) + 1;
if (start_pos < 0) start_pos = 0;
/* Loop through the lookahead buffer. */
int max_look = input_size - position + 1;
if (max_look > 16) max_look = 16;
int overlap_pos = 0;
int overlap_len = 0;
int i;
for (i = 3; i < max_look; ++i) {
/* Find the pattern match in the window. */
int result = find(input_data + position, i, input_data + start_pos, position - start_pos);
/* If match failed, we've found the longest. */
if (result < 0) break;
overlap_pos = position - start_pos - result;
overlap_len = i;
start_pos += result;
}
if (overlap_len > 0) {
if (literal_size > 0) {
append_byte(output, literal_size);
append_bytes(output, literal, literal_size);
literal_size = 0;
}
int val = 0x80 | (16 - overlap_len) << 3 | overlap_pos >> 8;
append_byte(output, val);
append_byte(output, overlap_pos & 0xFF);
position += overlap_len;
} else {
literal[literal_size++] = input_data[position];
if (literal_size == sizeof(literal)) {
append_byte(output, 0);
append_bytes(output, literal, literal_size);
literal_size = 0;
}
position += 1;
}
}
if (literal_size > 0) {
append_byte(output, literal_size);
append_bytes(output, literal, literal_size);
literal_size = 0;
}
}
/**
* Interface method to Python.
*
* @param self Unused.
* @param args Uncompressed data as "str", "bytes", "bytearray", or anything providing the buffer interface.
* @return Compressed data as "bytes".
*/
static PyObject *lz77_encode(PyObject *self, PyObject *args)
{
PyObject *result = NULL;
Py_buffer input;
if (PyArg_ParseTuple(args, "s*", &input) && input.buf) {
buffer_t output = {0};
Py_BEGIN_ALLOW_THREADS
encode((const char*)input.buf, input.len, &output);
Py_END_ALLOW_THREADS
PyBuffer_Release(&input);
result = Py_BuildValue("y#", output.buf, output.used);
free(output.buf);
}
return result;
}
/**
* Function table.
*/
static PyMethodDef lz77Methods[] = {
{"encode", lz77_encode, METH_VARARGS, "GRF compression algorithm"},
{NULL, NULL, 0, NULL}
};
/**
* Module table.
*/
static struct PyModuleDef lz77module = {
PyModuleDef_HEAD_INIT,
"nml_lz77", NULL, -1,
lz77Methods
};
/**
* Module intialisation.
* Called by Python upon import.
*/
PyMODINIT_FUNC
PyInit_nml_lz77(void)
{
return PyModule_Create(&lz77module);
}
nml-0.4.5/nml/__version__.py 0000644 0005672 0005672 00000000073 13315644467 017106 0 ustar jenkins jenkins 0000000 0000000 # this file is autogenerated by setup.py
version = "0.4.5"
nml-0.4.5/nml/palette.py 0000644 0005672 0005672 00000047134 13315644406 016265 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
raw_palette_data = [
#DOS palette
[
0, 0, 255, 16, 16, 16, 32, 32, 32, 48, 48, 48,
64, 64, 64, 80, 80, 80, 100, 100, 100, 116, 116, 116,
132, 132, 132, 148, 148, 148, 168, 168, 168, 184, 184, 184,
200, 200, 200, 216, 216, 216, 232, 232, 232, 252, 252, 252,
52, 60, 72, 68, 76, 92, 88, 96, 112, 108, 116, 132,
132, 140, 152, 156, 160, 172, 176, 184, 196, 204, 208, 220,
48, 44, 4, 64, 60, 12, 80, 76, 20, 96, 92, 28,
120, 120, 64, 148, 148, 100, 176, 176, 132, 204, 204, 168,
72, 44, 4, 88, 60, 20, 104, 80, 44, 124, 104, 72,
152, 132, 92, 184, 160, 120, 212, 188, 148, 244, 220, 176,
64, 0, 4, 88, 4, 16, 112, 16, 32, 136, 32, 52,
160, 56, 76, 188, 84, 108, 204, 104, 124, 220, 132, 144,
236, 156, 164, 252, 188, 192, 252, 208, 0, 252, 232, 60,
252, 252, 128, 76, 40, 0, 96, 60, 8, 116, 88, 28,
136, 116, 56, 156, 136, 80, 176, 156, 108, 196, 180, 136,
68, 24, 0, 96, 44, 4, 128, 68, 8, 156, 96, 16,
184, 120, 24, 212, 156, 32, 232, 184, 16, 252, 212, 0,
252, 248, 128, 252, 252, 192, 32, 4, 0, 64, 20, 8,
84, 28, 16, 108, 44, 28, 128, 56, 40, 148, 72, 56,
168, 92, 76, 184, 108, 88, 196, 128, 108, 212, 148, 128,
8, 52, 0, 16, 64, 0, 32, 80, 4, 48, 96, 4,
64, 112, 12, 84, 132, 20, 104, 148, 28, 128, 168, 44,
28, 52, 24, 44, 68, 32, 60, 88, 48, 80, 104, 60,
104, 124, 76, 128, 148, 92, 152, 176, 108, 180, 204, 124,
16, 52, 24, 32, 72, 44, 56, 96, 72, 76, 116, 88,
96, 136, 108, 120, 164, 136, 152, 192, 168, 184, 220, 200,
32, 24, 0, 56, 28, 0, 72, 40, 4, 88, 52, 12,
104, 64, 24, 124, 84, 44, 140, 108, 64, 160, 128, 88,
76, 40, 16, 96, 52, 24, 116, 68, 40, 136, 84, 56,
164, 96, 64, 184, 112, 80, 204, 128, 96, 212, 148, 112,
224, 168, 128, 236, 188, 148, 80, 28, 4, 100, 40, 20,
120, 56, 40, 140, 76, 64, 160, 100, 96, 184, 136, 136,
36, 40, 68, 48, 52, 84, 64, 64, 100, 80, 80, 116,
100, 100, 136, 132, 132, 164, 172, 172, 192, 212, 212, 224,
40, 20, 112, 64, 44, 144, 88, 64, 172, 104, 76, 196,
120, 88, 224, 140, 104, 252, 160, 136, 252, 188, 168, 252,
0, 24, 108, 0, 36, 132, 0, 52, 160, 0, 72, 184,
0, 96, 212, 24, 120, 220, 56, 144, 232, 88, 168, 240,
128, 196, 252, 188, 224, 252, 16, 64, 96, 24, 80, 108,
40, 96, 120, 52, 112, 132, 80, 140, 160, 116, 172, 192,
156, 204, 220, 204, 240, 252, 172, 52, 52, 212, 52, 52,
252, 52, 52, 252, 100, 88, 252, 144, 124, 252, 184, 160,
252, 216, 200, 252, 244, 236, 72, 20, 112, 92, 44, 140,
112, 68, 168, 140, 100, 196, 168, 136, 224, 200, 176, 248,
208, 184, 255, 232, 208, 252, 60, 0, 0, 92, 0, 0,
128, 0, 0, 160, 0, 0, 196, 0, 0, 224, 0, 0,
252, 0, 0, 252, 80, 0, 252, 108, 0, 252, 136, 0,
252, 164, 0, 252, 192, 0, 252, 220, 0, 252, 252, 0,
204, 136, 8, 228, 144, 4, 252, 156, 0, 252, 176, 48,
252, 196, 100, 252, 216, 152, 8, 24, 88, 12, 36, 104,
20, 52, 124, 28, 68, 140, 40, 92, 164, 56, 120, 188,
72, 152, 216, 100, 172, 224, 92, 156, 52, 108, 176, 64,
124, 200, 76, 144, 224, 92, 224, 244, 252, 200, 236, 248,
180, 220, 236, 132, 188, 216, 88, 152, 172, 244, 0, 244,
245, 0, 245, 246, 0, 246, 247, 0, 247, 248, 0, 248,
249, 0, 249, 250, 0, 250, 251, 0, 251, 252, 0, 252,
253, 0, 253, 254, 0, 254, 255, 0, 255, 76, 24, 8,
108, 44, 24, 144, 72, 52, 176, 108, 84, 210, 146, 126,
252, 60, 0, 252, 84, 0, 252, 104, 0, 252, 124, 0,
252, 148, 0, 252, 172, 0, 252, 196, 0, 64, 0, 0,
255, 0, 0, 48, 48, 0, 64, 64, 0, 80, 80, 0,
255, 255, 0, 32, 68, 112, 36, 72, 116, 40, 76, 120,
44, 80, 124, 48, 84, 128, 72, 100, 144, 100, 132, 168,
216, 244, 252, 96, 128, 164, 68, 96, 140, 255, 255, 255
], #end of DOS palette
#Windows palette
[
0, 0, 255, 238, 0, 238, 239, 0, 239, 240, 0, 240,
241, 0, 241, 242, 0, 242, 243, 0, 243, 244, 0, 244,
245, 0, 245, 246, 0, 246, 168, 168, 168, 184, 184, 184,
200, 200, 200, 216, 216, 216, 232, 232, 232, 252, 252, 252,
52, 60, 72, 68, 76, 92, 88, 96, 112, 108, 116, 132,
132, 140, 152, 156, 160, 172, 176, 184, 196, 204, 208, 220,
48, 44, 4, 64, 60, 12, 80, 76, 20, 96, 92, 28,
120, 120, 64, 148, 148, 100, 176, 176, 132, 204, 204, 168,
100, 100, 100, 116, 116, 116, 104, 80, 44, 124, 104, 72,
152, 132, 92, 184, 160, 120, 212, 188, 148, 244, 220, 176,
132, 132, 132, 88, 4, 16, 112, 16, 32, 136, 32, 52,
160, 56, 76, 188, 84, 108, 204, 104, 124, 220, 132, 144,
236, 156, 164, 252, 188, 192, 252, 208, 0, 252, 232, 60,
252, 252, 128, 76, 40, 0, 96, 60, 8, 116, 88, 28,
136, 116, 56, 156, 136, 80, 176, 156, 108, 196, 180, 136,
68, 24, 0, 96, 44, 4, 128, 68, 8, 156, 96, 16,
184, 120, 24, 212, 156, 32, 232, 184, 16, 252, 212, 0,
252, 248, 128, 252, 252, 192, 32, 4, 0, 64, 20, 8,
84, 28, 16, 108, 44, 28, 128, 56, 40, 148, 72, 56,
168, 92, 76, 184, 108, 88, 196, 128, 108, 212, 148, 128,
8, 52, 0, 16, 64, 0, 32, 80, 4, 48, 96, 4,
64, 112, 12, 84, 132, 20, 104, 148, 28, 128, 168, 44,
64, 64, 64, 44, 68, 32, 60, 88, 48, 80, 104, 60,
104, 124, 76, 128, 148, 92, 152, 176, 108, 180, 204, 124,
16, 52, 24, 32, 72, 44, 56, 96, 72, 76, 116, 88,
96, 136, 108, 120, 164, 136, 152, 192, 168, 184, 220, 200,
32, 24, 0, 56, 28, 0, 80, 80, 80, 88, 52, 12,
104, 64, 24, 124, 84, 44, 140, 108, 64, 160, 128, 88,
76, 40, 16, 96, 52, 24, 116, 68, 40, 136, 84, 56,
164, 96, 64, 184, 112, 80, 204, 128, 96, 212, 148, 112,
224, 168, 128, 236, 188, 148, 80, 28, 4, 100, 40, 20,
120, 56, 40, 140, 76, 64, 160, 100, 96, 184, 136, 136,
36, 40, 68, 48, 52, 84, 64, 64, 100, 80, 80, 116,
100, 100, 136, 132, 132, 164, 172, 172, 192, 212, 212, 224,
48, 48, 48, 64, 44, 144, 88, 64, 172, 104, 76, 196,
120, 88, 224, 140, 104, 252, 160, 136, 252, 188, 168, 252,
0, 24, 108, 0, 36, 132, 0, 52, 160, 0, 72, 184,
0, 96, 212, 24, 120, 220, 56, 144, 232, 88, 168, 240,
128, 196, 252, 188, 224, 252, 16, 64, 96, 24, 80, 108,
40, 96, 120, 52, 112, 132, 80, 140, 160, 116, 172, 192,
156, 204, 220, 204, 240, 252, 172, 52, 52, 212, 52, 52,
252, 52, 52, 252, 100, 88, 252, 144, 124, 252, 184, 160,
252, 216, 200, 252, 244, 236, 72, 20, 112, 92, 44, 140,
112, 68, 168, 140, 100, 196, 168, 136, 224, 200, 176, 248,
208, 184, 255, 232, 208, 252, 60, 0, 0, 92, 0, 0,
128, 0, 0, 160, 0, 0, 196, 0, 0, 224, 0, 0,
252, 0, 0, 252, 80, 0, 252, 108, 0, 252, 136, 0,
252, 164, 0, 252, 192, 0, 252, 220, 0, 252, 252, 0,
204, 136, 8, 228, 144, 4, 252, 156, 0, 252, 176, 48,
252, 196, 100, 252, 216, 152, 8, 24, 88, 12, 36, 104,
20, 52, 124, 28, 68, 140, 40, 92, 164, 56, 120, 188,
72, 152, 216, 100, 172, 224, 92, 156, 52, 108, 176, 64,
124, 200, 76, 144, 224, 92, 224, 244, 252, 200, 236, 248,
180, 220, 236, 132, 188, 216, 88, 152, 172, 16, 16, 16,
32, 32, 32, 32, 68, 112, 36, 72, 116, 40, 76, 120,
44, 80, 124, 48, 84, 128, 72, 100, 144, 100, 132, 168,
216, 244, 252, 96, 128, 164, 68, 96, 140, 76, 24, 8,
108, 44, 24, 144, 72, 52, 176, 108, 84, 210, 146, 126,
252, 60, 0, 252, 84, 0, 252, 104, 0, 252, 124, 0,
252, 148, 0, 252, 172, 0, 252, 196, 0, 64, 0, 0,
255, 0, 0, 48, 48, 0, 64, 64, 0, 80, 80, 0,
255, 255, 0, 148, 148, 148, 247, 0, 247, 248, 0, 248,
249, 0, 249, 250, 0, 250, 251, 0, 251, 252, 0, 252,
253, 0, 253, 254, 0, 254, 255, 0, 255, 255, 255, 255
], #end of Windows palette
# DOS Toyland palette
[
0, 0, 255, 16, 16, 16, 32, 32, 32, 48, 48, 48,
64, 64, 64, 80, 80, 80, 100, 100, 100, 116, 116, 116,
132, 132, 132, 148, 148, 148, 168, 168, 168, 184, 184, 184,
200, 200, 200, 216, 216, 216, 232, 232, 232, 252, 252, 252,
52, 60, 72, 68, 76, 92, 88, 96, 112, 108, 116, 132,
132, 140, 152, 156, 160, 172, 176, 184, 196, 204, 208, 220,
48, 44, 4, 64, 60, 12, 80, 76, 20, 96, 92, 28,
120, 120, 64, 148, 148, 100, 176, 176, 132, 204, 204, 168,
72, 44, 4, 88, 60, 20, 104, 80, 44, 124, 104, 72,
152, 132, 92, 184, 160, 120, 212, 188, 148, 244, 220, 176,
64, 0, 4, 88, 4, 16, 112, 16, 32, 136, 32, 52,
160, 56, 76, 188, 84, 108, 204, 104, 124, 220, 132, 144,
236, 156, 164, 252, 188, 192, 252, 208, 0, 252, 232, 60,
252, 252, 128, 76, 40, 0, 96, 60, 8, 116, 88, 28,
136, 116, 56, 156, 136, 80, 176, 156, 108, 196, 180, 136,
68, 24, 0, 96, 44, 4, 128, 68, 8, 156, 96, 16,
184, 120, 24, 212, 156, 32, 232, 184, 16, 252, 212, 0,
252, 248, 128, 252, 252, 192, 32, 4, 0, 64, 20, 8,
84, 28, 16, 108, 44, 28, 128, 56, 40, 148, 72, 56,
168, 92, 76, 184, 108, 88, 196, 128, 108, 212, 148, 128,
8, 52, 0, 16, 64, 0, 32, 80, 4, 48, 96, 4,
64, 112, 12, 84, 132, 20, 104, 148, 28, 128, 168, 44,
28, 52, 24, 44, 68, 32, 60, 88, 48, 80, 104, 60,
104, 124, 76, 128, 148, 92, 152, 176, 108, 180, 204, 124,
16, 52, 24, 32, 72, 44, 56, 96, 72, 76, 116, 88,
96, 136, 108, 120, 164, 136, 152, 192, 168, 184, 220, 200,
32, 24, 0, 56, 28, 0, 72, 40, 4, 88, 52, 12,
104, 64, 24, 124, 84, 44, 140, 108, 64, 160, 128, 88,
76, 40, 16, 96, 52, 24, 116, 68, 40, 136, 84, 56,
164, 96, 64, 184, 112, 80, 204, 128, 96, 212, 148, 112,
224, 168, 128, 236, 188, 148, 80, 28, 4, 100, 40, 20,
120, 56, 40, 140, 76, 64, 160, 100, 96, 184, 136, 136,
36, 40, 68, 48, 52, 84, 64, 64, 100, 80, 80, 116,
100, 100, 136, 132, 132, 164, 172, 172, 192, 212, 212, 224,
40, 20, 112, 64, 44, 144, 88, 64, 172, 104, 76, 196,
120, 88, 224, 140, 104, 252, 160, 136, 252, 188, 168, 252,
0, 24, 108, 0, 36, 132, 0, 52, 160, 0, 72, 184,
0, 96, 212, 24, 120, 220, 56, 144, 232, 88, 168, 240,
128, 196, 252, 188, 224, 252, 16, 64, 96, 24, 80, 108,
40, 96, 120, 52, 112, 132, 80, 140, 160, 116, 172, 192,
156, 204, 220, 204, 240, 252, 172, 52, 52, 212, 52, 52,
252, 52, 52, 252, 100, 88, 252, 144, 124, 252, 184, 160,
252, 216, 200, 252, 244, 236, 72, 20, 112, 92, 44, 140,
112, 68, 168, 140, 100, 196, 168, 136, 224, 200, 176, 248,
208, 184, 255, 232, 208, 252, 60, 0, 0, 92, 0, 0,
128, 0, 0, 160, 0, 0, 196, 0, 0, 224, 0, 0,
252, 0, 0, 252, 80, 0, 252, 108, 0, 252, 136, 0,
252, 164, 0, 252, 192, 0, 252, 220, 0, 252, 252, 0,
204, 136, 8, 228, 144, 4, 252, 156, 0, 252, 176, 48,
252, 196, 100, 252, 216, 152, 8, 24, 88, 12, 36, 104,
20, 52, 124, 28, 68, 140, 40, 92, 164, 56, 120, 188,
72, 152, 216, 100, 172, 224, 92, 156, 52, 108, 176, 64,
124, 200, 76, 144, 224, 92, 224, 244, 252, 200, 236, 248,
180, 220, 236, 132, 188, 216, 88, 152, 172, 244, 0, 244,
245, 0, 245, 246, 0, 246, 247, 0, 247, 248, 0, 248,
249, 0, 249, 250, 0, 250, 251, 0, 251, 252, 0, 252,
253, 0, 253, 254, 0, 254, 255, 0, 255, 76, 24, 8,
108, 44, 24, 144, 72, 52, 176, 108, 84, 210, 146, 126,
252, 60, 0, 252, 84, 0, 252, 104, 0, 252, 124, 0,
252, 148, 0, 252, 172, 0, 252, 196, 0, 64, 0, 0,
255, 0, 0, 48, 48, 0, 64, 64, 0, 80, 80, 0,
255, 255, 0, 28, 108, 124, 32, 112, 128, 36, 116, 132,
40, 120, 136, 44, 124, 140, 92, 164, 184, 116, 180, 196,
216, 244, 252, 112, 176, 192, 88, 160, 180, 255, 255, 255
], #end of DOS Toyland palette
#Windows Toyland palette
[
0, 255, 255, 238, 0, 238, 239, 0, 239, 240, 0, 240,
241, 0, 241, 242, 0, 242, 243, 0, 243, 244, 0, 244,
245, 0, 245, 246, 0, 246, 168, 168, 168, 184, 184, 184,
200, 200, 200, 216, 216, 216, 232, 232, 232, 252, 252, 252,
52, 60, 72, 68, 76, 92, 88, 96, 112, 108, 116, 132,
132, 140, 152, 156, 160, 172, 176, 184, 196, 204, 208, 220,
48, 44, 4, 64, 60, 12, 80, 76, 20, 96, 92, 28,
120, 120, 64, 148, 148, 100, 176, 176, 132, 204, 204, 168,
100, 100, 100, 116, 116, 116, 104, 80, 44, 124, 104, 72,
152, 132, 92, 184, 160, 120, 212, 188, 148, 244, 220, 176,
132, 132, 132, 88, 4, 16, 112, 16, 32, 136, 32, 52,
160, 56, 76, 188, 84, 108, 204, 104, 124, 220, 132, 144,
236, 156, 164, 252, 188, 192, 252, 208, 0, 252, 232, 60,
252, 252, 128, 76, 40, 0, 96, 60, 8, 116, 88, 28,
136, 116, 56, 156, 136, 80, 176, 156, 108, 196, 180, 136,
68, 24, 0, 96, 44, 4, 128, 68, 8, 156, 96, 16,
184, 120, 24, 212, 156, 32, 232, 184, 16, 252, 212, 0,
252, 248, 128, 252, 252, 192, 32, 4, 0, 64, 20, 8,
84, 28, 16, 108, 44, 28, 128, 56, 40, 148, 72, 56,
168, 92, 76, 184, 108, 88, 196, 128, 108, 212, 148, 128,
8, 52, 0, 16, 64, 0, 32, 80, 4, 48, 96, 4,
64, 112, 12, 84, 132, 20, 104, 148, 28, 128, 168, 44,
64, 64, 64, 44, 68, 32, 60, 88, 48, 80, 104, 60,
104, 124, 76, 128, 148, 92, 152, 176, 108, 180, 204, 124,
16, 52, 24, 32, 72, 44, 56, 96, 72, 76, 116, 88,
96, 136, 108, 120, 164, 136, 152, 192, 168, 184, 220, 200,
32, 24, 0, 56, 28, 0, 80, 80, 80, 88, 52, 12,
104, 64, 24, 124, 84, 44, 140, 108, 64, 160, 128, 88,
76, 40, 16, 96, 52, 24, 116, 68, 40, 136, 84, 56,
164, 96, 64, 184, 112, 80, 204, 128, 96, 212, 148, 112,
224, 168, 128, 236, 188, 148, 80, 28, 4, 100, 40, 20,
120, 56, 40, 140, 76, 64, 160, 100, 96, 184, 136, 136,
36, 40, 68, 48, 52, 84, 64, 64, 100, 80, 80, 116,
100, 100, 136, 132, 132, 164, 172, 172, 192, 212, 212, 224,
48, 48, 48, 64, 44, 144, 88, 64, 172, 104, 76, 196,
120, 88, 224, 140, 104, 252, 160, 136, 252, 188, 168, 252,
0, 24, 108, 0, 36, 132, 0, 52, 160, 0, 72, 184,
0, 96, 212, 24, 120, 220, 56, 144, 232, 88, 168, 240,
128, 196, 252, 188, 224, 252, 16, 64, 96, 24, 80, 108,
40, 96, 120, 52, 112, 132, 80, 140, 160, 116, 172, 192,
156, 204, 220, 204, 240, 252, 172, 52, 52, 212, 52, 52,
252, 52, 52, 252, 100, 88, 252, 144, 124, 252, 184, 160,
252, 216, 200, 252, 244, 236, 72, 20, 112, 92, 44, 140,
112, 68, 168, 140, 100, 196, 168, 136, 224, 200, 176, 248,
208, 184, 255, 232, 208, 252, 60, 0, 0, 92, 0, 0,
128, 0, 0, 160, 0, 0, 196, 0, 0, 224, 0, 0,
252, 0, 0, 252, 80, 0, 252, 108, 0, 252, 136, 0,
252, 164, 0, 252, 192, 0, 252, 220, 0, 252, 252, 0,
204, 136, 8, 228, 144, 4, 252, 156, 0, 252, 176, 48,
252, 196, 100, 252, 216, 152, 8, 24, 88, 12, 36, 104,
20, 52, 124, 28, 68, 140, 40, 92, 164, 56, 120, 188,
72, 152, 216, 100, 172, 224, 92, 156, 52, 108, 176, 64,
124, 200, 76, 144, 224, 92, 224, 244, 252, 200, 236, 248,
180, 220, 236, 132, 188, 216, 88, 152, 172, 16, 16, 16,
32, 32, 32, 28, 108, 124, 32, 112, 128, 36, 116, 132,
40, 120, 136, 44, 124, 140, 92, 164, 184, 116, 180, 196,
216, 244, 252, 112, 176, 192, 88, 160, 180, 76, 24, 8,
108, 44, 24, 144, 72, 52, 176, 108, 84, 210, 146, 126,
252, 60, 0, 252, 84, 0, 252, 104, 0, 252, 124, 0,
252, 148, 0, 252, 172, 0, 252, 196, 0, 64, 0, 0,
255, 0, 0, 48, 48, 0, 64, 64, 0, 80, 80, 0,
255, 255, 0, 148, 148, 148, 247, 0, 247, 248, 0, 248,
249, 0, 249, 250, 0, 250, 251, 0, 251, 252, 0, 252,
253, 0, 253, 254, 0, 254, 255, 0, 255, 255, 255, 255
] #end of Windows Toyland palette
]
# Convert palettes to strings for fast comparison.
palette_data = [bytes(pal) for pal in raw_palette_data]
palette_name = ["DEFAULT", "LEGACY", "DEFAULT_TOYLAND", "LEGACY_TOYLAND"]
def validate_palette(image, filename):
palette = image.palette.palette
if len(palette) != 768:
raise generic.ImageError("Invalid palette; does not contain 256 entries.", filename)
for i, pal in enumerate(palette_data):
if pal != palette: continue
return palette_name[i]
raise generic.ImageError("Palette is not recognized as a valid palette.", filename)
nml-0.4.5/nml/actions/ 0000755 0005672 0005672 00000000000 13315644467 015713 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/nml/actions/actionF.py 0000644 0005672 0005672 00000016274 13315644406 017653 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
"""
Code for storing and generating action F
"""
from nml import expression, grfstrings, generic
from nml.actions import base_action
# Helper functions to allocate townname IDs
#
# Numbers available are from 0 to 0x7f (inclusive).
# These numbers can be in five states:
# - free: Number is available for use.
# - named: Number is allocated to represent a name.
# - safe numbered: Number is allocated by the user, and is safe to refer to.
# - unsafe numbered: Number is allocated by the user, and is not safe to refer to (that is, it is below the point of 'prepare_output')
# - invisible: Number is allocated by a final town_name, without attaching a name to it. It is not accessible any more.
# Instances of the TownNames class have a 'name' attribute, which can be 'None' (for an invisible number),
# a string (for a named number), or a (constant numeric) expression (for a safe/unsafe number).
#
total_numbers = 0x80
free_numbers = set(range(total_numbers)) #: Free numbers.
first_free_id = 0 #: All numbers before this are allocated.
named_numbers = {} #: Mapping of names to named numbers. Note that these are always safe to refer to.
numbered_numbers = set() #: Safe numbers introduced by the user (without name).
def get_free_id():
"""Allocate a number from the free_numbers."""
global first_free_id
while first_free_id not in free_numbers:
first_free_id = first_free_id + 1
if first_free_id >= total_numbers:
raise generic.ScriptError("Too many town name blocks. Some of these are autogenerated. Please use less town names.")
number = first_free_id
free_numbers.remove(number)
first_free_id = first_free_id + 1
return number
town_names_blocks = {} # Mapping of town_names ID number to TownNames instance.
def print_stats():
"""
Print statistics about used ids.
"""
num_used = total_numbers - len(free_numbers)
if num_used > 0:
generic.print_info("Town names: {}/{}".format(num_used, total_numbers))
class ActionF(base_action.BaseAction):
"""
Town names action.
@ivar name: Name ID of the town_name.
@type name: C{None}, L{Identifier}, or L{ConstantNumeric}
@ivar id_number: Allocated ID number for this town_name action F node.
@type id_number: C{None} or C{int}
@ivar style_name: Name of the translated string containing the name of the style, if any.
@type style_name: C{None} or L{String}
@ivar style_names: List translations of L{style_name}, pairs (languageID, text).
@type style_names: C{list} of (C{int}, L{Identifier})
@ivar parts: Parts of the names.
@type parts: C{list} of L{TownNamesPart}
@ivar free_bit: First available bit above the bits used by this block.
@type free_bit: C{None} if unset, else C{int}
@ivar pos: Position information of the 'town_names' block.
@type pos: L{Position}
"""
def __init__(self, name, id_number, style_name, parts, pos):
self.name = name
self.id_number = id_number
self.style_name = style_name
self.style_names = []
self.parts = parts
self.free_bit = None
self.pos = pos
def prepare_output(self, sprite_num):
# Resolve references to earlier townname actions
blocks = set()
for part in self.parts:
blocks.update(part.resolve_townname_id())
# Allocate a number for this action F.
if self.name is None or isinstance(self.name, expression.Identifier):
self.id_number = get_free_id()
if isinstance(self.name, expression.Identifier):
if self.name.value in named_numbers:
raise generic.ScriptError('Cannot define town name "{}", it is already in use'.format(self.name), self.pos)
named_numbers[self.name.value] = self.id_number # Add name to the set 'safe' names.
else: numbered_numbers.add(self.id_number) # Add number to the set of 'safe' numbers.
town_names_blocks[self.id_number] = self # Add self to the available blocks.
# Ask descendants for the lowest available bit.
if len(blocks) == 0: startbit = 0 # No descendants, all bits are free.
else: startbit = max(town_names_blocks[block].free_bit for block in blocks)
# Allocate random bits to all parts.
for part in self.parts:
num_bits = part.assign_bits(startbit)
startbit += num_bits
self.free_bit = startbit
if startbit > 32:
raise generic.ScriptError("Not enough random bits for the town name generation ({:d} needed, 32 available)".format(startbit), self.pos)
# Pull style names if needed.
if self.style_name is not None:
grfstrings.validate_string(self.style_name)
self.style_names = [(lang_id, grfstrings.get_translation(self.style_name, lang_id)) for lang_id in grfstrings.get_translations(self.style_name)]
self.style_names.append( (0x7F, grfstrings.get_translation(self.style_name)) )
self.style_names.sort()
if len(self.style_names) == 0:
raise generic.ScriptError('Style "{}" defined, but no translations found for it'.format(self.style_name.name.value), self.pos)
else: self.style_names = []
# Style names
def get_length_styles(self):
if len(self.style_names) == 0:
return 0
size = 0
for _lang, txt in self.style_names:
size += 1 + grfstrings.get_string_size(txt) # Language ID, text
return size + 1 # Terminating 0
def write_styles(self, handle):
if len(self.style_names) == 0: return
for lang, txt in self.style_names:
handle.print_bytex(lang)
handle.print_string(txt, final_zero = True)
handle.newline()
handle.print_bytex(0)
handle.newline()
# Parts
def get_length_parts(self):
size = 1 # num_parts byte
return size + sum(part.get_length() for part in self.parts)
def write_parts(self, handle):
handle.print_bytex(len(self.parts))
for part in self.parts:
part.write(handle)
handle.newline()
def write(self, handle):
handle.start_sprite(2 + self.get_length_styles() + self.get_length_parts())
handle.print_bytex(0xF)
handle.print_bytex(self.id_number | (0x80 if len(self.style_names) > 0 else 0))
handle.newline(str(self.name) if self.name is not None else "")
self.write_styles(handle)
self.write_parts(handle)
handle.end_sprite()
def skip_action7(self):
return False
nml-0.4.5/nml/actions/action14.py 0000644 0005672 0005672 00000022701 13315644406 017702 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import grfstrings
from nml.actions import base_action
class Action14(base_action.BaseAction):
def __init__(self, nodes):
self.nodes = nodes
def skip_action7(self):
return False
def write(self, file):
size = 2 # final 0-byte
for node in self.nodes: size += node.get_size()
file.start_sprite(size)
file.print_bytex(0x14)
for node in self.nodes:
node.write(file)
file.print_bytex(0)
file.end_sprite()
def split_action14(node, max_size):
if node.get_size() <= max_size:
return [node, None]
if not isinstance(node, BranchNode):
return [None, node]
new_node = BranchNode(node.id)
rest = BranchNode(node.id)
copy_to_rest = False
for subnode in node.subnodes:
if copy_to_rest:
rest.subnodes.append(subnode)
continue
new_subnode, subnode_rest = split_action14(subnode, max_size - new_node.get_size())
if new_subnode is not None:
new_node.subnodes.append(new_subnode)
if subnode_rest is not None:
rest.subnodes.append(subnode_rest)
copy_to_rest = True
assert len(rest.subnodes) > 0
if len(new_node.subnodes) == 0:
return [None, rest]
return [new_node, rest]
def get_actions(root):
action_list = []
while True:
node, root = split_action14(root, 65535)
assert node is not None
action_list.append(Action14([node]))
if root is None:
break
return action_list
class Action14Node(object):
def __init__(self, type_string, id):
self.type_string = type_string
self.id = id
def get_size(self):
"""
How many bytes will be written to the output file by L{write}?
@return: The size (in bytes) of this node.
"""
raise NotImplementedError('get_size must be implemented in Action14Node-subclass {!r}'.format(type(self)))
def write(self, file):
"""
Write this node to the output file.
@param file: The file to write the output to.
"""
raise NotImplementedError('write must be implemented in Action14Node-subclass {!r}'.format(type(self)))
def write_type_id(self, file):
file.print_string(self.type_string, False, True)
if isinstance(self.id, str):
file.print_string(self.id, False, True)
else:
file.print_dword(self.id)
class TextNode(Action14Node):
def __init__(self, id, string, skip_default_langid = False):
Action14Node.__init__(self, "T", id)
self.string = string
grfstrings.validate_string(self.string)
self.skip_default_langid = skip_default_langid
def get_size(self):
if self.skip_default_langid:
size = 0
else:
size = 6 + grfstrings.get_string_size(grfstrings.get_translation(self.string))
for lang_id in grfstrings.get_translations(self.string):
# 6 is for "T" (1), id (4), langid (1)
size += 6 + grfstrings.get_string_size(grfstrings.get_translation(self.string, lang_id))
return size
def write(self, file):
if not self.skip_default_langid:
self.write_type_id(file)
file.print_bytex(0x7F)
file.print_string(grfstrings.get_translation(self.string))
file.newline()
for lang_id in grfstrings.get_translations(self.string):
self.write_type_id(file)
file.print_bytex(lang_id)
file.print_string(grfstrings.get_translation(self.string, lang_id))
file.newline()
class BranchNode(Action14Node):
def __init__(self, id):
Action14Node.__init__(self, "C", id)
self.subnodes = []
def get_size(self):
size = 6 # "C", id, final 0-byte
for node in self.subnodes:
size += node.get_size()
return size
def write(self, file):
self.write_type_id(file)
file.newline()
for node in self.subnodes:
node.write(file)
file.print_bytex(0)
file.newline()
class BinaryNode(Action14Node):
def __init__(self, id, size, val = None):
Action14Node.__init__(self, "B", id)
self.size = size
self.val = val
def get_size(self):
return 7 + self.size # "B" (1), id (4), size (2), data (self.size)
def write(self, file):
self.write_type_id(file)
file.print_word(self.size)
file.print_varx(self.val, self.size)
file.newline()
class UsedPaletteNode(BinaryNode):
def __init__(self, pal):
BinaryNode.__init__(self, "PALS", 1)
self.pal = pal
def write(self, file):
self.write_type_id(file)
file.print_word(self.size)
file.print_string(self.pal, False, True)
file.newline()
class BlitterNode(BinaryNode):
def __init__(self, blitter):
BinaryNode.__init__(self, "BLTR", 1)
self.blitter = blitter
def write(self, file):
self.write_type_id(file)
file.print_word(self.size)
file.print_string(self.blitter, False, True)
file.newline()
class SettingMaskNode(BinaryNode):
def __init__(self, param_num, first_bit, num_bits):
BinaryNode.__init__(self, "MASK", 3)
self.param_num = param_num
self.first_bit = first_bit
self.num_bits = num_bits
def write(self, file):
self.write_type_id(file)
file.print_word(self.size)
file.print_byte(self.param_num)
file.print_byte(self.first_bit)
file.print_byte(self.num_bits)
file.newline()
class LimitNode(BinaryNode):
def __init__(self, min_val, max_val):
BinaryNode.__init__(self, "LIMI", 8)
self.min_val = min_val
self.max_val = max_val
def write(self, file):
self.write_type_id(file)
file.print_word(self.size)
file.print_dword(self.min_val)
file.print_dword(self.max_val)
file.newline()
def grf_name_desc_actions(root, name, desc, url, version, min_compatible_version):
if len(grfstrings.get_translations(name)) > 0:
name_node = TextNode("NAME", name, True)
root.subnodes.append(name_node)
if len(grfstrings.get_translations(desc)) > 0:
desc_node = TextNode("DESC", desc, True)
root.subnodes.append(desc_node)
if url is not None:
desc_node = TextNode("URL_", url)
root.subnodes.append(desc_node)
version_node = BinaryNode("VRSN", 4, version.value)
root.subnodes.append(version_node)
min_compatible_version_node = BinaryNode("MINV", 4, min_compatible_version.value)
root.subnodes.append(min_compatible_version_node)
def param_desc_actions(root, params):
num_params = 0
for param_desc in params:
num_params += len(param_desc.setting_list)
root.subnodes.append(BinaryNode("NPAR", 1, num_params))
param_root = BranchNode("PARA")
param_num = 0
setting_num = 0
for param_desc in params:
if param_desc.num is not None:
param_num = param_desc.num.value
for setting in param_desc.setting_list:
setting_node = BranchNode(setting_num)
if setting.name_string is not None:
setting_node.subnodes.append(TextNode("NAME", setting.name_string))
if setting.desc_string is not None:
setting_node.subnodes.append(TextNode("DESC", setting.desc_string))
if setting.type == 'int':
setting_node.subnodes.append(BinaryNode("MASK", 1, param_num))
min_val = setting.min_val.value if setting.min_val is not None else 0
max_val = setting.max_val.value if setting.max_val is not None else 0xFFFFFFFF
setting_node.subnodes.append(LimitNode(min_val, max_val))
if len(setting.val_names) > 0:
value_names_node = BranchNode("VALU")
for set_val_pair in setting.val_names:
value_names_node.subnodes.append(TextNode(set_val_pair[0], set_val_pair[1]))
setting_node.subnodes.append(value_names_node)
else:
assert setting.type == 'bool'
setting_node.subnodes.append(BinaryNode("TYPE", 1, 1))
bit = setting.bit_num.value if setting.bit_num is not None else 0
setting_node.subnodes.append(SettingMaskNode(param_num, bit, 1))
if setting.def_val is not None:
setting_node.subnodes.append(BinaryNode("DFLT", 4, setting.def_val.value))
param_root.subnodes.append(setting_node)
setting_num += 1
param_num += 1
if len(param_root.subnodes) > 0:
root.subnodes.append(param_root)
def PaletteAction(pal):
root = BranchNode("INFO")
pal_node = UsedPaletteNode(pal)
root.subnodes.append(pal_node)
return [Action14([root])]
nml-0.4.5/nml/actions/action11.py 0000644 0005672 0005672 00000010000 13315644406 017664 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
"""
Action 11 support classes (sounds).
"""
import os
from nml import generic
from nml.actions import base_action, action0
class Action11(base_action.BaseAction):
def __init__(self, num_sounds):
self.num_sounds = num_sounds
def write(self, file):
file.start_sprite(3)
file.print_bytex(0x11)
file.print_word(self.num_sounds)
file.end_sprite()
class LoadBinaryFile(base_action.BaseAction):
'''
* FF 00
'''
def __init__(self, fname, pos):
self.fname = fname
self.pos = pos
self.last = False
def prepare_output(self, sprite_num):
if not os.access(self.fname, os.R_OK):
raise generic.ScriptError("Sound file '{}' does not exist.".format(self.fname), self.pos)
size = os.path.getsize(self.fname)
if size == 0:
raise generic.ScriptError("Expected sound file '{}' to have a non-zero length.".format(self.fname), self.pos)
def write(self, file):
file.print_named_filedata(self.fname)
if self.last: file.newline()
class ImportSound(base_action.BaseAction):
"""
Import a sound from another grf::
* FE 00
@ivar grfid: ID of the other grf.
@type grfid: C{int}
@ivar number: Sound number to load.
@type number: C{int}
@ivar pos: Position information
@type pos: L{Position}
"""
def __init__(self, grfid, number, pos):
self.grfid = grfid
self.number = number
self.pos = pos
self.last = False
def write(self, file):
file.start_sprite(8)
file.print_bytex(0xfe)
file.print_bytex(0)
file.print_dwordx(self.grfid)
file.print_wordx(self.number)
file.end_sprite()
if self.last: file.newline()
registered_sounds = {}
SOUND_OFFSET = 73 # No of original sounds
NUM_ANIMATION_SOUNDS = 0x80 - SOUND_OFFSET # Number of custom sound ids, which can be returned by animation callbacks.
def print_stats():
"""
Print statistics about used ids.
"""
if len(registered_sounds) > 0:
# Currently NML does not optimise the order of sound effects. So we assume NUM_ANIMATION_SOUNDS as the maximum.
generic.print_info("Sound effects: {}/{}".format(len(registered_sounds), NUM_ANIMATION_SOUNDS))
def add_sound(args, pos):
if args not in registered_sounds:
registered_sounds[args] = (len(registered_sounds), pos)
return registered_sounds[args][0] + SOUND_OFFSET
def get_sound_actions():
"""
Get a list of actions that actually includes all sounds in the output file.
"""
if not registered_sounds:
return []
action_list = []
action_list.append(Action11(len(registered_sounds)))
volume_list = []
sound_data = [(sound_id, args, pos) for args, (sound_id, pos) in list(registered_sounds.items())]
# Sort on first item, i.e. sound ID
for i, args, pos in sorted(sound_data):
if len(args) == 3:
action_list.append(ImportSound(args[0], args[1], pos))
else:
action_list.append(LoadBinaryFile(args[0], pos))
if args[-1] != 100:
volume_list.append( (i + SOUND_OFFSET, int(args[-1] * 128 / 100)) )
if volume_list:
action_list.extend(action0.get_volume_actions(volume_list))
return action_list
nml-0.4.5/nml/actions/action2real.py 0000644 0005672 0005672 00000012462 13315644406 020466 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from nml.actions import action2, action1
from nml.ast import general
class Action2Real(action2.Action2):
def __init__(self, feature, name, pos, loaded_list, loading_list):
action2.Action2.__init__(self, feature, name, pos)
self.loaded_list = loaded_list
self.loading_list = loading_list
def write(self, file):
size = 2 + 2 * len(self.loaded_list) + 2 * len(self.loading_list)
action2.Action2.write_sprite_start(self, file, size)
file.print_byte(len(self.loaded_list))
file.print_byte(len(self.loading_list))
file.newline()
for i in self.loaded_list:
file.print_word(i)
file.newline()
for i in self.loading_list:
file.print_word(i)
file.newline()
file.end_sprite()
def get_real_action2s(spritegroup, feature):
loaded_list = []
loading_list = []
actions = []
if feature not in action2.features_sprite_group:
raise generic.ScriptError("Sprite groups that combine sprite sets are not supported for feature '{}'.".format(general.feature_name(feature)), spritegroup.pos)
# First make sure that all referenced real sprites are put in a single action1
spriteset_list = []
for view in spritegroup.spriteview_list:
spriteset_list.extend([action2.resolve_spritegroup(sg_ref.name) for sg_ref in view.spriteset_list])
actions.extend(action1.add_to_action1(spriteset_list, feature, spritegroup.pos))
view_names = sorted([view.name.value for view in spritegroup.spriteview_list])
if feature in (0x00, 0x01, 0x02, 0x03):
if view_names != sorted(['loading', 'loaded']):
raise generic.ScriptError("Expected a 'loading' and a 'loaded' (list of) sprite set(s).", spritegroup.pos)
elif feature in (0x05, 0x0B, 0x0D, 0x10):
msg = "Sprite groups for feature {:02X} will not be supported in the future, as they are no longer needed. Directly refer to sprite sets instead."
msg = msg.format(feature)
generic.print_warning(msg, spritegroup.pos)
if view_names != ['default']:
raise generic.ScriptError("Expected only a 'default' (list of) sprite set(s).", spritegroup.pos)
for view in spritegroup.spriteview_list:
if len(view.spriteset_list) == 0:
raise generic.ScriptError("Expected at least one sprite set, encountered 0.", view.pos)
for set_ref in view.spriteset_list:
spriteset = action2.resolve_spritegroup(set_ref.name)
action1_index = action1.get_action1_index(spriteset)
if view.name.value == 'loading': loading_list.append(action1_index)
else: loaded_list.append(action1_index)
actions.append(Action2Real(feature, spritegroup.name.value + " - feature {:02X}".format(feature), spritegroup.pos, loaded_list, loading_list))
spritegroup.set_action2(actions[-1], feature)
return actions
def make_simple_real_action2(feature, name, pos, action1_index):
"""
Make a simple real action2, referring to only 1 action1 sprite set
@param feature: Feature of the needed action2
@type feature: C{int}
@param name: Name of the action2
@type name: C{str}
@param pos: Positional context.
@type pos: L{Position}
@param action1_index: Index of the action1 sprite set to use
@type action1_index: C{int}
@return: The created real action2
@rtype: L{Action2Real}
"""
return Action2Real(feature, name, pos, [action1_index], [action1_index] if feature <= 0x03 else [])
def create_spriteset_actions(spritegroup):
"""
Create action2s for directly-referenced sprite sets
@param spritegroup: Spritegroup to create the sprite sets for
@type spritegroup: L{ASTSpriteGroup}
@return: Resulting list of actions
@rtype: C{list} of L{BaseAction}
"""
action_list = []
# Iterate over features first for more efficient action1s
for feature in spritegroup.feature_set:
if len(spritegroup.used_sprite_sets) != 0 and feature not in action2.features_sprite_group:
raise generic.ScriptError("Directly referring to sprite sets is not possible for feature {:02X}".format(feature), spritegroup.pos)
for spriteset in spritegroup.used_sprite_sets:
if spriteset.has_action2(feature): continue
action_list.extend(action1.add_to_action1([spriteset], feature, spritegroup.pos))
real_action2 = make_simple_real_action2(feature, spriteset.name.value + " - feature {:02X}".format(feature), spritegroup.pos, action1.get_action1_index(spriteset))
action_list.append(real_action2)
spriteset.set_action2(real_action2, feature)
return action_list
nml-0.4.5/nml/actions/action2.py 0000644 0005672 0005672 00000047151 13315644406 017625 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from nml.actions import base_action
from nml.ast import base_statement, general
import nml
total_action2_ids = 0x100
free_action2_ids = list(range(0, total_action2_ids))
"""
Statistics about spritegroups.
The 1st field of type C{int} contains the largest number of concurrently active spritegroup ids.
The 2nd field of type L{Position} contains a positional reference to the last spritegroup of the concurrently active ones.
"""
spritegroup_stats = (0, None)
total_tmp_locations = 0x7F
"""
Statistics about temporary Action2 registers.
The 1st field of type C{int} contains the largest number of concurrently active register ids.
The 2nd field of type L{Position} contains a positional reference to the spritegroup.
"""
a2register_stats = (0, None)
def print_stats():
"""
Print statistics about used ids.
"""
if spritegroup_stats[0] > 0:
generic.print_info("Concurrent spritegroups: {}/{} ({})".format(spritegroup_stats[0], total_action2_ids, str(spritegroup_stats[1])))
if a2register_stats[0] > 0:
generic.print_info("Concurrent Action2 registers: {}/{} ({})".format(a2register_stats[0], total_tmp_locations, str(a2register_stats[1])))
class Action2(base_action.BaseAction):
"""
Abstract Action2 base class.
@ivar name: Name of the action2.
@type name: C{str}
@ivar feature: Action2 feature byte.
@type feature: C{int}
@ivar pos: Position reference to source.
@type pos: L{Position}
@ivar num_refs: Number of references to this action2.
@type num_refs: C{int}
@ivar id: Number of this action2.
@type id: C{int}, or C{None} if no number is allocated yet.
@ivar references: All Action2s that are referenced by this Action2.
@type references: C{list} of L{Action2Reference}
@ivar tmp_locations: List of address in the temporary storage that are free
to be used in this varaction2.
@type tmp_locations: C{list} of C{int}
"""
def __init__(self, feature, name, pos):
self.feature = feature
self.name = name
self.pos = pos
self.num_refs = 0
self.id = None
self.references = []
#0x00 - 0x7F: available to user
#0x80 - 0xFE: used by NML
#0xFF: Used for some house variables
#0x100 - 0x10F: Special meaning (used for some CB results)
self.tmp_locations = list(range(0x80, 0x80 + total_tmp_locations))
def prepare_output(self, sprite_num):
free_references(self)
global spritegroup_stats
try:
if self.num_refs == 0:
self.id = free_action2_ids[0]
else:
self.id = free_action2_ids.pop()
num_used = total_action2_ids - len(free_action2_ids)
if num_used > spritegroup_stats[0]:
spritegroup_stats = (num_used, self.pos)
except IndexError:
raise generic.ScriptError("Unable to allocate ID for [random]switch, sprite set/layout/group or produce-block. Try reducing the number of such blocks.", self.pos)
def write_sprite_start(self, file, size):
assert self.num_refs == 0, "Action2 reference counting has {:d} dangling references.".format(self.num_refs)
file.comment("Name: " + self.name)
file.start_sprite(size + 3)
file.print_bytex(2)
file.print_bytex(self.feature)
file.print_bytex(self.id)
def skip_action7(self):
return False
def skip_action9(self):
return False
def skip_needed(self):
return False
def remove_tmp_location(self, location, force_recursive):
"""
Recursively remove a location from the list of available temporary
storage locations. It is not only removed from the the list of the
current Action2Var but also from all Action2Var it calls. If an
Action2Var is referenced as a procedure call, the location is always
removed recursively, otherwise only if force_recursive is True.
@param location: Number of the storage register to remove.
@type location: C{int}
@param force_recursive: Force removing this location recursively,
also for 'chained' action2s.
@type force_recursive: C{bool}
"""
global a2register_stats
if location in self.tmp_locations:
self.tmp_locations.remove(location)
num_used = total_tmp_locations - len(self.tmp_locations)
if num_used > a2register_stats[0]:
a2register_stats = (num_used, self.pos)
for act2_ref in self.references:
if force_recursive or act2_ref.is_proc:
act2_ref.action2.remove_tmp_location(location, True)
class Action2Reference:
"""
Container class to store information about an action2 reference
@ivar action2: The target action2
@type action2: L{Action2}
@ivar is_proc: Whether this reference is made because of a procedure call
@type is_proc: C{bool}
"""
def __init__(self, action2, is_proc):
self.action2 = action2
self.is_proc = is_proc
def add_ref(ref, source_action, reference_as_proc = False):
"""
Add a reference to a certain action2.
This is needed so we can correctly reserve / free action2 IDs later on.
To be called when creating the actions from the AST.
@param ref: Reference to the sprite group that corresponds to the action2.
@type ref: L{SpriteGroupRef}
@param source_action: Source action (act2 or act3) that contains the reference.
@type source_action: L{Action2} or L{Action3}
@param reference_as_proc: True iff the reference source is a procedure call,
which needs special precautions for temp registers.
@type reference_as_proc: C{bool}
"""
# Add reference to list of references of the source action
act2 = ref.act2 if ref.act2 is not None else resolve_spritegroup(ref.name).get_action2(source_action.feature)
source_action.references.append(Action2Reference(act2, reference_as_proc))
act2.num_refs += 1
def free_references(source_action):
"""
Free all references to other action2s from a certain action 2/3
@param source_action: Action that contains the reference
@type source_action: L{Action2} or L{Action3}
"""
for act2_ref in source_action.references:
act2 = act2_ref.action2
act2.num_refs -= 1
if act2.num_refs == 0: free_action2_ids.append(act2.id)
# Features using sprite groups directly: vehicles, canals, cargos, railtypes, airports
features_sprite_group = [0x00, 0x01, 0x02, 0x03, 0x05, 0x0B, 0x0D, 0x10]
# Features using sprite layouts: stations, houses, industry tiles, objects and airport tiles
features_sprite_layout = [0x04, 0x07, 0x09, 0x0F, 0x11]
# All features that need sprite sets
features_sprite_set = features_sprite_group + features_sprite_layout
def make_sprite_group_class(cls_is_spriteset, cls_is_referenced, cls_has_explicit_feature, cls_is_relocatable = False):
"""
Metaclass factory which makes base classes for all nodes 'Action 2 graph'
This graph is made up of all blocks that are eventually compiled to Action2,
which use the same name space. Spritesets do inherit from this class to make
referencing them possible, but they are not part of the refernce graph that
is built.
@param cls_is_spriteset: Whether this class represents a spriteset
@type cls_is_spriteset: C{bool}
@param cls_is_referenced: True iff this node can be referenced by other nodes
@type cls_is_referenced: C{bool}
@param cls_has_explicit_feature: Whether the feature of an instance is explicitly set,
or derived from nodes that link to it.
@type cls_has_explicit_feature: C{bool}
@param cls_is_relocatable: Whether instances of this class can be freely moved around or whether they need
to to be converted to nfo code at the same location as they are in the nml code.
@type cls_is_relocatable: C{bool}
@return: The constructed class
@rtype: C{type}
"""
#without either references or an explicit feature, we have nothing to base our feature on
assert cls_is_referenced or cls_has_explicit_feature
class ASTSpriteGroup(base_statement.BaseStatement):
"""
Abstract base class for all AST nodes that represent a sprite group
This handles all the relations between the various nodes
Child classes should do the following:
- Implement their own __init__ method
- Call BaseStatement.__init__
- Call initialize, pre_process and perpare_output (in that order)
- Implement collect_references
- Call set_action2 after generating the corresponding action2 (if applicable)
@ivar _referencing_nodes: Set of nodes that refer to this node
@type _referencing_nodes: C{set}
@ivar _referenced_nodes: Set of nodes that this node refers to
@type _referenced_nodes: C{set}
@ivar _prepared: True iff prepare_output has already been executed
@type _prepared: C{bool}
@ivar _action2: Mapping of features to action2s
@type _action2: C{dict} that maps C{int} to L{Action2}
@ivar feature_set: Set of features that use this node
@type feature_set: C{set} of C{int}
@ivar name: Name of this node, as declared by the user
@type name: L{Identifier}
@ivar num_params: Number of parameters that can be (and have to be) passed
@type num_params: C{int}
@ivar used_sprite_sets: List of sprite sets used by this node
@type used_sprite_sets: C{list} of L{SpriteSet}
"""
def __init__(self):
"""
Subclasses should implement their own __init__ method.
This method should not be called, because calling a method on a meta class can be troublesome.
Instead, call initialize(..).
"""
raise NotImplementedError('__init__ must be implemented in ASTSpriteGroup-subclass {!r}, initialize(..) should be called instead'.format(type(self)))
def initialize(self, name = None, feature = None, num_params = 0):
"""
Initialize this instance.
This function is generally, but not necessarily, called from the child class' constructor.
Calling it later (during pre-processing) is also possible, as long as it's called
before any other actions are done.
@param name: Name of this node, as set by the user (if applicable)
Should be be set (not None) iff cls_is_referenced is True
@type name: L{Identifier} or C{None} if N/A
@param feature: Feature of this node, if set by the user.
Should be set (not None) iff cls_has_explicit_feature is True
@type feature: C{int} or C{None}
"""
assert not (self._has_explicit_feature() and feature is None)
assert not (cls_is_referenced and name is None)
self._referencing_nodes = set()
self._referenced_nodes = set()
self._prepared = False
self._action2 = {}
self.feature_set = set([feature]) if feature is not None else set()
self.name = name
self.num_params = num_params
self.used_sprite_sets = []
def register_names(self):
if cls_is_relocatable and cls_is_referenced:
register_spritegroup(self)
def pre_process(self):
"""
Pre-process this node.
During this stage, the reference graph is built.
"""
refs = self.collect_references()
for ref in refs:
self._add_reference(ref)
if (not cls_is_relocatable) and cls_is_referenced:
register_spritegroup(self)
def prepare_act2_output(self):
"""
Prepare this node for outputting.
This sets the feature and makes sure it is correct.
@return: True iff parsing of this node is needed
@rtype: C{bool}
"""
if not cls_is_referenced: return True
if not self._prepared:
self._prepared = True
# copy, since we're going to modify
ref_nodes = self._referencing_nodes.copy()
for node in ref_nodes:
used = node.prepare_act2_output()
if not used:
node._remove_reference(self)
# now determine the feature
if self._has_explicit_feature():
# by this time, feature should be set
assert len(self.feature_set) == 1
for n in self._referencing_nodes:
if n.feature_set != self.feature_set:
msg = "Cannot refer to block '{}' with feature '{}', expected feature is '{}'"
msg = msg.format(self.name.value,
general.feature_name(next(iter(self.feature_set))),
general.feature_name(n.feature_set.difference(self.feature_set).pop()))
raise generic.ScriptError(msg, n.pos)
elif len(self._referencing_nodes) != 0:
for n in self._referencing_nodes:
# Add the features from all calling blocks to the set
self.feature_set.update(n.feature_set)
if len(self._referencing_nodes) == 0:
# if we can be 'not used', there ought to be a way to refer to this block
assert self.name is not None
generic.print_warning("Block '{}' is not referenced, ignoring.".format(self.name.value), self.pos)
return len(self._referencing_nodes) != 0
def referenced_nodes(self):
"""
Get the nodes that this node refers to.
@note: Make sure to sort this in a deterministic way when the order of items affects the output.
@return: A set of nodes
@rtype: C{set} of L{ASTSpriteGroup}
"""
return self._referenced_nodes
def referencing_nodes(self):
"""
Get the nodes that refer to this node.
@note: Make sure to sort this in a deterministic way when the order of items affects the output.
@return: A set of nodes
@rtype: C{set} of L{ASTSpriteGroup}
"""
return self._referencing_nodes
def collect_references(self):
"""
This function should collect all references to other nodes from this instance.
@return: A collection containing all links to other nodes.
@rtype: C{iterable} of L{SpriteGroupRef}
"""
raise NotImplementedError('collect_references must be implemented in ASTSpriteGroup-subclass {!r}'.format(type(self)))
def set_action2(self, action2, feature):
"""
Set this node's resulting action2
@param feature: Feature of the Action2
@type feature: C{int}
@param action2: Action2 to set
@type action2: L{Action2}
"""
assert feature not in self._action2
self._action2[feature] = action2
def get_action2(self, feature):
"""
Get this node's resulting action2
@param feature: Feature of the Action2
@type feature: C{int}
@return: Action2 to get
@rtype: L{Action2}
"""
assert feature in self._action2
return self._action2[feature]
def has_action2(self, feature):
"""
Check, if this node already has an action2 for a given feature
@param feature: Feature to check
@type feature: C{int}
@return: True iff there is an action2 for this feature
@rtype: C{bool}
"""
return feature in self._action2
def _add_reference(self, target_ref):
"""
Add a reference from C{self} to a target with a given name.
@param target_ref: Name of the reference target
@type target_ref: L{SpriteGroupRef}
"""
if target_ref.name.value == "CB_FAILED": return
target = resolve_spritegroup(target_ref.name)
if target.is_spriteset():
assert target.num_params == 0
# Referencing a spriteset directly from graphics/[random]switch
# Passing parameters is not possible here
if len(target_ref.param_list) != 0:
raise generic.ScriptError("Passing parameters to '{}' is only possible from a spritelayout.".format(target_ref.name.value), target_ref.pos)
self.used_sprite_sets.append(target)
else:
if len(target_ref.param_list) != target.num_params:
msg = "'{}' expects {:d} parameters, encountered {:d}."
msg = msg.format(target_ref.name.value, target.num_params, len(target_ref.param_list))
raise generic.ScriptError(msg, target_ref.pos)
self._referenced_nodes.add(target)
target._referencing_nodes.add(self)
def _remove_reference(self, target):
"""
Add a reference from C{self} to a target
@param target: Existing reference target to be removed
@type target: L{ASTSpriteGroup}
"""
assert target in self._referenced_nodes
assert self in target._referencing_nodes
self._referenced_nodes.remove(target)
target._referencing_nodes.remove(self)
#Make metaclass arguments available outside of the class
def is_spriteset(self):
return cls_is_spriteset
def _has_explicit_feature(self):
return cls_has_explicit_feature
return ASTSpriteGroup
#list of all registered sprite sets and sprite groups
spritegroup_list = {}
def register_spritegroup(spritegroup):
"""
Register a sprite group, so it can be resolved by name later
@param spritegroup: Sprite group to register
@type spritegroup: L{ASTSpriteGroup}
"""
name = spritegroup.name.value
if name in spritegroup_list:
raise generic.ScriptError("Block with name '{}' has already been defined".format(name), spritegroup.pos)
spritegroup_list[name] = spritegroup
nml.global_constants.spritegroups[name] = name
def resolve_spritegroup(name):
"""
Resolve a sprite group with a given name
@param name: Name of the sprite group.
@type name: L{Identifier}
@return: The sprite group that the name refers to.
@rtype: L{ASTSpriteGroup}
"""
if name.value not in spritegroup_list:
raise generic.ScriptError("Unknown identifier encountered: '{}'".format(name.value), name.pos)
return spritegroup_list[name.value]
nml-0.4.5/nml/actions/sprite_count.py 0000644 0005672 0005672 00000001727 13315644406 021003 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
class SpriteCountAction(object):
def __init__(self, count):
self.count = count
def prepare_output(self, sprite_num):
assert sprite_num == 0
def write(self, file):
file.start_sprite(4)
file.print_dword(self.count)
file.newline()
file.end_sprite()
nml-0.4.5/nml/actions/action10.py 0000644 0005672 0005672 00000002073 13315644406 017676 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions import base_action
class Action10(base_action.BaseAction):
def __init__(self, label):
self.label = label
def write(self, file):
file.start_sprite(2)
file.print_bytex(0x10)
file.print_bytex(self.label)
file.newline()
file.end_sprite()
def skip_action9(self):
return False
def skip_needed(self):
return False
nml-0.4.5/nml/actions/action12.py 0000644 0005672 0005672 00000006051 13315644406 017700 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, expression
from nml.actions import base_action, real_sprite
class Action12(base_action.BaseAction):
#sets: list of (font_size, num_char, base_char) tuples
def __init__(self, sets):
self.sets = sets
def write(self, file):
# * 12 ( ){n}
size = 2 + 4 * len(self.sets)
file.start_sprite(size)
file.print_bytex(0x12)
file.print_byte(len(self.sets))
file.newline()
for font_size, num_char, base_char in self.sets:
font_size.write(file, 1)
file.print_byte(num_char)
file.print_word(base_char)
file.newline()
file.end_sprite()
font_sizes = {
'NORMAL' : 0,
'SMALL' : 1,
'LARGE' : 2,
'MONO' : 3,
}
def parse_action12(font_glyphs):
try:
font_size = font_glyphs.font_size.reduce_constant([font_sizes])
if isinstance(font_glyphs.base_char, expression.StringLiteral) and len(font_glyphs.base_char.value) == 1:
base_char = ord(font_glyphs.base_char.value)
else:
base_char = font_glyphs.base_char.reduce_constant()
except generic.ConstError:
raise generic.ScriptError("Parameters of font_glyph have to be compile-time constants", font_glyphs.pos)
if font_size.value not in list(font_sizes.values()):
raise generic.ScriptError("Invalid value for parameter 'font_size' in font_glyph, valid values are 0, 1, 2", font_size.pos)
if not (0 <= base_char.value <= 0xFFFF):
raise generic.ScriptError("Invalid value for parameter 'base_char' in font_glyph, valid values are 0-0xFFFF", base_char.pos)
real_sprite_list = real_sprite.parse_sprite_data(font_glyphs)
char = base_char.value
last_char = char + len(real_sprite_list)
if last_char > 0xFFFF:
raise generic.ScriptError("Character numbers in font_glyph block exceed the allowed range (0-0xFFFF)", font_glyphs.pos)
sets = []
while char < last_char:
#each set of characters must fit in a single 128-char block, according to specs / TTDP
if (char // 128) * 128 != (last_char // 128) * 128:
num_in_set = (char // 128 + 1) * 128 - char
else:
num_in_set = last_char - char
sets.append((font_size, num_in_set, char))
char += num_in_set
return [Action12(sets)] + real_sprite_list
nml-0.4.5/nml/actions/action5.py 0000644 0005672 0005672 00000012771 13315644406 017630 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from nml.actions import base_action, real_sprite
class Action5(base_action.BaseAction):
def __init__(self, type, num_sprites, offset):
self.type = type
self.num_sprites = num_sprites
self.offset = offset
def prepare_output(self, sprite_num):
if self.offset is not None:
self.type |= 0x80
def write(self, file):
# * 05 []
size = 5 if self.offset is None else 8
file.start_sprite(size)
file.print_bytex(0x05)
file.print_bytex(self.type)
file.print_bytex(0xFF)
file.print_word(self.num_sprites)
if self.offset is not None:
file.print_bytex(0xFF)
file.print_word(self.offset)
file.newline()
file.end_sprite()
def skip_action7(self):
#skipping with Action7 should work, according to the Action7/9 specs
#However, skipping invalid (OpenTTD-only) Action5s in TTDP can only be done using Action9, else an error occurs
#To be on the safe side, don't allow skipping with Action7 at all
return False
class Action5BlockType(object):
FIXED = 0, #fixed number of sprites
ANY = 1, #any number of sprites
OFFSET = 2, #flexible number of sprites, offset may be set
action5_table = {
'PRE_SIGNAL' : (0x04, 48, Action5BlockType.FIXED),
'PRE_SIGNAL_SEMAPHORE' : (0x04, 112, Action5BlockType.FIXED),
'PRE_SIGNAL_SEMAPHORE_PBS' : (0x04, 240, Action5BlockType.OFFSET),
'CATENARY' : (0x05, 48, Action5BlockType.OFFSET),
'FOUNDATIONS_SLOPES' : (0x06, 74, Action5BlockType.FIXED),
'FOUNDATIONS_SLOPES_HALFTILES' : (0x06, 90, Action5BlockType.OFFSET),
'TTDP_GUI_25' : (0x07, 73, Action5BlockType.FIXED),
'TTDP_GUI' : (0x07, 93, Action5BlockType.FIXED),
'CANALS' : (0x08, 65, Action5BlockType.OFFSET),
'ONE_WAY_ROAD' : (0x09, 6, Action5BlockType.OFFSET),
'COLOURMAP_2CC' : (0x0A, 256, Action5BlockType.OFFSET),
'TRAMWAY' : (0x0B, 113, Action5BlockType.OFFSET),
'SNOWY_TEMPERATE_TREES' : (0x0C, 133, Action5BlockType.FIXED),
'COAST_TILES' : (0x0D, 16, Action5BlockType.FIXED),
'COAST_TILES_BASEGFX' : (0x0D, 10, Action5BlockType.FIXED),
'COAST_TILES_DIAGONAL' : (0x0D, 18, Action5BlockType.FIXED),
'NEW_SIGNALS' : (0x0E, 0, Action5BlockType.ANY),
'SLOPED_RAILS' : (0x0F, 12, Action5BlockType.OFFSET),
'AIRPORTS' : (0x10, 15, Action5BlockType.OFFSET),
'ROAD_STOPS' : (0x11, 8, Action5BlockType.OFFSET),
'AQUEDUCTS' : (0x12, 8, Action5BlockType.OFFSET),
'AUTORAIL' : (0x13, 55, Action5BlockType.OFFSET),
'FLAGS' : (0x14, 36, Action5BlockType.OFFSET),
'OTTD_GUI' : (0x15, 175, Action5BlockType.OFFSET),
'AIRPORT_PREVIEW' : (0x16, 9, Action5BlockType.OFFSET),
'RAILTYPE_TUNNELS': (0x17, 16, Action5BlockType.OFFSET),
'OTTD_RECOLOUR' : (0x18, 1, Action5BlockType.OFFSET),
}
def parse_action5(replaces):
real_sprite_list = real_sprite.parse_sprite_data(replaces)
num_sprites = len(real_sprite_list)
if replaces.type.value not in action5_table:
raise generic.ScriptError(replaces.type.value + " is not a valid sprite replacement type", replaces.type.pos)
type_id, num_required, block_type = action5_table[replaces.type.value]
offset = None
if block_type == Action5BlockType.FIXED:
if num_sprites < num_required:
msg = "Invalid sprite count for sprite replacement type '{}', expected {:d}, got {:d}"
msg = msg.format(replaces.type, num_required, num_sprites)
raise generic.ScriptError(msg, replaces.pos)
elif num_sprites > num_required:
msg = "Too many sprites specified for sprite replacement type '{}', expected {:d}, got {:d}, extra sprites may be ignored"
msg = msg.format(replaces.type, num_required, num_sprites)
generic.print_warning(msg, replaces.pos)
if replaces.offset != 0:
msg = "replacenew parameter 'offset' must be zero for sprite replacement type '{}'".format(replaces.type)
raise generic.ScriptError(msg, replaces.pos)
elif block_type == Action5BlockType.ANY:
if replaces.offset != 0:
msg = "replacenew parameter 'offset' must be zero for sprite replacement type '{}'".format(replaces.type)
raise generic.ScriptError(msg, replaces.pos)
elif block_type == Action5BlockType.OFFSET:
if num_sprites + replaces.offset > num_required:
msg = "Exceeding the limit of {:d} sprites for sprite replacement type '{}', extra sprites may be ignored"
msg = msg.format(num_required, replaces.type)
generic.print_warning(msg, replaces.pos)
if replaces.offset != 0 or num_sprites != num_required:
offset = replaces.offset
else:
assert 0
return [Action5(type_id, num_sprites, offset)] + real_sprite_list
nml-0.4.5/nml/actions/base_action.py 0000644 0005672 0005672 00000003652 13315644406 020533 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
class BaseAction(object):
def prepare_output(self, sprite_num):
"""
Called just before L{write}, this function can be used to do some
last modifications to the action (like resolving some IDs that can't
be resolved earlier).
@param sprite_num: The sprite number in the nfo/grf output.
@type sprite_num: C{int}
"""
pass
def write(self, file):
"""
Write this action to the given outputfile.
@param file: The outputfile to write the data to.
@type file: L{SpriteOutputBase}
"""
raise NotImplementedError('write is not implemented in {!r}'.format(type(self)))
def skip_action7(self):
"""
Can this action be skipped safely by an action7?
@return True iff this action can be skipped by action7.
"""
return True
def skip_action9(self):
"""
Can this action be skipped safely by an action9?
@return True iff this action can be skipped by action9.
"""
return True
def skip_needed(self):
"""
Do we need to skip this action at all?
@return False when it doesn't matter whether this action is skipped
or not.
"""
return True
nml-0.4.5/nml/actions/real_sprite.py 0000644 0005672 0005672 00000061066 13315644406 020600 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, expression
from nml.actions import base_action
from nml.ast import assignment
try:
from PIL import Image
except ImportError:
import Image
FLAG_NOCROP = 0x0040
FLAG_NOALPHA = 0x0100
FLAG_WHITE = 0x0200
FLAG_ANIM = 0x0400
real_sprite_flags = {
'CROP' : 0, # Allow cropping
'NOCROP' : FLAG_NOCROP, # Disallow cropping
'ALPHA' : 0, # Allow semi-transparency
'NOALPHA' : FLAG_NOALPHA, # Warn about semi-transparency
'WHITE' : FLAG_WHITE, # Allow pure-white
'NOWHITE' : 0, # Warn about pure-white
'ANIM' : FLAG_ANIM, # Allow anim colours
'NOANIM' : 0, # Warn about anim colours
}
palmap_d2w = [
0, 215, 216, 136, 88, 106, 32, 33, # 0..7
40, 245, 10, 11, 12, 13, 14, 15, # 8..15
16, 17, 18, 19, 20, 21, 22, 23, # 16..23
24, 25, 26, 27, 28, 29, 30, 31, # 24..31
53, 54, 34, 35, 36, 37, 38, 39, # 32..39
178, 41, 42, 43, 44, 45, 46, 47, # 40..47
48, 49, 50, 51, 52, 53, 54, 55, # 48..55
56, 57, 58, 59, 60, 61, 62, 63, # 56..63
64, 65, 66, 67, 68, 69, 70, 71, # 64..71
72, 73, 74, 75, 76, 77, 78, 79, # 72..79
80, 81, 82, 83, 84, 85, 86, 87, # 80..87
96, 89, 90, 91, 92, 93, 94, 95, # 88..95
96, 97, 98, 99, 100, 101, 102, 103, # 96..103
104, 105, 53, 107, 108, 109, 110, 111, # 104..111
112, 113, 114, 115, 116, 117, 118, 119, # 112..119
120, 121, 122, 123, 124, 125, 126, 127, # 120..127
128, 129, 130, 131, 132, 133, 134, 135, # 128..135
170, 137, 138, 139, 140, 141, 142, 143, # 136..143
144, 145, 146, 147, 148, 149, 150, 151, # 144..151
152, 153, 154, 155, 156, 157, 158, 159, # 152..159
160, 161, 162, 163, 164, 165, 166, 167, # 160..167
168, 169, 170, 171, 172, 173, 174, 175, # 168..175
176, 177, 178, 179, 180, 181, 182, 183, # 176..183
184, 185, 186, 187, 188, 189, 190, 191, # 184..191
192, 193, 194, 195, 196, 197, 198, 199, # 192..199
200, 201, 202, 203, 204, 205, 206, 207, # 200..207
208, 209, 210, 211, 212, 213, 214, 215, # 208..215
216, 217, 246, 247, 248, 249, 250, 251, # 216..223
252, 253, 254, 227, 228, 229, 230, 231, # 224..231
232, 233, 234, 235, 236, 237, 238, 239, # 232..239
240, 241, 242, 243, 244, 217, 218, 219, # 240..247
220, 221, 222, 223, 224, 225, 226, 255, # 248..255
]
palmap_w2d = [
0, 1, 2, 3, 4, 5, 6, 7, # 0..7
8, 9, 10, 11, 12, 13, 14, 15, # 8..15
16, 17, 18, 19, 20, 21, 22, 23, # 16..23
24, 25, 26, 27, 28, 29, 30, 31, # 24..31
6, 7, 34, 35, 36, 37, 38, 39, # 32..39
8, 41, 42, 43, 44, 45, 46, 47, # 40..47
48, 49, 50, 51, 52, 53, 54, 55, # 48..55
56, 57, 58, 59, 60, 61, 62, 63, # 56..63
64, 65, 66, 67, 68, 69, 70, 71, # 64..71
72, 73, 74, 75, 76, 77, 78, 79, # 72..79
80, 81, 82, 83, 84, 85, 86, 87, # 80..87
4, 89, 90, 91, 92, 93, 94, 95, # 88..95
96, 97, 98, 99, 100, 101, 102, 103, # 96..103
104, 105, 5, 107, 108, 109, 110, 111, # 104..111
112, 113, 114, 115, 116, 117, 118, 119, # 112..119
120, 121, 122, 123, 124, 125, 126, 127, # 120..127
128, 129, 130, 131, 132, 133, 134, 135, # 128..135
3, 137, 138, 139, 140, 141, 142, 143, # 136..143
144, 145, 146, 147, 148, 149, 150, 151, # 144..151
152, 153, 154, 155, 156, 157, 158, 159, # 152..159
160, 161, 162, 163, 164, 165, 166, 167, # 160..167
168, 169, 170, 171, 172, 173, 174, 175, # 168..175
176, 177, 178, 179, 180, 181, 182, 183, # 176..183
184, 185, 186, 187, 188, 189, 190, 191, # 184..191
192, 193, 194, 195, 196, 197, 198, 199, # 192..199
200, 201, 202, 203, 204, 205, 206, 207, # 200..207
208, 209, 210, 211, 212, 213, 214, 1, # 208..215
2, 245, 246, 247, 248, 249, 250, 251, # 216..223
252, 253, 254, 229, 230, 231, 227, 228, # 224..231
232, 233, 234, 235, 236, 237, 238, 239, # 232..239
240, 241, 242, 243, 244, 9, 218, 219, # 240..247
220, 221, 222, 223, 224, 225, 226, 255, # 248..255
]
translate_w2d = bytearray(v for v in palmap_w2d)
translate_d2w = bytearray(v for v in palmap_d2w)
def convert_palette(pal):
ret = 256 * [0]
for idx, colour in enumerate(pal):
if 0xD7 <= idx <= 0xE2:
if idx != colour:
raise generic.ScriptError("Indices 0xD7..0xE2 are not allowed in recolour sprites when the output is in the WIN palette")
continue
ret[palmap_d2w[idx]] = palmap_d2w[colour]
return ret
class RealSprite(object):
"""
@ivar param_list: Original parameters from NML source file.
@type param_list: List of L{expression.Expression}, or C{None}
@ivar label: Optional label from NML source file.
@type label: L{expression.Identifier} or C{None}
@ivar is_empty: True, if this is an empty sprite with zero dimension.
@type is_empty: C{bool}
@ivar file: Filename of primary image file.
@type file: L{expression.StringLiteral}
@ivar xpos: X position of sprite in source image file.
@type xpos: L{expression.ConstantNumeric} or C{None}
@ivar ypos: Y position of sprite in source image file.
@type ypos: L{expression.ConstantNumeric} or C{None}
@ivar mask_file: Filename of additional mask image file.
@type mask_file: L{expression.StringLiteral} or C{None}
@ivar mask_pos: Position of sprite in source mask image file.
@type mask_pos: Pair of L{expression.ConstantNumeric}, or C{None}
@ivar xsize: X size of sprite in both source image and mask file.
@type xsize: L{expression.ConstantNumeric} or C{None}
@ivar ysize: Y position of sprite in both source image and mask file.
@type ysize: L{expression.ConstantNumeric} or C{None}
@ivar xrel: X sprite offset.
@type xrel: L{expression.ConstantNumeric}
@ivar yrel: Y sprite offset.
@type yrel: L{expression.ConstantNumeric}
@ivar flags: Cropping/warning flags.
@type flags: L{expression.ConstantNumeric}
@ivar poslist: Position of creation of the sprite, if available.
@type poslist: C{list} of L{Position}
"""
def __init__(self, param_list = None, label = None, poslist = None):
self.param_list = param_list
self.label = label
self.is_empty = False
self.xpos = None
self.ypos = None
self.xsize = None
self.ysize = None
if poslist is None:
self.poslist = []
else:
self.poslist = poslist
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Real sprite, parameters:')
for param in self.param_list:
param.debug_print(indentation + 2)
def get_labels(self):
labels = {}
if self.label is not None:
labels[self.label.value] = 0
return labels, 1
def expand(self, default_file, default_mask_file, poslist, id_dict):
return [parse_real_sprite(self, default_file, default_mask_file, poslist, id_dict)]
def check_sprite_size(self):
generic.check_range(self.xpos.value, 0, 0x7fffFFFF, "Real sprite paramater 'xpos'", self.xpos.pos)
generic.check_range(self.ypos.value, 0, 0x7fffFFFF, "Real sprite paramater 'ypos'", self.ypos.pos)
generic.check_range(self.xsize.value, 1, 0xFFFF, "Real sprite paramater 'xsize'", self.xsize.pos)
generic.check_range(self.ysize.value, 1, 0xFFFF, "Real sprite paramater 'ysize'", self.ysize.pos)
def validate_size(self):
"""
Check if xpos/ypos/xsize/ysize are already set and if not, set them
to 0,0,image_width,image_height.
"""
if self.xpos is None:
im = Image.open(generic.find_file(self.file.value))
self.xpos = expression.ConstantNumeric(0)
self.ypos = expression.ConstantNumeric(0)
self.xsize = expression.ConstantNumeric(im.size[0])
self.ysize = expression.ConstantNumeric(im.size[1])
self.check_sprite_size()
if self.mask_pos is None:
self.mask_pos = (self.xpos, self.ypos)
def __str__(self):
ret = ""
if self.label is not None:
ret += str(self.label) + ": "
ret += "["
ret += ", ".join([str(param) for param in self.param_list])
ret += "]"
return ret
def get_cache_key(self, crop_sprites):
"""
Assemble the sprite meta data into a key, able to identify the sprite.
@param crop_sprites: Whether to crop sprites, which allow it.
@type crop_sprites: C{bool}
@return: Key
@rtype: C{tuple}
"""
filename_8bpp = None
filename_32bpp = None
if self.bit_depth == 8:
filename_8bpp = self.file
else:
filename_32bpp = self.file
filename_8bpp = self.mask_file
x = self.xpos.value
y = self.ypos.value
size_x = self.xsize.value
size_y = self.ysize.value
if self.bit_depth == 8 or self.mask_pos is None:
mask_x, mask_y = x, y
else:
mask_x = self.mask_pos[0].value
mask_y = self.mask_pos[1].value
rgb_file, rgb_rect = (filename_32bpp.value, (x, y, size_x, size_y)) if filename_32bpp is not None else (None, None)
mask_file, mask_rect = (filename_8bpp.value, (mask_x, mask_y, size_x, size_y)) if filename_8bpp is not None else (None, None)
do_crop = crop_sprites and ((self.flags.value & FLAG_NOCROP) == 0)
return (rgb_file, rgb_rect, mask_file, mask_rect, do_crop)
class SpriteAction(base_action.BaseAction):
"""
@ivar sprite_num: Number of the sprite, or C{None} if not decided yet.
@type sprite_num: C{int} or C{None}
@ivar last: Whether this sprite action is the last of a series.
@type last: C{bool}
"""
def __init__(self):
self.sprite_num = None
self.last = False
def prepare_output(self, sprite_num):
if self.sprite_num is not None and self.sprite_num.value != sprite_num:
msg = "Sprite number {:d} given in base_graphics-block, but it doesn't match output sprite number {:d}"
msg = msg.format(self.sprite_num.value, sprite_num)
raise generic.ScriptError(msg)
class RealSpriteAction(SpriteAction):
def __init__(self):
SpriteAction.__init__(self)
self.sprite_list = []
def add_sprite(self, sprite):
self.sprite_list.append(sprite)
def write(self, file):
if len(self.sprite_list) == 0 or self.sprite_list[0].is_empty:
file.print_empty_realsprite()
else:
file.print_sprite([s for s in self.sprite_list if not s.is_empty])
if self.last: file.newline()
class RecolourSprite(object):
def __init__(self, mapping, label = None, poslist = None):
self.mapping = mapping
self.label = label
if poslist is None:
self.poslist = []
else:
self.poslist = poslist
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Recolour sprite, mapping:')
for assignment in self.mapping:
generic.print_dbg(indentation + 2, '{}: {};'.format(assignment.name, assignment.value))
def get_labels(self):
labels = {}
if self.label is not None:
labels[self.label.value] = 0
return labels, 1
def expand(self, default_file, default_mask_file, poslist, id_dict):
# create new struct, needed for template expansion
new_mapping = []
for old_assignment in self.mapping:
from_min_value = old_assignment.name.min.reduce_constant([id_dict])
from_max_value = from_min_value if old_assignment.name.max is None else old_assignment.name.max.reduce_constant([id_dict])
to_min_value = old_assignment.value.min.reduce_constant([id_dict])
to_max_value = None if old_assignment.value.max is None else old_assignment.value.max.reduce_constant([id_dict])
new_mapping.append(assignment.Assignment(assignment.Range(from_min_value, from_max_value), assignment.Range(to_min_value, to_max_value), old_assignment.pos))
return [RecolourSprite(new_mapping, poslist = poslist)]
def __str__(self):
ret = "" if self.label is None else str(self.label) + ": "
ret += "recolour_sprite {\n"
for assignment in self.mapping:
ret += '{}: {};'.format(assignment.name, assignment.value)
ret += "}"
return ret
class RecolourSpriteAction(SpriteAction):
def __init__(self, sprite):
SpriteAction.__init__(self)
self.sprite = sprite
self.output_table = []
def prepare_output(self, sprite_num):
SpriteAction.prepare_output(self, sprite_num)
colour_mapping = {}
for assignment in self.sprite.mapping:
if assignment.value.max is not None and assignment.name.max.value - assignment.name.min.value != assignment.value.max.value - assignment.value.min.value:
raise generic.ScriptError("From and to ranges in a recolour block need to have the same size", assignment.pos)
for i in range(assignment.name.max.value - assignment.name.min.value + 1):
idx = assignment.name.min.value + i
val = assignment.value.min.value
if assignment.value.max is not None:
val += i
colour_mapping[idx] = val
for i in range(256):
if i in colour_mapping:
colour = colour_mapping[i]
else:
colour = i
self.output_table.append(colour)
def write(self, file):
file.start_sprite(257)
file.print_bytex(0)
if file.palette not in ("DEFAULT", "LEGACY"):
raise generic.ScriptError("Recolour sprites are only supported when writing to the DEFAULT (DOS) or LEGACY (WIN) palette. If you don't have any real sprites use the commandline option -p to set a palette.")
colour_table = self.output_table if file.palette == "DEFAULT" else convert_palette(self.output_table)
for idx, colour in enumerate(colour_table):
if idx % 16 == 0:
file.newline()
file.print_bytex(colour)
if self.last: file.newline()
file.end_sprite()
class TemplateUsage(object):
def __init__(self, name, param_list, label, pos):
self.name = name
self.param_list = param_list
self.label = label
self.pos = pos
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Template used:', self.name.value)
generic.print_dbg(indentation + 2, 'Parameters:')
for param in self.param_list:
param.debug_print(indentation + 4)
def get_labels(self):
# Load labels from the template definition
if self.name.value not in sprite_template_map:
raise generic.ScriptError("Encountered unknown template identifier: " + self.name.value, self.name.pos)
labels, offset = sprite_template_map[self.name.value].get_labels()
# Add (possibly) label applied to ourselves
if self.label is not None:
if self.label.value in labels:
raise generic.ScriptError("Duplicate label encountered; '{}' already exists.".format(self.label.value), self.pos)
labels[self.label.value] = 0
return labels, offset
def expand(self, default_file, default_mask_file, poslist, parameters):
if self.name.value not in sprite_template_map:
raise generic.ScriptError("Encountered unknown template identifier: " + self.name.value, self.name.pos)
template = sprite_template_map[self.name.value]
if len(self.param_list) != len(template.param_list):
raise generic.ScriptError("Incorrect number of template arguments. Expected " + str(len(template.param_list)) + ", got " + str(len(self.param_list)), self.pos)
param_dict = {}
for i, param in enumerate(self.param_list):
param = param.reduce([real_sprite_flags, parameters])
if not isinstance(param, (expression.ConstantNumeric, expression.StringLiteral)):
raise generic.ScriptError("Template parameters should be compile-time constants", param.pos)
param_dict[template.param_list[i].value] = param.value
return parse_sprite_list(template.sprite_list, default_file, default_mask_file, poslist + [self.pos], param_dict)
def __str__(self):
return "{}({})".format(self.name, ", ".join(str(param) for param in self.param_list))
def parse_real_sprite(sprite, default_file, default_mask_file, poslist, id_dict):
# check the number of parameters
num_param = len(sprite.param_list)
if num_param == 0:
sprite.is_empty = True
return sprite
elif not (2 <= num_param <= 9):
raise generic.ScriptError("Invalid number of arguments for real sprite. Expected 2..9.", sprite.param_list[0].pos)
# create new sprite struct, needed for template expansion
new_sprite = RealSprite(poslist = poslist + sprite.poslist)
param_offset = 0
if num_param >= 6:
# xpos, ypos, xsize and ysize are all optional. If not specified they'll default
# to 0, 0, image_width, image_height
new_sprite.xpos = sprite.param_list[0].reduce_constant([id_dict])
new_sprite.ypos = sprite.param_list[1].reduce_constant([id_dict])
new_sprite.xsize = sprite.param_list[2].reduce_constant([id_dict])
new_sprite.ysize = sprite.param_list[3].reduce_constant([id_dict])
new_sprite.check_sprite_size()
param_offset += 4
new_sprite.xrel = sprite.param_list[param_offset].reduce_constant([id_dict])
new_sprite.yrel = sprite.param_list[param_offset + 1].reduce_constant([id_dict])
generic.check_range(new_sprite.xrel.value, -0x8000, 0x7fff, "Real sprite paramater {:d} 'xrel'".format(param_offset + 1), new_sprite.xrel.pos)
generic.check_range(new_sprite.yrel.value, -0x8000, 0x7fff, "Real sprite paramater {:d} 'yrel'".format(param_offset + 2), new_sprite.yrel.pos)
param_offset += 2
# Next may follow any combination of (flags, filename, mask), but always in that order
new_sprite.flags = expression.ConstantNumeric(0)
if num_param > param_offset:
try:
new_sprite.flags = sprite.param_list[param_offset].reduce_constant([real_sprite_flags, id_dict])
param_offset += 1
except generic.ConstError:
pass
new_sprite.file = default_file
if num_param > param_offset and not isinstance(sprite.param_list[param_offset], expression.Array):
new_sprite.file = sprite.param_list[param_offset].reduce([id_dict])
param_offset += 1
if not isinstance(new_sprite.file, expression.StringLiteral):
raise generic.ScriptError("Real sprite parameter {:d} 'file' should be a string literal".format(param_offset + 1), new_sprite.file.pos)
if new_sprite.file is None:
raise generic.ScriptError("No image file specified for real sprite", sprite.param_list[0].pos)
new_sprite.mask_file = default_mask_file
new_sprite.mask_pos = None
if num_param > param_offset:
mask = sprite.param_list[param_offset]
param_offset += 1
# Mask may be either string (file only) or array (empty => no mask, 1 value => file only, 2 => offsets only, 3 => file + offsets)
if isinstance(mask, expression.Array):
if not (0 <= len(mask.values) <= 3):
raise generic.ScriptError("Real sprite mask should be an array with 0 to 3 values, encountered {:d}".format(len(mask.values)), mask.pos)
if len(mask.values) == 0:
# disable any default mask
new_sprite.mask_file = None
else:
if len(mask.values) & 1:
new_sprite.mask_file = mask.values[0].reduce([id_dict])
if not isinstance(new_sprite.mask_file, expression.StringLiteral):
raise generic.ScriptError("Real sprite parameter 'mask_file' should be a string literal", new_sprite.file.pos)
if len(mask.values) & 2:
new_sprite.mask_pos = tuple(mask.values[i].reduce_constant([id_dict]) for i in range(-2, 0))
# Check that there is also a mask specified, else the offsets make no sense
if new_sprite.mask_file is None:
raise generic.ScriptError("Mask offsets are specified, but there is no mask file set.", new_sprite.mask_pos[0].pos)
else:
new_sprite.mask_file = mask.reduce([id_dict])
if not isinstance(new_sprite.mask_file, expression.StringLiteral):
raise generic.ScriptError("Real sprite parameter {:d} 'mask' should be an array or string literal".format(param_offset + 1), new_sprite.file.pos)
if num_param > param_offset:
raise generic.ScriptError("Real sprite has too many parameters, the last {:d} parameter(s) cannot be parsed.".format(num_param - param_offset), sprite.param_list[param_offset].pos)
return new_sprite
sprite_template_map = {}
def parse_sprite_list(sprite_list, default_file, default_mask_file, poslist, parameters = {}):
real_sprite_list = []
for sprite in sprite_list:
real_sprite_list.extend(sprite.expand(default_file, default_mask_file, poslist, parameters))
return real_sprite_list
def parse_sprite_data(sprite_container):
"""
@param sprite_container: AST node that contains the sprite data
@type sprite_container: L{SpriteContainer}
@return: List of real sprite actions
@rtype: C{list} of L{BaseAction}
"""
all_sprite_data = sprite_container.get_all_sprite_data()
action_list = []
first = True
for sprite_data in all_sprite_data:
sprite_list, default_file, default_mask_file, pos, zoom_level, bit_depth = sprite_data
new_sprite_list = parse_sprite_list(sprite_list, default_file, default_mask_file, [pos])
if not first and len(new_sprite_list) != len(action_list):
msg = "Expected {:d} alternative sprites for {} '{}', got {:d}."
msg = msg.format(len(action_list), sprite_container.block_type, sprite_container.block_name.value, len(new_sprite_list))
raise generic.ScriptError(msg, sprite_container.pos)
for i, sprite in enumerate(new_sprite_list):
sprite.zoom_level = zoom_level
sprite.bit_depth = bit_depth
if bit_depth == 8 and isinstance(sprite, RealSprite) and (not sprite.is_empty) and sprite.mask_file is not None:
raise generic.ScriptError("Mask file may only be specified for 32bpp sprites.", sprite.mask_file.pos)
if first:
if isinstance(sprite, RealSprite):
action_list.append(RealSpriteAction())
else:
assert isinstance(sprite, RecolourSprite)
action_list.append(RecolourSpriteAction(sprite))
else:
# Not the first sprite, so an alternative sprite
if isinstance(sprite, RecolourSprite) or isinstance(action_list[i], RecolourSpriteAction):
raise generic.ScriptError("Alternative sprites may only be provided for and contain real sprites, not recolour sprites.", sprite_container.pos)
if action_list[i].sprite_list[0].is_empty and not sprite.is_empty:
# if the first sprite is empty, all others are ignored
generic.print_warning("Alternative sprites for an empty real sprite are ignored.", sprite_container.pos)
if isinstance(sprite, RealSprite): action_list[i].add_sprite(sprite)
first = False
if len(action_list) != 0: action_list[-1].last = True
return action_list
nml-0.4.5/nml/actions/actionE.py 0000644 0005672 0005672 00000002320 13315644406 017635 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions import base_action
class ActionE(base_action.BaseAction):
def __init__(self, grfid_list):
self.grfid_list = grfid_list
def write(self, file):
size = 2 + 4 * len(self.grfid_list)
file.start_sprite(size)
file.print_bytex(0x0E)
file.print_byte(len(self.grfid_list))
for grfid in self.grfid_list:
file.newline()
file.print_dwordx(grfid)
file.newline()
file.end_sprite()
def parse_deactivate_block(block):
return [ActionE(block.grfid_list)]
nml-0.4.5/nml/actions/action3.py 0000644 0005672 0005672 00000050037 13315644406 017623 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, expression, global_constants, nmlop
from nml.actions import base_action, action0, action2, action2real, action2var, action3_callbacks, action6, actionD
class Action3(base_action.BaseAction):
"""
Class representing a single Action3.
@ivar feature: Action3 feature byte.
@type feature: C{int}
@ivar id: Item ID of the item that this action3 represents.
@type id: C{int}
@ivar is_livery_override: Whether this action 3 is a livery override
@type is_livery_override: C{bool}
@ivar cid_mappings: List of mappings that map cargo IDs to Action2s.
@type cid_mappings: C{list} of C{tuple}: (C{int}, L{Expression} before prepare_output, C{int} afterwards, C{str})
@ivar def_cid: Default Action2 to use if no cargo ID matches.
@type def_cid: C{None} or L{SpriteGroupRef} before prepare_output, C{int} afterwards
@ivar references: All Action2s that are referenced by this Action3.
@type references: C{list} of L{Action2Reference}
"""
def __init__(self, feature, id, is_livery_override):
self.feature = feature
self.id = id
self.is_livery_override = is_livery_override
self.cid_mappings = []
self.references = []
def prepare_output(self, sprite_num):
action2.free_references(self)
map_cid = lambda cid: cid.get_action2_id(self.feature) if isinstance(cid, expression.SpriteGroupRef) else cid.value | 0x8000
self.cid_mappings = [(cargo, map_cid(cid), comment) for cargo, cid, comment in self.cid_mappings]
if self.def_cid is None:
self.def_cid = 0
else:
self.def_cid = map_cid(self.def_cid)
def write(self, file):
size = 7 + 3 * len(self.cid_mappings)
if self.feature <= 3: size += 2 # Vehicles use extended byte
file.start_sprite(size)
file.print_bytex(3)
file.print_bytex(self.feature)
file.print_bytex(1 if not self.is_livery_override else 0x81) # a single id
file.print_varx(self.id, 3 if self.feature <= 3 else 1)
file.print_byte(len(self.cid_mappings))
file.newline()
for cargo, cid, comment in self.cid_mappings:
file.print_bytex(cargo)
file.print_wordx(cid)
file.newline(comment)
file.print_wordx(self.def_cid)
file.newline(self.default_comment)
file.end_sprite()
def skip_action9(self):
return False
# Make sure all action2s created here get a unique name
action2_id = 0
def create_intermediate_varaction2(feature, varact2parser, mapping, default, pos):
"""
Create a varaction2 based on a parsed expression and a value mapping
@param feature: Feature of the varaction2
@type feature: C{int}
@param varact2parser: Parser containing a parsed expression
@type varact2parser: L{Varaction2Parser}
@param mapping: Mapping of various values to sprite groups / return values, with a possible extra function to apply to the return value
@type mapping: C{dict} that maps C{int} to C{tuple} of (L{SpriteGroupRef}, C{function}, or C{None})
@param default: Default sprite group if no value matches
@type default: L{SpriteGroupRef}
@param pos: Positional context.
@type pos: L{Position}
@return: A tuple containing the action list and a reference to the created action2
@rtype: C{tuple} of (C{list} of L{BaseAction}, L{SpriteGroupRef})
"""
global action2_id
action_list = varact2parser.extra_actions
act6 = action6.Action6()
for mod in varact2parser.mods:
act6.modify_bytes(mod.param, mod.size, mod.offset + 4)
name = expression.Identifier("@action3_{:d}".format(action2_id))
action2_id += 1
varaction2 = action2var.Action2Var(feature, name.value, pos, 0x89)
varaction2.var_list = varact2parser.var_list
offset = 5 + varact2parser.var_list_size
for proc in varact2parser.proc_call_list:
action2.add_ref(proc, varaction2, True)
for switch_value in sorted(mapping):
return_value, ret_value_function = mapping[switch_value]
if ret_value_function is None:
result, comment = action2var.parse_result(return_value, action_list, act6, offset, varaction2, None, 0x89)
else:
if isinstance(return_value, expression.SpriteGroupRef):
# We need to execute the callback via a procedure call
# then return CB_FAILED if the CB failed,
# or the CB result (with ret_value_function applied) if successful
if return_value.name.value == 'CB_FAILED':
result, comment = action2var.parse_result(return_value, action_list, act6, offset, varaction2, None, 0x89)
else:
extra_actions, result, comment = create_proc_call_varaction2(feature, return_value, ret_value_function, pos)
action_list.extend(extra_actions)
else:
return_value = ret_value_function(return_value).reduce()
result, comment = action2var.parse_result(return_value, action_list, act6, offset, varaction2, None, 0x89)
varaction2.ranges.append(action2var.VarAction2Range(expression.ConstantNumeric(switch_value), expression.ConstantNumeric(switch_value), result, comment))
offset += 10
result, comment = action2var.parse_result(default, action_list, act6, offset, varaction2, None, 0x89)
varaction2.default_result = result
varaction2.default_comment = comment
return_ref = expression.SpriteGroupRef(name, [], None, varaction2)
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(varaction2)
return (action_list, return_ref)
def create_proc_call_varaction2(feature, proc, ret_value_function, pos):
"""
Create a varaction2 that executes a procedure call and applies a function on the result
@param feature: Feature of the varaction2
@type feature: C{int}
@param proc: Procedure to execute
@type proc: L{SpriteGroupRef}
@param ret_value_function: Function to apply on the result (L{Expression} -> L{Expression})
@type ret_value_function: C{function}
@param pos: Positional context.
@type pos: L{Position}
@return: A list of extra actions, reference to the created action2 and a comment to add
@rtype: C{tuple} of (C{list} of L{BaseAction}, L{SpriteGroupRef}, C{str})
"""
varact2parser = action2var.Varaction2Parser(feature)
varact2parser.parse_proc_call(proc)
mapping = {0xFFFF: (expression.SpriteGroupRef(expression.Identifier('CB_FAILED'), [], None), None)}
default = ret_value_function(expression.Variable(expression.ConstantNumeric(0x1C)))
action_list, result = create_intermediate_varaction2(feature, varact2parser, mapping, default, pos)
comment = result.name.value + ';'
return (action_list, result, comment)
def create_cb_choice_varaction2(feature, expr, mapping, default, pos):
"""
Create a varaction2 that maps callback numbers to various sprite groups
@param feature: Feature of the varaction2
@type feature: C{int}
@param expr: Expression to evaluate
@type expr: L{Expression}
@param mapping: Mapping of various values to sprite groups, with a possible extra function to apply to the return value
@type mapping: C{dict} that maps C{int} to C{tuple} of (L{SpriteGroupRef}, C{function}, or C{None})
@param default: Default sprite group if no value matches
@type default: L{SpriteGroupRef}
@param pos: Positional context.
@type pos: L{Position}
@return: A tuple containing the action list and a reference to the created action2
@rtype: C{tuple} of (C{list} of L{BaseAction}, L{SpriteGroupRef})
"""
varact2parser = action2var.Varaction2Parser(feature)
varact2parser.parse_expr(expr)
return create_intermediate_varaction2(feature, varact2parser, mapping, default, pos)
def create_action3(feature, id, action_list, act6, is_livery_override):
# Vehicles use an extended byte
size = 2 if feature <= 3 else 1
offset = 4 if feature <= 3 else 3
id, offset = actionD.write_action_value(id, action_list, act6, offset, size)
return Action3(feature, id.value, is_livery_override)
house_tiles = {
0 : 'n', # 1x1
2 : 'nw', # 2x1
3 : 'ne', # 1x2
4 : 'news', # 2x2
}
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 expression.BinOp(nmlop.ADD, id, expression.ConstantNumeric(i, id.pos), id.pos).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_graphics_block_single_id(graphics_block, feature, id, is_livery_override, house_tile = None, house_north_tile_id = None):
action6.free_parameters.save()
prepend_action_list = []
action_list = []
act6 = action6.Action6()
act3 = create_action3(feature, id, action_list, act6, is_livery_override)
cargo_gfx = {}
seen_callbacks = set()
callbacks = []
livery_override = None # Used for rotor graphics
for graphics in graphics_block.graphics_list:
cargo_id = graphics.cargo_id
if isinstance(cargo_id, expression.Identifier):
cb_name = cargo_id.value
cb_table = action3_callbacks.callbacks[feature]
if cb_name in cb_table:
if cb_name in seen_callbacks:
raise generic.ScriptError("Callback '{}' is defined multiple times.".format(cb_name), cargo_id.pos)
seen_callbacks.add(cb_name)
info_list = cb_table[cb_name]
if not isinstance(info_list, list):
info_list = [info_list]
for info in info_list:
if 'deprecate_message' in info:
generic.print_warning(info['deprecate_message'], cargo_id.pos)
if house_tile is not None and 'tiles' in info and house_tile not in info['tiles']:
continue
if info['type'] == 'cargo':
# Not a callback, but an alias for a certain cargo type
if info['num'] in cargo_gfx:
raise generic.ScriptError("Graphics for '{}' are defined multiple times.".format(cb_name), cargo_id.pos)
cargo_gfx[info['num']] = graphics.result.value
elif info['type'] == 'cb':
callbacks.append( (info, graphics.result.value) )
elif info['type'] == 'override':
assert livery_override is None
livery_override = graphics.result.value
else:
assert False
continue
# Not a callback, so it must be a 'normal' cargo (vehicles/stations only)
cargo_id = cargo_id.reduce_constant(global_constants.const_list)
# Raise the error only now, to let the 'unknown identifier' take precedence
if feature >= 5: raise generic.ScriptError("Associating graphics with a specific cargo is possible only for vehicles and stations.", cargo_id.pos)
if cargo_id.value in cargo_gfx:
raise generic.ScriptError("Graphics for cargo {:d} are defined multiple times.".format(cargo_id.value), cargo_id.pos)
cargo_gfx[cargo_id.value] = graphics.result.value
if graphics_block.default_graphics is not None:
if 'default' not in action3_callbacks.callbacks[feature]:
raise generic.ScriptError("Default graphics may not be defined for this feature (0x{:02X}).".format(feature), graphics_block.default_graphics.pos)
if None in cargo_gfx:
raise generic.ScriptError("Default graphics are defined twice.", graphics_block.default_graphics.pos)
cargo_gfx[None] = graphics_block.default_graphics.value
# An in-between varaction2 is always needed for houses
if len(callbacks) != 0 or feature == 0x07:
cb_flags = 0
# Determine the default value
if None not in cargo_gfx:
cargo_gfx[None] = expression.SpriteGroupRef(expression.Identifier('CB_FAILED', None), [], None)
default_val = cargo_gfx[None]
cb_mapping = {}
cb_buy_mapping = {}
# Special case for vehicle cb 36, maps var10 values to spritegroups
cb36_mapping = {}
cb36_buy_mapping = {}
# Sspecial case for industry production CB, maps var18 values to spritegroups
prod_cb_mapping = {}
for cb_info, gfx in callbacks:
if 'flag_bit' in cb_info:
# Set a bit in the CB flags property
cb_flags |= 1 << cb_info['flag_bit']
value_function = cb_info.get('value_function', None)
mapping_val = (gfx, value_function)
# See action3_callbacks for info on possible values
purchase = cb_info.get('purchase', 0)
if isinstance(purchase, str):
# Not in purchase list, if separate purchase CB is set
purchase = 0 if purchase in seen_callbacks else 1
# Explicit purchase CBs will need a purchase cargo, even if not needed for graphics
if purchase == 2 and 0xFF not in cargo_gfx:
cargo_gfx[0xFF] = default_val
num = cb_info['num']
if num == 0x36:
if purchase != 2: cb36_mapping[cb_info['var10']] = mapping_val
if purchase != 0: cb36_buy_mapping[cb_info['var10']] = mapping_val
elif feature == 0x0A and num == 0x00:
# Industry production CB
assert purchase == 0
prod_cb_mapping[cb_info['var18']] = mapping_val
else:
if purchase != 2: cb_mapping[num] = mapping_val
if purchase != 0: cb_buy_mapping[num] = mapping_val
if cb_flags != 0:
prepend_action_list.extend(action0.get_callback_flags_actions(feature, id, cb_flags))
# Handle CB 36
if len(cb36_mapping) != 0:
expr = expression.Variable(expression.ConstantNumeric(0x10), mask = expression.ConstantNumeric(0xFF))
actions, cb36_ref = create_cb_choice_varaction2(feature, expr, cb36_mapping, default_val, graphics_block.pos)
prepend_action_list.extend(actions)
cb_mapping[0x36] = (cb36_ref, None)
if len(cb36_buy_mapping) != 0:
expr = expression.Variable(expression.ConstantNumeric(0x10), mask = expression.ConstantNumeric(0xFF))
actions, cb36_ref = create_cb_choice_varaction2(feature, expr, cb36_buy_mapping, default_val, graphics_block.pos)
prepend_action_list.extend(actions)
cb_buy_mapping[0x36] = (cb36_ref, None)
if len(prod_cb_mapping) != 0:
expr = expression.Variable(expression.ConstantNumeric(0x18), mask = expression.ConstantNumeric(0xFF))
actions, cb_ref = create_cb_choice_varaction2(feature, expr, prod_cb_mapping, default_val, graphics_block.pos)
prepend_action_list.extend(actions)
cb_mapping[0x00] = (cb_ref, None)
for cargo in sorted(cargo_gfx, key=lambda x: -1 if x is None else x):
mapping = cb_buy_mapping if cargo == 0xFF else cb_mapping
if len(mapping) == 0 and feature != 0x07:
# No callbacks here, so move along
# Except for houses, where we need to store some stuff in a register
continue
if cargo_gfx[cargo] != default_val:
# There are cargo-specific graphics, be sure to handle those
# Unhandled callbacks should chain to the default, though
mapping = mapping.copy()
mapping[0x00] = (cargo_gfx[cargo], None)
expr = expression.Variable(expression.ConstantNumeric(0x0C), mask = expression.ConstantNumeric(0xFFFF))
if feature == 0x07:
# Store relative x/y, item id (of the north tile) and house tile (HOUSE_TILE_XX constant) in register FF
# Format: 0xIIHHYYXX: II: item ID, HH: house tile, YY: relative y, XX: relative x
lowbytes_dict = {
'n' : 0x000000,
'e' : 0x010100,
'w' : 0x020001,
's' : 0x030101,
}
lowbytes = expression.ConstantNumeric(lowbytes_dict[house_tile])
highbyte = expression.BinOp(nmlop.SHIFT_LEFT, house_north_tile_id, expression.ConstantNumeric(24))
register_FF = expression.BinOp(nmlop.OR, lowbytes, highbyte, lowbytes.pos).reduce()
register_FF = expression.BinOp(nmlop.STO_TMP, register_FF, expression.ConstantNumeric(0xFF))
expr = expression.BinOp(nmlop.VAL2, register_FF, expr, register_FF.pos)
if len(mapping) == 0:
# mapping must not be empty
mapping[0x00] = (default_val, None)
actions, cb_ref = create_cb_choice_varaction2(feature, expr, mapping, default_val, graphics_block.pos)
prepend_action_list.extend(actions)
cargo_gfx[cargo] = cb_ref
# Make sure to sort to make the order well-defined
offset = 7 if feature <= 3 else 5
for cargo_id in sorted(cg for cg in cargo_gfx if cg is not None):
result, comment = action2var.parse_result(cargo_gfx[cargo_id], action_list, act6, offset + 1, act3, None, 0x89)
act3.cid_mappings.append( (cargo_id, result, comment) )
offset += 3
if None in cargo_gfx:
result, comment = action2var.parse_result(cargo_gfx[None], action_list, act6, offset, act3, None, 0x89)
act3.def_cid = result
act3.default_comment = comment
else:
act3.def_cid = None
act3.default_comment = ''
if livery_override is not None:
act6livery = action6.Action6()
# Add any extra actions before the main action3 (TTDP requirement)
act3livery = create_action3(feature, id, action_list, act6livery, True)
offset = 7 if feature <= 3 else 5
result, comment = action2var.parse_result(livery_override, action_list, act6livery, offset, act3livery, None, 0x89)
act3livery.def_cid = result
act3livery.default_comment = comment
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(act3)
if livery_override is not None:
if len(act6livery.modifications) > 0: action_list.append(act6livery)
action_list.append(act3livery)
action6.free_parameters.restore()
return prepend_action_list + action_list
nml-0.4.5/nml/actions/actionD.py 0000644 0005672 0005672 00000045137 13315644406 017651 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, global_constants, expression, nmlop
from nml.actions import base_action, action6
from nml.ast import base_statement
import nml
class ActionD(base_action.BaseAction):
"""
ActionD class
General procedure: target = param1 op param2
If one of the params is 0xFF, the value of 'data' is read instead.
@ivar target: Number of the target parameter
@ivar target: L{ConstantNumeric}
@ivar param1: Paramter number of the first operand
@type param1: L{ConstantNumeric}
@ivar op: (Binary) operator to use.
@type op: L{Operator}
@ivar param2: Paramter number of the second operand
@type param2: L{ConstantNumeric}
@ivar data: Numerical data that will be used instead of parameter value,
if the parameter number is 0xFF. None if n/a.
@type data: L{ConstantNumeric} or C{None}
"""
def __init__(self, target, param1, op, param2, data = None):
self.target = target
self.param1 = param1
self.op = op
self.param2 = param2
self.data = data
def write(self, file):
size = 5
if self.data is not None: size += 4
#print the statement for easier debugging
str1 = "param[{}]".format(self.param1) if self.param1.value != 0xFF else str(self.data)
str2 = "param[{}]".format(self.param2) if self.param2.value != 0xFF else str(self.data)
str_total = self.op.to_string(str1, str2) if self.op != nmlop.ASSIGN else str1
file.comment("param[{}] = {}".format(self.target, str_total))
file.start_sprite(size)
file.print_bytex(0x0D)
self.target.write(file, 1)
file.print_bytex(self.op.actd_num, self.op.actd_str)
self.param1.write(file, 1)
self.param2.write(file, 1)
if self.data is not None: self.data.write(file, 4)
file.newline()
file.end_sprite()
def skip_action7(self):
return False
class ParameterAssignment(base_statement.BaseStatement):
"""
AST-node for a parameter assignment.
NML equivalent: param[$num] = $expr;
@ivar param: Target expression to assign (must evaluate to a parameter)
@type param: L{Expression}
@ivar value: Value to assign to this parameter
@type value: L{Expression}
"""
def __init__(self, param, value):
base_statement.BaseStatement.__init__(self, "parameter assignment", param.pos)
self.param = param
self.value = value
def pre_process(self):
self.value = self.value.reduce(global_constants.const_list)
self.param = self.param.reduce(global_constants.const_list, unknown_id_fatal = False)
if isinstance(self.param, expression.SpecialParameter):
if not self.param.can_assign():
raise generic.ScriptError("Trying to assign a value to the read-only variable '{}'".format(self.param.name), self.param.pos)
elif isinstance(self.param, expression.Identifier):
num = action6.free_parameters.pop_unique(self.pos)
global_constants.named_parameters[self.param.value] = num
elif not isinstance(self.param, expression.Parameter):
raise generic.ScriptError("Left side of an assignment must be a parameter.", self.param.pos)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Parameter assignment')
self.param.debug_print(indentation + 2)
self.value.debug_print(indentation + 2)
def get_action_list(self):
return parse_actionD(self)
def __str__(self):
return '{} = {};\n'.format(self.param, self.value)
#prevent evaluating common sub-expressions multiple times
def parse_subexpression(expr, action_list):
if isinstance(expr, expression.ConstantNumeric) or \
(isinstance(expr, expression.Parameter) and isinstance(expr.num, expression.ConstantNumeric)):
return expr
else:
tmp_param, tmp_param_actions = get_tmp_parameter(expr)
action_list.extend(tmp_param_actions)
return expression.Parameter(expression.ConstantNumeric(tmp_param))
#returns a (param_num, action_list) tuple.
def get_tmp_parameter(expr):
param = action6.free_parameters.pop(expr.pos)
actions = parse_actionD(ParameterAssignment(expression.Parameter(expression.ConstantNumeric(param)), expr))
return (param, actions)
def write_action_value(expr, action_list, act6, offset, size):
"""
Write a single numeric value that is part of an action
Possibly use action6 and/or actionD if needed
@param expr: Expression to write
@type expr: L{Expression}
@param action_list: Action list to append any extra actions to
@type action_list: C{list} of L{BaseAction}
@param act6: Action6 to append any modifications to
@type act6: L{Action6}
@param offset: Offset to use for the action 6 (if applicable)
@type offset: C{int}
@param size: Size to use for the action 6 (if applicable)
@type size: C{int}
@return: 2-tuple containing -
- the value to be used when writing the action (0 if overwritten by action 6)
- the offset just past this numeric value (=offset + size)
@rtype: C{tuple} of (L{ConstantNumeric}, C{int})
"""
if isinstance(expr, expression.ConstantNumeric):
result = expr
elif isinstance(expr, expression.Parameter) and isinstance(expr.num, expression.ConstantNumeric):
act6.modify_bytes(expr.num.value, size, offset)
result = expression.ConstantNumeric(0)
else:
tmp_param, tmp_param_actions = get_tmp_parameter(expr)
action_list.extend(tmp_param_actions)
act6.modify_bytes(tmp_param, size, offset)
result = expression.ConstantNumeric(0)
return result, offset + size
def parse_ternary_op(assignment):
assert isinstance(assignment.value, expression.TernaryOp)
actions = parse_actionD(ParameterAssignment(assignment.param, assignment.value.expr2))
cond_block = nml.ast.conditional.Conditional(assignment.value.guard, [ParameterAssignment(assignment.param, assignment.value.expr1)], None)
actions.extend(nml.ast.conditional.ConditionalList([cond_block]).get_action_list())
return actions
def parse_special_check(assignment):
check = assignment.value
assert isinstance(check, expression.SpecialCheck)
actions = parse_actionD(ParameterAssignment(assignment.param, expression.ConstantNumeric(check.results[0])))
value = check.value
if check.mask is not None:
value &= check.mask
value += check.mask << 32
assert check.varsize == 8
else:
assert check.varsize <= 4
actions.append(nml.actions.action7.SkipAction(9, check.varnum, check.varsize, check.op, value, 1))
actions.extend(parse_actionD(ParameterAssignment(assignment.param, expression.ConstantNumeric(check.results[1]))))
return actions
def parse_grm(assignment):
assert isinstance(assignment.value, expression.GRMOp)
action6.free_parameters.save()
action_list = []
act6 = action6.Action6()
assert isinstance(assignment.param, expression.Parameter)
target = assignment.param.num
if isinstance(target, expression.Parameter) and isinstance(target.num, expression.ConstantNumeric):
act6.modify_bytes(target.num.value, 1, 1)
target = expression.ConstantNumeric(0)
elif not isinstance(target, expression.ConstantNumeric):
tmp_param, tmp_param_actions = get_tmp_parameter(target)
act6.modify_bytes(tmp_param, 1, 1)
target = expression.ConstantNumeric(0)
action_list.extend(tmp_param_actions)
op = nmlop.ASSIGN
param1 = assignment.value.op
param2 = expression.ConstantNumeric(0xFE)
data = expression.ConstantNumeric(0xFF | (assignment.value.feature << 8) | (assignment.value.count << 16))
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(ActionD(target, param1, op, param2, data))
action6.free_parameters.restore()
return action_list
def parse_hasbit(assignment):
assert isinstance(assignment.value, expression.BinOp) and (assignment.value.op == nmlop.HASBIT or assignment.value.op == nmlop.NOTHASBIT)
actions = parse_actionD(ParameterAssignment(assignment.param, expression.ConstantNumeric(0)))
cond_block = nml.ast.conditional.Conditional(assignment.value, [ParameterAssignment(assignment.param, expression.ConstantNumeric(1))], None)
actions.extend(nml.ast.conditional.ConditionalList([cond_block]).get_action_list())
return actions
def parse_min_max(assignment):
assert isinstance(assignment.value, expression.BinOp) and assignment.value.op in (nmlop.MIN, nmlop.MAX)
#min(a, b) ==> a < b ? a : b.
#max(a, b) ==> a > b ? a : b.
action6.free_parameters.save()
action_list = []
expr1 = parse_subexpression(assignment.value.expr1, action_list)
expr2 = parse_subexpression(assignment.value.expr2, action_list)
guard = expression.BinOp(nmlop.CMP_LT if assignment.value.op == nmlop.MIN else nmlop.CMP_GT, expr1, expr2)
action_list.extend(parse_actionD(ParameterAssignment(assignment.param, expression.TernaryOp(guard, expr1, expr2, None))))
action6.free_parameters.restore()
return action_list
def parse_boolean(assignment):
assert isinstance(assignment.value, expression.Boolean)
actions = parse_actionD(ParameterAssignment(assignment.param, expression.ConstantNumeric(0)))
expr = expression.BinOp(nmlop.CMP_NEQ, assignment.value.expr, expression.ConstantNumeric(0))
cond_block = nml.ast.conditional.Conditional(expr, [ParameterAssignment(assignment.param, expression.ConstantNumeric(1))], None)
actions.extend(nml.ast.conditional.ConditionalList([cond_block]).get_action_list())
return actions
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, expression.BinOp(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, expression.BinOp(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, expression.BinOp(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, expression.BinOp(nmlop.SUB, expr1, expr2))))
# Clamp the value to 0/1, see above for details
extra_actions.extend(parse_actionD(ParameterAssignment(assignment.param, expression.BinOp(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 = expression.BinOp(nmlop.SUB, expression.ConstantNumeric(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(expression.BinOp(nmlop.OR, expr1, expr2))
tmp_param2, tmp_action_list2 = get_tmp_parameter(expression.BinOp(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
def parse_actionD(assignment):
assignment.value.supported_by_actionD(True)
if isinstance(assignment.param, expression.SpecialParameter):
assignment.param, assignment.value = assignment.param.to_assignment(assignment.value)
elif isinstance(assignment.param, expression.Identifier):
assignment.param = expression.Parameter(expression.ConstantNumeric(global_constants.named_parameters[assignment.param.value]), assignment.param.pos)
assert isinstance(assignment.param, expression.Parameter)
if isinstance(assignment.value, expression.SpecialParameter):
assignment.value = assignment.value.to_reading()
if isinstance(assignment.value, expression.TernaryOp):
return parse_ternary_op(assignment)
if isinstance(assignment.value, expression.SpecialCheck):
return parse_special_check(assignment)
if isinstance(assignment.value, expression.GRMOp):
return parse_grm(assignment)
if isinstance(assignment.value, expression.BinOp):
op = assignment.value.op
if op == nmlop.HASBIT or op == nmlop.NOTHASBIT:
return parse_hasbit(assignment)
elif op == nmlop.MIN or op == nmlop.MAX:
return parse_min_max(assignment)
if isinstance(assignment.value, expression.Boolean):
return parse_boolean(assignment)
if isinstance(assignment.value, expression.Not):
expr = expression.BinOp(nmlop.SUB, expression.ConstantNumeric(1), assignment.value.expr)
assignment = ParameterAssignment(assignment.param, expr)
if isinstance(assignment.value, expression.BinNot):
expr = expression.BinOp(nmlop.SUB, expression.ConstantNumeric(0xFFFFFFFF), assignment.value.expr)
assignment = ParameterAssignment(assignment.param, expr)
action6.free_parameters.save()
action_list = []
act6 = action6.Action6()
assert isinstance(assignment.param, expression.Parameter)
target = assignment.param.num
if isinstance(target, expression.Parameter) and isinstance(target.num, expression.ConstantNumeric):
act6.modify_bytes(target.num.value, 1, 1)
target = expression.ConstantNumeric(0)
elif not isinstance(target, expression.ConstantNumeric):
tmp_param, tmp_param_actions = get_tmp_parameter(target)
act6.modify_bytes(tmp_param, 1, 1)
target = expression.ConstantNumeric(0)
action_list.extend(tmp_param_actions)
data = None
#print assignment.value
if isinstance(assignment.value, expression.ConstantNumeric):
op = nmlop.ASSIGN
param1 = expression.ConstantNumeric(0xFF)
param2 = expression.ConstantNumeric(0)
data = assignment.value
elif isinstance(assignment.value, expression.Parameter):
if isinstance(assignment.value.num, expression.ConstantNumeric):
op = nmlop.ASSIGN
param1 = assignment.value.num
else:
tmp_param, tmp_param_actions = get_tmp_parameter(assignment.value.num)
act6.modify_bytes(tmp_param, 1, 3)
action_list.extend(tmp_param_actions)
op = nmlop.ASSIGN
param1 = expression.ConstantNumeric(0)
param2 = expression.ConstantNumeric(0)
elif isinstance(assignment.value, expression.OtherGRFParameter):
op = nmlop.ASSIGN
if isinstance(assignment.value.num, expression.ConstantNumeric):
param1 = assignment.value.num
else:
tmp_param, tmp_param_actions = get_tmp_parameter(assignment.value.num)
act6.modify_bytes(tmp_param, 1, 3)
action_list.extend(tmp_param_actions)
param1 = expression.ConstantNumeric(0)
param2 = expression.ConstantNumeric(0xFE)
data = expression.ConstantNumeric(expression.parse_string_to_dword(assignment.value.grfid))
elif isinstance(assignment.value, expression.PatchVariable):
op = nmlop.ASSIGN
param1 = expression.ConstantNumeric(assignment.value.num)
param2 = expression.ConstantNumeric(0xFE)
data = expression.ConstantNumeric(0xFFFF)
elif isinstance(assignment.value, expression.BinOp):
op, expr1, expr2, extra_actions = transform_bin_op(assignment)
action_list.extend(extra_actions)
if isinstance(expr1, expression.ConstantNumeric):
param1 = expression.ConstantNumeric(0xFF)
data = expr1
elif isinstance(expr1, expression.Parameter) and isinstance(expr1.num, expression.ConstantNumeric):
param1 = expr1.num
else:
tmp_param, tmp_param_actions = get_tmp_parameter(expr1)
action_list.extend(tmp_param_actions)
param1 = expression.ConstantNumeric(tmp_param)
# We can use the data only for one for the parameters.
# If the first parameter uses "data" we need a temp parameter for this one
if isinstance(expr2, expression.ConstantNumeric) and data is None:
param2 = expression.ConstantNumeric(0xFF)
data = expr2
elif isinstance(expr2, expression.Parameter) and isinstance(expr2.num, expression.ConstantNumeric):
param2 = expr2.num
else:
tmp_param, tmp_param_actions = get_tmp_parameter(expr2)
action_list.extend(tmp_param_actions)
param2 = expression.ConstantNumeric(tmp_param)
else: raise generic.ScriptError("Invalid expression in argument assignment", assignment.value.pos)
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(ActionD(target, param1, op, param2, data))
action6.free_parameters.restore()
return action_list
nml-0.4.5/nml/actions/action4.py 0000644 0005672 0005672 00000017577 13315644406 017640 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, grfstrings, generic
from nml.actions import base_action, action6, actionD
class Action4(base_action.BaseAction):
"""
Class representing a single action 4.
Format: 04
@ivar feature: Feature of this action 4
@type feature: C{int}
@ivar lang: Language ID of the text (set as ##grflangid in the .lng file, 0x7F for default)
@type lang: C{int}
@ivar size: Size of the id, may be 1 (byte), 2 (word) or 3 (ext. byte)
@type size: C{int}
@ivar id: ID of the first string to write
@type id: C{int}
@ivar texts: List of strings to write
@type texts: C{list} of C{str}
"""
def __init__(self, feature, lang, size, id, texts):
self.feature = feature
self.lang = lang
self.size = size
self.id = id
self.texts = texts
def prepare_output(self, sprite_num):
#To indicate a word value, bit 7 of the lang ID must be set
if self.size == 2: self.lang = self.lang | 0x80
def write(self, file):
size = 4 + self.size
for text in self.texts:
size += grfstrings.get_string_size(text)
file.start_sprite(size)
file.print_bytex(4)
file.print_bytex(self.feature)
file.print_bytex(self.lang)
file.print_bytex(len(self.texts))
file.print_varx(self.id, self.size)
for text in self.texts:
file.print_string(text)
file.newline()
file.end_sprite()
def skip_action9(self):
return False
# List of various string ranges that may be used
# Attributes:
# - random_id: If true, string IDs may be allocated randomly, else the ID has a special meaning and must be assigned (e.g. for vehicles, string ID = vehicle ID)
# - ids: List of free IDs, only needed if random_id is true. Whenever an ID is used, it's removed from the list
string_ranges = {
0xC4: {'random_id': False}, # Station class names
0xC5: {'random_id': False}, # Station names
0xC9: {'random_id': False}, # House name
# Misc. text ids, used for callbacks and such
0xD0: {'random_id': True, 'total': 0x400, 'ids': list(range(0xD3FF, 0xCFFF, -1))},
# Misc. persistent text ids, used to set properties. Use Ids DC00..DCFF first to keep compatibility with older versions of OTTD.
0xDC: {'random_id': True, 'total': 0x800, 'ids': list(range(0xDBFF, 0xD7FF, -1)) + list(range(0xDFFF, 0xDBFF, -1))},
}
# Mapping of string identifiers to D0xx/DCxx text ids
# This allows outputting strings only once, instead of everywhere they are used
used_strings = {
0xD0: {},
0xDC: {},
}
def print_stats():
"""
Print statistics about used ids.
"""
for t, l in string_ranges.items():
if l['random_id']:
num_used = l['total'] - len(l['ids'])
if num_used > 0:
generic.print_info("{:02X}xx strings: {}/{}".format(t, num_used, l['total']))
def get_global_string_actions():
"""
Get a list of global string actions
i.e. print all D0xx / DCxx texts at once
@return: A list of all D0xx / DCxx action4s
@rtype: C{list} of L{BaseAction}
"""
texts = []
actions = []
for string_range, strings in list(used_strings.items()):
for feature_name, id in list(strings.items()):
feature, string_name = feature_name
texts.append( (0x7F, id, grfstrings.get_translation(string_name), feature) )
for lang_id in grfstrings.get_translations(string_name):
texts.append( (lang_id, id, grfstrings.get_translation(string_name, lang_id), feature) )
last_lang = -1
last_id = -1
last_feature = -1
# Sort to have a deterministic ordering and to have as much consecutive IDs as possible
texts.sort(key=lambda text: (-1 if text[0] == 0x7F else text[0], text[1]))
for text in texts:
str_lang, str_id, str_text, feature = text
# If possible, append strings to the last action 4 instead of creating a new one
if str_lang != last_lang or str_id - 1 != last_id or feature != last_feature or len(actions[-1].texts) == 0xFF:
actions.append(Action4(feature, str_lang, 2, str_id, [str_text]))
else:
actions[-1].texts.append(str_text)
last_lang = str_lang
last_id = str_id
last_feature = feature
return actions
def get_string_action4s(feature, string_range, string, id = None):
"""
Let a string from the lang files be used in the rest of NML.
This may involve adding actions directly, but otherwise an ID is allocated and the string will be written later
@param feature: Feature that uses the string
@type feature: C{int}
@param string_range: String range to use, either a value from L{string_ranges} or C{None} if N/A (item names)
@type string_range: C{int} or C{None}
@param string: String to parse
@type string: L{expression.String}
@param id: ID to use for this string, or C{None} if it will be allocated dynamically (random_id is true for the string range)
@type id: L{Expression} or C{None}
@return: A tuple of two values:
- ID of the string (useful if allocated dynamically)
- Resulting action list to be appended
@rtype: C{tuple} of (C{int}, C{list} of L{BaseAction})
"""
grfstrings.validate_string(string)
write_action4s = True
action6.free_parameters.save()
actions = []
mod = None
if string_range is not None:
size = 2
if string_ranges[string_range]['random_id']:
# ID is allocated randomly, we will output the actions later
write_action4s = False
if (feature, string) in used_strings[string_range]:
id_val = used_strings[string_range][(feature, string)]
else:
id_val = string_ranges[string_range]['ids'].pop()
used_strings[string_range][(feature, string)] = id_val
else:
# ID must be supplied
assert id is not None
assert isinstance(id, expression.ConstantNumeric)
id_val = id.value | (string_range << 8)
else:
# Not a string range, so we must have an id
assert id is not None
size = 3 if feature <= 3 else 1
if isinstance(id, expression.ConstantNumeric):
id_val = id.value
else:
id_val = 0
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(id)
actions.extend(tmp_param_actions)
# Apply ID via action4 later
mod = (tmp_param, 2 if feature <= 3 else 1, 5 if feature <= 3 else 4)
if write_action4s:
strings = [(lang_id, grfstrings.get_translation(string, lang_id)) for lang_id in grfstrings.get_translations(string)]
# Sort the strings for deterministic ordering and prepend the default language
strings = [(0x7F, grfstrings.get_translation(string))] + sorted(strings, key=lambda lang_text: lang_text[0])
for lang_id, text in strings:
if mod is not None:
act6 = action6.Action6()
act6.modify_bytes(*mod)
actions.append(act6)
actions.append(Action4(feature, lang_id, size, id_val, [text]))
action6.free_parameters.restore()
return (id_val, actions)
nml-0.4.5/nml/actions/action2random.py 0000644 0005672 0005672 00000034642 13315644406 021027 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions import action2, action2var, action2real, action6
from nml import generic, expression, nmlop
class Action2Random(action2.Action2):
def __init__(self, feature, name, pos, type_byte, count, triggers, randbit, nrand):
action2.Action2.__init__(self, feature, name, pos)
self.type_byte = type_byte
self.count = count
self.triggers = triggers
self.randbit = randbit
self.nrand = nrand
self.choices = []
def prepare_output(self, sprite_num):
action2.Action2.prepare_output(self, sprite_num)
for choice in self.choices:
if isinstance(choice.result, expression.SpriteGroupRef):
choice.result = choice.result.get_action2_id(self.feature)
else:
choice.result = choice.result.value | 0x8000
def write(self, file):
# []
size = 4 + 2 * self.nrand + (self.count is not None)
action2.Action2.write_sprite_start(self, file, size)
file.print_bytex(self.type_byte)
if self.count is not None: file.print_bytex(self.count)
file.print_bytex(self.triggers)
file.print_byte(self.randbit)
file.print_bytex(self.nrand)
file.newline()
for choice in self.choices:
for i in range(0, choice.prob):
file.print_wordx(choice.result)
file.comment(choice.comment)
file.end_sprite()
class RandomAction2Choice(object):
def __init__(self, result, prob, comment):
self.result = result
self.prob = prob
self.comment = comment
vehicle_random_types = {
'SELF' : {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 8, 'triggers': True},
'PARENT' : {'type': 0x83, 'param': 0, 'first_bit': 0, 'num_bits': 8, 'triggers': False},
'TILE' : {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 8, 'triggers': False},
'BACKWARD_SELF' : {'type': 0x84, 'param': 1, 'first_bit': 0, 'num_bits': 8, 'triggers': False, 'value': 0x00},
'FORWARD_SELF' : {'type': 0x84, 'param': 1, 'first_bit': 0, 'num_bits': 8, 'triggers': False, 'value': 0x40},
'BACKWARD_ENGINE' : {'type': 0x84, 'param': 1, 'first_bit': 0, 'num_bits': 8, 'triggers': False, 'value': 0x80},
'BACKWARD_SAMEID' : {'type': 0x84, 'param': 1, 'first_bit': 0, 'num_bits': 8, 'triggers': False, 'value': 0xC0},
}
random_types = {
0x00 : vehicle_random_types,
0x01 : vehicle_random_types,
0x02 : vehicle_random_types,
0x03 : vehicle_random_types,
0x04 : {'SELF': {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 16, 'triggers': True}, 'TILE': {'type': 0x80, 'param': 0, 'first_bit': 16, 'num_bits': 4, 'triggers': True}},
0x05 : {'SELF': {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 8, 'triggers': False}},
0x06 : {},
0x07 : {'SELF': {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 8, 'triggers': True}},
0x08 : {},
0x09 : {'SELF': {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 8, 'triggers': True}, 'PARENT': {'type': 0x83, 'first_bit': 0, 'num_bits': 16, 'triggers': True}},
0x0A : {'SELF': {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 16, 'triggers': False}},
0x0B : {},
0x0C : {},
0x0D : {},
0x0E : {},
0x0F : {'SELF': {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 8, 'triggers': False}},
0x10 : {'SELF': {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 2, 'triggers': False}},
0x11 : {'SELF': {'type': 0x80, 'param': 0, 'first_bit': 0, 'num_bits': 16, 'triggers': False}, 'TILE': {'type': 0x80, 'first_bit': 16, 'num_bits': 4, 'triggers': False}},
}
def parse_randomswitch_type(random_switch):
"""
Parse the type of a random switch to determine the type and random bits to use.
@param random_switch: Random switch to parse the type of
@type random_switch: L{RandomSwitch}
@return: A tuple containing the following:
- The type byte of the resulting random action2.
- The value to use as , None if N/A.
- Expression to parse in a preceding switch-block, None if N/A.
- The first random bit that should be used (often 0)
- The number of random bits available
@rtype: C{tuple} of (C{int}, C{int} or C{None}, L{Expression} or C{None}, C{int}, C{int})
"""
# Extract some stuff we'll often need
type_str = random_switch.type.value
type_pos = random_switch.type.pos
feature_val = next(iter(random_switch.feature_set))
# Validate type name / param combination
if type_str not in random_types[feature_val]:
raise generic.ScriptError("Invalid combination for random_switch feature {:d} and type '{}'. ".format(feature_val, type_str), type_pos)
type_info = random_types[feature_val][type_str]
count_expr = None
if random_switch.type_count is None:
# No param given
if type_info['param'] == 1:
raise generic.ScriptError("Value '{}' for random_switch parameter 2 'type' requires a parameter.".format(type_str), type_pos)
count = None
else:
# Param given
if type_info['param'] == 0:
raise generic.ScriptError("Value '{}' for random_switch parameter 2 'type' should not have a parameter.".format(type_str), type_pos)
if isinstance(random_switch.type_count, expression.ConstantNumeric) and 1 <= random_switch.type_count.value <= 15:
count = random_switch.type_count.value
else:
count = 0
count_expr = expression.BinOp(nmlop.STO_TMP, random_switch.type_count, expression.ConstantNumeric(0x100), type_pos)
count = type_info['value'] | count
if random_switch.triggers.value != 0 and not type_info['triggers']:
raise generic.ScriptError("Triggers may not be set for random_switch feature {:d} and type '{}'. ".format(feature_val, type_str), type_pos)
# Determine type byte and random bits
type_byte = type_info['type']
start_bit = type_info['first_bit']
bits_available = type_info['num_bits']
return type_byte, count, count_expr, start_bit, bits_available
def lookup_random_action2(sg_ref):
"""
Lookup a sprite group reference to find the corresponding random action2
@param sg_ref: Reference to random action2
@type sg_ref: L{SpriteGroupRef}
@return: Random action2 corresponding to this sprite group, or None if N/A
@rtype: L{Action2Random} or C{None}
"""
spritegroup = action2.resolve_spritegroup(sg_ref.name)
act2 = spritegroup.random_act2
assert act2 is None or isinstance(act2, Action2Random)
return act2
def parse_randomswitch_dependencies(random_switch, start_bit, bits_available, nrand):
"""
Handle the dependencies between random chains to determine the random bits to use
@param random_switch: Random switch to parse
@type random_switch: L{RandomSwitch}
@param start_bit: First available random bit
@type start_bit: C{int}
@param bits_available: Number of random bits available
@type bits_available: C{int}
@param nrand: Number of random choices to use
@type nrand: C{int}
@return: A tuple of two values:
- The first random bit to use
- The number of random choices to use. This may be higher the the original amount passed as paramter
@rtype: C{tuple} of (C{int}, C{int})
"""
#Dependent random chains
act2_to_copy = None
for dep in random_switch.dependent:
act2 = lookup_random_action2(dep)
if act2 is None: continue # May happen if said random switch is not used and therefore not parsed
if act2_to_copy is not None:
if act2_to_copy.randbit != act2.randbit:
msg = "random_switch '{}' cannot be dependent on both '{}' and '{}' as these are independent of each other."
msg = msg.format(random_switch.name.value, act2_to_copy.name, act2.name)
raise generic.ScriptError(msg, random_switch.pos)
if act2_to_copy.nrand != act2.nrand:
msg = "random_switch '{}' cannot be dependent on both '{}' and '{}' as they don't use the same amount of random data."
msg = msg.format(random_switch.name.value, act2_to_copy.name, act2.name)
raise generic.ScriptError(msg, random_switch.pos)
else:
act2_to_copy = act2
if act2_to_copy is not None:
randbit = act2_to_copy.randbit
if nrand > act2_to_copy.nrand:
msg = "random_switch '{}' cannot be dependent on '{}' as it requires more random data."
msg = msg.format(random_switch.name.value, act2_to_copy.name)
raise generic.ScriptError(msg, random_switch.pos)
nrand = act2_to_copy.nrand
else:
randbit = -1
#INdependent random chains
possible_mask = ((1 << bits_available) - 1) << start_bit
for indep in random_switch.independent:
act2 = lookup_random_action2(indep)
if act2 is None: continue # May happen if said random switch is not used and therefore not parsed
possible_mask &= ~((act2.nrand - 1) << act2.randbit)
required_mask = nrand - 1
if randbit != -1:
#randbit has already been determined. Check that it is suitable
if possible_mask & (required_mask << randbit) != (required_mask << randbit):
msg = "Combination of dependence on and independence from random_switches is not possible for random_switch '{}'."
msg = msg.format(random_switch.name.value)
raise generic.ScriptError(msg, random_switch.pos)
else:
#find a suitable randbit
for i in range(start_bit, bits_available + start_bit):
if possible_mask & (required_mask << i) == (required_mask << i):
randbit = i
break
else:
msg = "Independence of all given random_switches is not possible for random_switch '{}'."
msg = msg.format(random_switch.name.value)
raise generic.ScriptError(msg, random_switch.pos)
return randbit, nrand
def parse_randomswitch(random_switch):
"""
Parse a randomswitch block into actions
@param random_switch: RandomSwitch block to parse
@type random_switch: L{RandomSwitch}
@return: List of actions
@rtype: C{list} of L{BaseAction}
"""
action_list = action2real.create_spriteset_actions(random_switch)
feature = next(iter(random_switch.feature_set))
type_byte, count, count_expr, start_bit, bits_available = parse_randomswitch_type(random_switch)
total_prob = sum([choice.probability.value for choice in random_switch.choices])
assert total_prob > 0
nrand = 1
while nrand < total_prob: nrand <<= 1
# Verify that enough random data is available
if min(1 << bits_available, 0x80) < nrand:
msg = "The maximum sum of all random_switch probabilities is {:d}, encountered {:d}."
msg = msg.format(min(1 << bits_available, 0x80), total_prob)
raise generic.ScriptError(msg, random_switch.pos)
randbit, nrand = parse_randomswitch_dependencies(random_switch, start_bit, bits_available, nrand)
random_action2 = Action2Random(feature, random_switch.name.value, random_switch.pos, type_byte, count, random_switch.triggers.value, randbit, nrand)
random_switch.random_act2 = random_action2
action6.free_parameters.save()
act6 = action6.Action6()
offset = 8 if count is not None else 7
#divide the 'extra' probabilities in an even manner
i = 0
resulting_prob = dict((c, c.probability.value) for c in random_switch.choices)
while i < (nrand - total_prob):
best_choice = None
best_ratio = 0
for choice in random_switch.choices:
#float division, so 9 / 10 = 0.9
ratio = choice.probability.value / float(resulting_prob[choice] + 1)
if ratio > best_ratio:
best_ratio = ratio
best_choice = choice
assert best_choice is not None
resulting_prob[best_choice] += 1
i += 1
for choice in random_switch.choices:
res_prob = resulting_prob[choice]
result, comment = action2var.parse_result(choice.result.value, action_list, act6, offset, random_action2, None, 0x89, res_prob)
offset += res_prob * 2
comment = "({:d}/{:d}) -> ({:d}/{:d}): ".format(choice.probability.value, total_prob, res_prob, nrand) + comment
random_action2.choices.append(RandomAction2Choice(result, res_prob, comment))
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(random_action2)
if count_expr is None:
random_switch.set_action2(random_action2, feature)
else:
# Create intermediate varaction2
varaction2 = action2var.Action2Var(feature, '{}@registers'.format(random_switch.name.value), random_switch.pos, 0x89)
varact2parser = action2var.Varaction2Parser(feature)
varact2parser.parse_expr(count_expr)
varaction2.var_list = varact2parser.var_list
action_list.extend(varact2parser.extra_actions)
extra_act6 = action6.Action6()
for mod in varact2parser.mods:
extra_act6.modify_bytes(mod.param, mod.size, mod.offset + 4)
if len(extra_act6.modifications) > 0: action_list.append(extra_act6)
ref = expression.SpriteGroupRef(random_switch.name, [], None, random_action2)
varaction2.ranges.append(action2var.VarAction2Range(expression.ConstantNumeric(0), expression.ConstantNumeric(0), ref, ''))
varaction2.default_result = ref
varaction2.default_comment = ''
# Add two references (default + range)
action2.add_ref(ref, varaction2)
action2.add_ref(ref, varaction2)
random_switch.set_action2(varaction2, feature)
action_list.append(varaction2)
action6.free_parameters.restore()
return action_list
nml-0.4.5/nml/actions/actionA.py 0000644 0005672 0005672 00000005476 13315644406 017650 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, nmlop
from nml.actions import base_action, real_sprite, actionD, action6
class ActionA(base_action.BaseAction):
"""
Action class for Action A (sprite replacement)
@ivar sets: List of sprite collections to be replaced.
@type sets: C{list} of (C{int}, C{int})-tuples
"""
def __init__(self, sets):
self.sets = sets
def write(self, file):
# * 0A [ ]+
size = 2 + 3 * len(self.sets)
file.start_sprite(size)
file.print_bytex(0x0A)
file.print_byte(len(self.sets))
for num, first in self.sets:
file.print_byte(num)
file.print_word(first)
file.newline()
file.end_sprite()
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
sprite_offset = 0 # Number of sprites already covered by previous [ ]-pairs
while total_sprites > 0:
this_block = min(total_sprites, 255) # number of sprites in this block
total_sprites -= this_block
offset += 1 # Skip
first_sprite = replaces.start_id # number of first sprite
if sprite_offset != 0:
first_sprite = expression.BinOp(nmlop.ADD, first_sprite, expression.ConstantNumeric(sprite_offset, first_sprite.pos), first_sprite.pos).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
nml-0.4.5/nml/actions/__init__.py 0000644 0005672 0005672 00000001243 13315644406 020015 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
nml-0.4.5/nml/actions/action2var.py 0000644 0005672 0005672 00000126270 13315644406 020336 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions import action2, action2real, action2var_variables, action4, action6, actionD
from nml import expression, generic, global_constants, nmlop
from nml.ast import general
class Action2Var(action2.Action2):
"""
Variational Action2. This is the NFO equivalent of a switch-block in NML.
It computes a single integer from one or more variables and picks it's
return value based on the result of the computation. The return value can
be either a 15bit integer or a reference to another action2.
@ivar type_byte: The size (byte, word, double word) and access type (own
object or related object). 0x89 (own object, double word)
and 0x8A (related object, double word) and the only
supported values.
@type type_byte: C{int}
@ivar ranges: List of return value ranges. Each range contains a minimum and
a maximum value and a return value. The list is checked in order,
if the result of the computation is between the minimum and
maximum (inclusive) of one range the result of that range is
returned. The result can be either an integer of another
action2.
@ivar ranges: C{list} of L{VarAction2Range}
"""
def __init__(self, feature, name, pos, type_byte):
action2.Action2.__init__(self, feature, name, pos)
self.type_byte = type_byte
self.ranges = []
def resolve_tmp_storage(self):
for var in self.var_list:
if isinstance(var, VarAction2StoreTempVar):
if not self.tmp_locations:
raise generic.ScriptError("There are not enough registers available " +
"to perform all required computations in switch blocks. " +
"Please reduce the complexity of your code.", self.pos)
location = self.tmp_locations[0]
self.remove_tmp_location(location, False)
var.set_register(location)
def prepare_output(self, sprite_num):
action2.Action2.prepare_output(self, sprite_num)
for i in range(0, len(self.var_list) - 1, 2):
self.var_list[i].shift |= 0x20
for i in range(0, len(self.var_list), 2):
if isinstance(self.var_list[i], VarAction2ProcCallVar):
self.var_list[i].resolve_parameter(self.feature)
for r in self.ranges:
if isinstance(r.result, expression.SpriteGroupRef):
r.result = r.result.get_action2_id(self.feature)
else:
r.result = r.result.value | 0x8000
if isinstance(self.default_result, expression.SpriteGroupRef):
self.default_result = self.default_result.get_action2_id(self.feature)
else:
self.default_result = self.default_result.value | 0x8000
def write(self, file):
#type_byte, num_ranges, default_result = 4
#2 bytes for the result, 8 bytes for the min/max range.
size = 4 + (2 + 8) * len(self.ranges)
for var in self.var_list:
if isinstance(var, nmlop.Operator):
size += 1
else:
size += var.get_size()
self.write_sprite_start(file, size)
file.print_bytex(self.type_byte)
file.newline()
for var in self.var_list:
if isinstance(var, nmlop.Operator):
file.print_bytex(var.act2_num, var.act2_str)
else:
var.write(file, 4)
file.newline(var.comment)
file.print_byte(len(self.ranges))
file.newline()
for r in self.ranges:
file.print_wordx(r.result)
file.print_varx(r.min.value, 4)
file.print_varx(r.max.value, 4)
file.newline(r.comment)
file.print_wordx(self.default_result)
file.comment(self.default_comment)
file.end_sprite()
class VarAction2Var(object):
"""
Represents a variable for use in a (advanced) variational action2.
@ivar var_num: Number of the variable to use.
@type var_num: C{int}
@ivar shift: The number of bits to shift the value of the given variable to the right.
@type shift: C{int}
@ivar mask: Bitmask to use on the value after shifting it.
@type mask: C{int}
@ivar parameter: Parameter to be used as argument for the variable.
@type parameter: C{int} or C{None}
@precondition: (0x60 <= var_num <= 0x7F) == (parameter is not None)
@ivar add: If not C{None}, add this value to the result.
@type add: C{int} or C{None}
@ivar div: If not C{None}, divide the result by this.
@type add: C{int} or C{None}
@ivar mod: If not C{None}, compute (result module mod).
@type add: C{int} or C{None}
@ivar comment: Textual description of this variable.
@type comment: C{basestr}
"""
def __init__(self, var_num, shift, mask, parameter = None):
self.var_num = var_num
self.shift = shift
self.mask = mask
self.parameter = parameter
self.add = None
self.div = None
self.mod = None
self.comment = ""
def write(self, file, size):
file.print_bytex(self.var_num)
if self.parameter is not None:
file.print_bytex(self.parameter)
if self.mod is not None:
self.shift |= 0x80
elif self.add is not None or self.div is not None:
self.shift |= 0x40
file.print_bytex(self.shift)
file.print_varx(self.mask, size)
if self.add is not None:
file.print_varx(self.add, size)
if self.div is not None:
file.print_varx(self.div, size)
elif self.mod is not None:
file.print_varx(self.mod, size)
else:
#no div or add, just divide by 1
file.print_varx(1, size)
def get_size(self):
#var number (1) [+ parameter (1)] + shift num (1) + and mask (4) [+ add (4) + div/mod (4)]
size = 6
if self.parameter is not None: size += 1
if self.add is not None or self.div is not None or self.mod is not None: size += 8
return size
def supported_by_actionD(self, raise_error):
assert not raise_error
return False
# Class for var 7E procedure calls
class VarAction2ProcCallVar(VarAction2Var):
def __init__(self, sg_ref):
VarAction2Var.__init__(self, 0x7E, 0, 0)
# Reference to the called action2
self.sg_ref = sg_ref
def resolve_parameter(self, feature):
self.parameter = self.sg_ref.get_action2_id(feature)
def get_size(self):
return 7
def write(self, file, size):
self.mask = get_mask(size)
VarAction2Var.write(self, file, size)
# General load and store class for temp parameters
# Register is allocated at the store operation
class VarAction2StoreTempVar(VarAction2Var):
def __init__(self):
VarAction2Var.__init__(self, 0x1A, 0, 0)
#mask holds the number, it's resolved in Action2Var.resolve_tmp_storage
self.load_vars = []
def set_register(self, register):
self.mask = register
for load_var in self.load_vars:
load_var.parameter = register
def get_size(self):
return 6
def get_mask(size):
if size == 1: return 0xFF
elif size == 2: return 0xFFFF
return 0xFFFFFFFF
class VarAction2LoadTempVar(VarAction2Var, expression.Expression):
def __init__(self, tmp_var):
VarAction2Var.__init__(self, 0x7D, 0, 0)
expression.Expression.__init__(self, None)
assert isinstance(tmp_var, VarAction2StoreTempVar)
tmp_var.load_vars.append(self)
def write(self, file, size):
self.mask = get_mask(size)
VarAction2Var.write(self, file, size)
def get_size(self):
return 7
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def supported_by_action2(self, raise_error):
return True
def supported_by_actionD(self, raise_error):
assert not raise_error
return False
# Temporary load and store classes used for spritelayout parameters
# Register is allocated in a separate entity
class VarAction2LayoutParam(object):
def __init__(self):
self.register = None
self.store_vars = []
self.load_vars = []
def set_register(self, register):
self.register = register
for store_var in self.store_vars:
store_var.mask = register
for load_var in self.load_vars:
load_var.parameter = register
class VarAction2LoadLayoutParam(VarAction2Var, expression.Expression):
def __init__(self, param):
VarAction2Var.__init__(self, 0x7D, 0, 0)
expression.Expression.__init__(self, None)
assert isinstance(param, VarAction2LayoutParam)
param.load_vars.append(self)
# Register is stored in parameter
def write(self, file, size):
self.mask = get_mask(size)
VarAction2Var.write(self, file, size)
def get_size(self):
return 7
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def supported_by_action2(self, raise_error):
return True
def supported_by_actionD(self, raise_error):
assert not raise_error
return False
class VarAction2StoreLayoutParam(VarAction2Var):
def __init__(self, param):
VarAction2Var.__init__(self, 0x1A, 0, 0)
assert isinstance(param, VarAction2LayoutParam)
param.store_vars.append(self)
# Register is stored in mask
def get_size(self):
return 6
class VarAction2Range(object):
def __init__(self, min, max, result, comment):
self.min = min
self.max = max
self.result = result
self.comment = comment
class Modification(object):
def __init__(self, param, size, offset):
self.param = param
self.size = size
self.offset = offset
class Varaction2Parser(object):
def __init__(self, feature):
self.feature = feature # Depends on feature and var_range
self.extra_actions = []
self.mods = []
self.var_list = []
self.var_list_size = 0
self.proc_call_list = []
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 = expression.BinOp(nmlop.VACT2_CMP, expr.expr1, expr.expr2)
#reduce the problem to 0/1
expr = expression.BinOp(nmlop.MIN, expr, expression.ConstantNumeric(1))
#and invert the result
expr = expression.BinOp(nmlop.XOR, expr, expression.ConstantNumeric(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 = expression.BinOp(nmlop.VACT2_CMP, expr.expr1, expr.expr2)
#subtract one
expr = expression.BinOp(nmlop.SUB, expr, expression.ConstantNumeric(1))
#map -1 and 0 to 0
expr = expression.BinOp(nmlop.MAX, expr, expression.ConstantNumeric(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 = expression.BinOp(nmlop.VACT2_CMP, expr.expr1, expr.expr2)
#swap 0 and 2
expr = expression.BinOp(nmlop.XOR, expr, expression.ConstantNumeric(2))
#map 1/2 to 1
expr = expression.BinOp(nmlop.MIN, expr, expression.ConstantNumeric(1))
elif expr.op == nmlop.CMP_GE:
#return value is 0, 1 or 2, we want to map 1/2 to 1
expr = expression.BinOp(nmlop.VACT2_CMP, expr.expr1, expr.expr2)
expr = expression.BinOp(nmlop.MIN, expr, expression.ConstantNumeric(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 = expression.BinOp(nmlop.VACT2_CMP, expr.expr1, expr.expr2)
expr = expression.BinOp(nmlop.AND, expr, expression.ConstantNumeric(1))
elif expr.op == nmlop.CMP_NEQ:
#same as CMP_EQ but invert the result
expr = expression.BinOp(nmlop.VACT2_CMP, expr.expr1, expr.expr2)
expr = expression.BinOp(nmlop.AND, expr, expression.ConstantNumeric(1))
expr = expression.BinOp(nmlop.XOR, expr, expression.ConstantNumeric(1))
elif expr.op == nmlop.HASBIT:
# hasbit(x, n) ==> (x >> n) & 1
expr = expression.BinOp(nmlop.SHIFTU_RIGHT, expr.expr1, expr.expr2)
expr = expression.BinOp(nmlop.AND, expr, expression.ConstantNumeric(1))
elif expr.op == nmlop.NOTHASBIT:
# !hasbit(x, n) ==> ((x >> n) & 1) ^ 1
expr = expression.BinOp(nmlop.SHIFTU_RIGHT, expr.expr1, expr.expr2)
expr = expression.BinOp(nmlop.AND, expr, expression.ConstantNumeric(1))
expr = expression.BinOp(nmlop.XOR, expr, expression.ConstantNumeric(1))
return expr.reduce()
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 = expression.BinOp(nmlop.MUL, expr.expr1, VarAction2LoadTempVar(guard_var))
expr2 = expression.BinOp(nmlop.MUL, expr.expr2, VarAction2LoadTempVar(inverted_guard_var))
return expression.BinOp(nmlop.ADD, expr1, expr2)
def preprocess_storageop(self, expr):
assert isinstance(expr, expression.StorageOp)
max = 0xF if expr.info['perm'] else 0x10F
if isinstance(expr.register, expression.ConstantNumeric) and expr.register.value > max:
raise generic.ScriptError("Register number must be in range 0..{:d}, encountered {:d}.".format(max, expr.register.value), expr.pos)
if expr.info['perm'] and self.feature not in (0x08, 0x0A, 0x0D):
raise generic.ScriptError("Persistent storage is not supported for feature '{}'".format(general.feature_name(self.feature)), expr.pos)
if expr.info['store']:
op = nmlop.STO_PERM if expr.info['perm'] else nmlop.STO_TMP
ret = expression.BinOp(op, expr.value, expr.register, expr.pos)
else:
var_num = 0x7C if expr.info['perm'] else 0x7D
ret = expression.Variable(expression.ConstantNumeric(var_num), param=expr.register, pos=expr.pos)
if expr.info['perm'] and self.feature == 0x08:
# store grfid in register 0x100 for town persistent storage
grfid = expression.ConstantNumeric(0xFFFFFFFF if expr.grfid is None else expression.parse_string_to_dword(expr.grfid))
store_op = expression.BinOp(nmlop.STO_TMP, grfid, expression.ConstantNumeric(0x100), expr.pos)
ret = expression.BinOp(nmlop.VAL2, store_op, ret, expr.pos)
elif expr.grfid is not None:
raise generic.ScriptError("Specifying a grfid is only possible for town persistent storage.", expr.pos)
return ret
def parse_expr_to_constant(self, expr, offset):
if isinstance(expr, expression.ConstantNumeric): return expr.value
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(expr)
self.extra_actions.extend(tmp_param_actions)
self.mods.append(Modification(tmp_param, 4, self.var_list_size + offset))
return 0
def parse_variable(self, expr):
"""
Parse a variable in an expression.
@param expr:
@type expr: L{expression.Variable}
"""
if not isinstance(expr.num, expression.ConstantNumeric):
raise generic.ScriptError("Variable number must be a constant number", expr.num.pos)
if not (expr.param is None or isinstance(expr.param, expression.ConstantNumeric)):
raise generic.ScriptError("Variable parameter must be a constant number", expr.param.pos)
if len(expr.extra_params) > 0:
first_var = len(self.var_list) == 0
backup_op = None
value_backup = None
if not first_var:
backup_op = self.var_list.pop()
value_backup = VarAction2StoreTempVar()
self.var_list.append(nmlop.STO_TMP)
self.var_list.append(value_backup)
self.var_list.append(nmlop.VAL2)
self.var_list_size += value_backup.get_size() + 1
#Last value == 0, and this is right before we're going to use
#the extra parameters. Set them to their correct value here.
for extra_param in expr.extra_params:
self.parse(extra_param[1])
self.var_list.append(nmlop.STO_TMP)
var = VarAction2Var(0x1A, 0, extra_param[0])
self.var_list.append(var)
self.var_list.append(nmlop.VAL2)
self.var_list_size += var.get_size() + 2
if not first_var:
value_loadback = VarAction2LoadTempVar(value_backup)
self.var_list.append(value_loadback)
self.var_list.append(backup_op)
self.var_list_size += value_loadback.get_size() + 1
if expr.param is None:
offset = 2
param = None
else:
offset = 3
param = expr.param.value
mask = self.parse_expr_to_constant(expr.mask, offset)
var = VarAction2Var(expr.num.value, expr.shift.value, mask, param)
if expr.add is not None:
var.add = self.parse_expr_to_constant(expr.add, offset + 4)
if expr.div is not None:
var.div = self.parse_expr_to_constant(expr.div, offset + 8)
if expr.mod is not None:
var.mod = self.parse_expr_to_constant(expr.mod, offset + 8)
self.var_list.append(var)
self.var_list_size += var.get_size()
def parse_not(self, expr):
self.parse_binop(expression.BinOp(nmlop.XOR, expr.expr, expression.ConstantNumeric(1)))
def parse_binop(self, expr):
if expr.op.act2_num is None: expr.supported_by_action2(True)
if isinstance(expr.expr2, (expression.ConstantNumeric, expression.Variable)) or \
isinstance(expr.expr2, (VarAction2LoadTempVar, VarAction2LoadLayoutParam)) or \
(isinstance(expr.expr2, expression.Parameter) and isinstance(expr.expr2.num, expression.ConstantNumeric)) or \
expr.op == nmlop.VAL2:
expr2 = expr.expr2
elif expr.expr2.supported_by_actionD(False):
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(expr.expr2)
self.extra_actions.extend(tmp_param_actions)
expr2 = expression.Parameter(expression.ConstantNumeric(tmp_param))
else:
#The expression is so complex we need to compute it first, store the
#result and load it back later.
self.parse(expr.expr2)
tmp_var = VarAction2StoreTempVar()
self.var_list.append(nmlop.STO_TMP)
self.var_list.append(tmp_var)
self.var_list.append(nmlop.VAL2)
#the +2 is for both operators
self.var_list_size += tmp_var.get_size() + 2
expr2 = VarAction2LoadTempVar(tmp_var)
#parse expr1
self.parse(expr.expr1)
self.var_list.append(expr.op)
self.var_list_size += 1
self.parse(expr2)
def parse_constant(self, expr):
var = VarAction2Var(0x1A, 0, expr.value)
self.var_list.append(var)
self.var_list_size += var.get_size()
def parse_param(self, expr):
self.mods.append(Modification(expr.num.value, 4, self.var_list_size + 2))
var = VarAction2Var(0x1A, 0, 0)
var.comment = str(expr)
self.var_list.append(var)
self.var_list_size += var.get_size()
def parse_string(self, expr):
str_id, actions = action4.get_string_action4s(0, 0xD0, expr)
self.extra_actions.extend(actions)
self.parse_constant(expression.ConstantNumeric(str_id))
def parse_via_actionD(self, expr):
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(expr)
self.extra_actions.extend(tmp_param_actions)
num = expression.ConstantNumeric(tmp_param)
self.parse(expression.Parameter(num))
def parse_proc_call(self, expr):
assert isinstance(expr, expression.SpriteGroupRef)
var_access = VarAction2ProcCallVar(expr)
self.var_list.append(var_access)
self.var_list_size += var_access.get_size()
self.proc_call_list.append(expr)
def parse_expr(self, expr):
if isinstance(expr, expression.Array):
for expr2 in expr.values:
self.parse(expr2)
self.var_list.append(nmlop.VAL2)
self.var_list_size += 1
# Drop the trailing VAL2 again
self.var_list.pop()
self.var_list_size -= 1
else:
self.parse(expr)
def parse(self, expr):
#Preprocess the expression
if isinstance(expr, expression.SpecialParameter):
#do this first, since it may evaluate to a BinOp
expr = expr.to_reading()
if isinstance(expr, expression.BinOp):
expr = self.preprocess_binop(expr)
elif isinstance(expr, expression.Boolean):
expr = expression.BinOp(nmlop.MINU, expr.expr, expression.ConstantNumeric(1))
elif isinstance(expr, expression.BinNot):
expr = expression.BinOp(nmlop.XOR, expr.expr, expression.ConstantNumeric(0xFFFFFFFF))
elif isinstance(expr, expression.TernaryOp) and not expr.supported_by_actionD(False):
expr = self.preprocess_ternaryop(expr)
elif isinstance(expr, expression.StorageOp):
expr = self.preprocess_storageop(expr)
#Try to parse the expression to a list of variables+operators
if isinstance(expr, expression.ConstantNumeric):
self.parse_constant(expr)
elif isinstance(expr, expression.Parameter) and isinstance(expr.num, expression.ConstantNumeric):
self.parse_param(expr)
elif isinstance(expr, expression.Variable):
self.parse_variable(expr)
elif expr.supported_by_actionD(False):
self.parse_via_actionD(expr)
elif isinstance(expr, expression.BinOp):
self.parse_binop(expr)
elif isinstance(expr, expression.Not):
self.parse_not(expr)
elif isinstance(expr, expression.String):
self.parse_string(expr)
elif isinstance(expr, (VarAction2LoadTempVar, VarAction2LoadLayoutParam)):
self.var_list.append(expr)
self.var_list_size += expr.get_size()
else:
expr.supported_by_action2(True)
assert False #supported_by_action2 should have raised the correct error already
def parse_var(info, pos):
param = expression.ConstantNumeric(info['param']) if 'param' in info else None
res = expression.Variable(expression.ConstantNumeric(info['var']), expression.ConstantNumeric(info['start']),
expression.ConstantNumeric((1 << info['size']) - 1), param, pos)
if 'value_function' in info:
return info['value_function'](res, info)
return res
def parse_60x_var(name, args, pos, info):
if 'param_function' in info:
# Special function to extract parameters if there is more than one
param, extra_params = info['param_function'](name, args, pos, info)
else:
# Default function to extract parameters
param, extra_params = action2var_variables.default_60xvar(name, args, pos, info)
if isinstance(param, expression.ConstantNumeric) and (0 <= param.value <= 255):
res = expression.Variable(expression.ConstantNumeric(info['var']), expression.ConstantNumeric(info['start']), \
expression.ConstantNumeric((1 << info['size']) - 1), param, pos)
res.extra_params.extend(extra_params)
else:
# Make use of var 7B to pass non-constant parameters
var = expression.Variable(expression.ConstantNumeric(0x7B), expression.ConstantNumeric(info['start']), \
expression.ConstantNumeric((1 << info['size']) - 1), expression.ConstantNumeric(info['var']), pos)
var.extra_params.extend(extra_params)
# Set the param in the accumulator beforehand
res = expression.BinOp(nmlop.VAL2, param, var, pos)
if 'value_function' in info:
res = info['value_function'](res, info)
return res
def parse_minmax(value, unit_str, action_list, act6, offset):
"""
Parse a min or max value in a switch block.
@param value: Value to parse
@type value: L{Expression}
@param unit_str: Unit to use
@type unit_str: C{str} or C{None}
@param action_list: List to append any extra actions to
@type action_list: C{list} of L{BaseAction}
@param act6: Action6 to add any modifications to
@type act6: L{Action6}
@param offset: Current offset to use for action6
@type offset: C{int}
@return: A tuple of two values:
- The value to use as min/max
- Whether the resulting range may need a sanity check
@rtype: C{tuple} of (L{ConstantNumeric} or L{SpriteGroupRef}), C{bool}
"""
if unit_str is not None:
raise generic.ScriptError("Using a unit is in switch-ranges is not (temporarily) not supported", value.pos)
result, offset = actionD.write_action_value(value, action_list, act6, offset, 4)
check_range = isinstance(value, expression.ConstantNumeric)
return (result, offset, check_range)
return_action_id = 0
def create_return_action(expr, feature, name, var_range):
"""
Create a varaction2 to return the computed value
@param expr: Expression to return
@type expr: L{Expression}
@param feature: Feature of the switch-block
@type feature: C{int}
@param name: Name of the new varaction2
@type name: C{str}
@return: A tuple of two values:
- Action list to prepend
- Reference to the created varaction2
@rtype: C{tuple} of (C{list} of L{BaseAction}, L{SpriteGroupRef})
"""
varact2parser = Varaction2Parser(feature if var_range == 0x89 else action2var_variables.varact2parent_scope[feature])
varact2parser.parse_expr(expr)
action_list = varact2parser.extra_actions
extra_act6 = action6.Action6()
for mod in varact2parser.mods:
extra_act6.modify_bytes(mod.param, mod.size, mod.offset + 4)
if len(extra_act6.modifications) > 0: action_list.append(extra_act6)
varaction2 = Action2Var(feature, name, expr.pos, var_range)
varaction2.var_list = varact2parser.var_list
varaction2.default_result = expression.ConstantNumeric(0) # Bogus result, it's the nvar == 0 that matters
varaction2.default_comment = 'Return computed value'
ref = expression.SpriteGroupRef(expression.Identifier(name), [], None, varaction2)
action_list.append(varaction2)
return (action_list, ref)
failed_cb_results = {}
def get_failed_cb_result(feature, action_list, parent_action, pos):
"""
Get a sprite group reference to use for a failed callback
The actions needed are created on first use, then cached in L{failed_cb_results}
@param feature: Feature to use
@type feature: C{int}
@param action_list: Action list to append any extra actions to
@type action_list: C{list} of L{BaseAction}
@param parent_action: Reference to the action of which this is a result
@type parent_action: L{BaseAction}
@param pos: Positional context.
@type pos: L{Position}
@return: Sprite group reference to use
@rtype: L{SpriteGroupRef}
"""
if feature in failed_cb_results:
varaction2 = failed_cb_results[feature]
else:
# Create action2 (+ action1, if needed)
# Import here to avoid circular imports
from nml.actions import action1, action2layout, action2production, action2real
if feature == 0x0A:
# Industries -> production action2
act2 = action2production.make_empty_production_action2(pos)
elif feature in (0x07, 0x09, 0x0F, 0x11):
# Tile layout action2
act2 = action2layout.make_empty_layout_action2(feature, pos)
else:
# Normal action2
act1_actions, act1_index = action1.make_cb_failure_action1(feature)
action_list.extend(act1_actions)
act2 = action2real.make_simple_real_action2(feature, "@CB_FAILED_REAL{:02X}".format(feature), pos, act1_index)
action_list.append(act2)
# Create varaction2, to choose between returning graphics and 0, depending on CB
varact2parser = Varaction2Parser(feature)
varact2parser.parse_expr(expression.Variable(expression.ConstantNumeric(0x0C), mask=expression.ConstantNumeric(0xFFFF)))
varaction2 = Action2Var(feature, "@CB_FAILED{:02X}".format(feature), pos, 0x89)
varaction2.var_list = varact2parser.var_list
varaction2.ranges.append(VarAction2Range(expression.ConstantNumeric(0), expression.ConstantNumeric(0), expression.ConstantNumeric(0), "graphics callback -> return 0"))
varaction2.default_result = expression.SpriteGroupRef(expression.Identifier(act2.name), [], None, act2)
varaction2.default_comment = "Non-graphics callback, return graphics result"
action2.add_ref(varaction2.default_result, varaction2)
action_list.append(varaction2)
failed_cb_results[feature] = varaction2
ref = expression.SpriteGroupRef(expression.Identifier(varaction2.name), [], None, varaction2)
action2.add_ref(ref, parent_action)
return ref
def parse_sg_ref_result(result, action_list, parent_action, var_range):
"""
Parse a result that is a sprite group reference.
@param result: Result to parse
@type result: L{SpriteGroupRef}
@param action_list: List to append any extra actions to
@type action_list: C{list} of L{BaseAction}
@param parent_action: Reference to the action of which this is a result
@type parent_action: L{BaseAction}
@param var_range: Variable range to use for variables in the expression
@type var_range: C{int}
@return: Result to use in the calling varaction2
@rtype: L{SpriteGroupRef}
"""
if result.name.value == "CB_FAILED":
return get_failed_cb_result(parent_action.feature, action_list, parent_action, result.pos)
if len(result.param_list) == 0:
action2.add_ref(result, parent_action)
return result
# Result is parametrized
# Insert an intermediate varaction2 to store expressions in registers
var_feature = parent_action.feature if var_range == 0x89 else action2var_variables.varact2parent_scope[parent_action.feature]
varact2parser = Varaction2Parser(var_feature)
layout = action2.resolve_spritegroup(result.name)
for i, param in enumerate(result.param_list):
if i > 0:
varact2parser.var_list.append(nmlop.VAL2)
varact2parser.var_list_size += 1
varact2parser.parse_expr(reduce_varaction2_expr(param, var_feature))
varact2parser.var_list.append(nmlop.STO_TMP)
store_tmp = VarAction2StoreLayoutParam(layout.register_map[parent_action.feature][i])
varact2parser.var_list.append(store_tmp)
varact2parser.var_list_size += store_tmp.get_size() + 1 # Add 1 for operator
action_list.extend(varact2parser.extra_actions)
extra_act6 = action6.Action6()
for mod in varact2parser.mods:
extra_act6.modify_bytes(mod.param, mod.size, mod.offset + 4)
if len(extra_act6.modifications) > 0: action_list.append(extra_act6)
global return_action_id
name = "@return_action_{:d}".format(return_action_id)
varaction2 = Action2Var(parent_action.feature, name, result.pos, var_range)
return_action_id += 1
varaction2.var_list = varact2parser.var_list
ref = expression.SpriteGroupRef(result.name, [], result.pos)
varaction2.ranges.append(VarAction2Range(expression.ConstantNumeric(0), expression.ConstantNumeric(0), ref, result.name.value))
varaction2.default_result = ref
varaction2.default_comment = result.name.value
# Add the references as procs, to make sure, that any intermediate registers
# are freed at the spritelayout and thus not selected to pass parameters
# Reference is used twice (range + default) so call add_ref twice
action2.add_ref(ref, varaction2, True)
action2.add_ref(ref, varaction2, True)
ref = expression.SpriteGroupRef(expression.Identifier(name), [], None, varaction2)
action_list.append(varaction2)
action2.add_ref(ref, parent_action)
return ref
def parse_result(value, action_list, act6, offset, parent_action, none_result, var_range, repeat_result = 1):
"""
Parse a result (another switch or CB result) in a switch block.
@param value: Value to parse
@type value: L{Expression}
@param action_list: List to append any extra actions to
@type action_list: C{list} of L{BaseAction}
@param act6: Action6 to add any modifications to
@type act6: L{Action6}
@param offset: Current offset to use for action6
@type offset: C{int}
@param parent_action: Reference to the action of which this is a result
@type parent_action: L{BaseAction}
@param none_result: Result to use to return the computed value
@type none_result: L{Expression}
@param var_range: Variable range to use for variables in the expression
@type var_range: C{int}
@param repeat_result: Repeat any action6 modifying of the next sprite this many times.
@type repeat_result: C{int}
@return: A tuple of two values:
- The value to use as return value
- Comment to add to this value
@rtype: C{tuple} of (L{ConstantNumeric} or L{SpriteGroupRef}), C{str}
"""
if value is None:
comment = "return;"
assert none_result is not None
if isinstance(none_result, expression.SpriteGroupRef):
result = parse_sg_ref_result(none_result, action_list, parent_action, var_range)
else:
result = none_result
elif isinstance(value, expression.SpriteGroupRef):
result = parse_sg_ref_result(value, action_list, parent_action, var_range)
comment = result.name.value + ';'
elif isinstance(value, expression.ConstantNumeric):
comment = "return {:d};".format(value.value)
result = value
if not(-16384 <= value.value <= 32767):
msg = "Callback results are limited to -16384..16383 (when the result is a signed number) or 0..32767 (unsigned), encountered {:d}."
msg = msg.format(value.value)
raise generic.ScriptError(msg, value.pos)
elif isinstance(value, expression.String):
comment = "return {};".format(str(value))
str_id, actions = action4.get_string_action4s(0, 0xD0, value)
action_list.extend(actions)
result = expression.ConstantNumeric(str_id - 0xD000 + 0x8000)
elif value.supported_by_actionD(False):
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(expression.BinOp(nmlop.OR, value, expression.ConstantNumeric(0x8000)).reduce())
comment = "return param[{:d}];".format(tmp_param)
action_list.extend(tmp_param_actions)
for i in range(repeat_result):
act6.modify_bytes(tmp_param, 2, offset + 2*i)
result = expression.ConstantNumeric(0)
else:
global return_action_id
extra_actions, result = create_return_action(value, parent_action.feature, "@return_action_{:d}".format(return_action_id), var_range)
return_action_id += 1
action2.add_ref(result, parent_action)
action_list.extend(extra_actions)
comment = "return {}".format(value)
return (result, comment)
def get_feature(switch_block):
feature = next(iter(switch_block.feature_set))
if switch_block.var_range == 0x8A:
feature = action2var_variables.varact2parent_scope[feature]
if feature is None:
raise generic.ScriptError("Parent scope for this feature not available, feature: " + str(feature), switch_block.pos)
return feature
def reduce_varaction2_expr(expr, feature, extra_dicts = []):
# 'normal' and 60+x variables to use
vars_normal = action2var_variables.varact2vars[feature]
vars_60x = action2var_variables.varact2vars60x[feature]
# lambda function to convert (value, pos) to a function pointer
# since we need the variable name later on, a reverse lookup is needed
# TODO pass the function name along to avoid this
func60x = lambda value, pos: expression.FunctionPtr(expression.Identifier(generic.reverse_lookup(vars_60x, value), pos), parse_60x_var, value)
return expr.reduce(extra_dicts + [(action2var_variables.varact2_globalvars, parse_var), \
(vars_normal, parse_var), \
(vars_60x, func60x)] + \
global_constants.const_list)
def parse_varaction2(switch_block):
global return_action_id
return_action_id = 0
action6.free_parameters.save()
act6 = action6.Action6()
action_list = action2real.create_spriteset_actions(switch_block)
feature = next(iter(switch_block.feature_set))
varaction2 = Action2Var(feature, switch_block.name.value, switch_block.pos, switch_block.var_range)
expr = reduce_varaction2_expr(switch_block.expr, get_feature(switch_block))
offset = 4 #first var
parser = Varaction2Parser(get_feature(switch_block))
parser.parse_expr(expr)
action_list.extend(parser.extra_actions)
for mod in parser.mods:
act6.modify_bytes(mod.param, mod.size, mod.offset + offset)
varaction2.var_list = parser.var_list
offset += parser.var_list_size + 1 # +1 for the byte num-ranges
none_result = None
if any(x is not None and x.value is None for x in [r.result for r in switch_block.body.ranges] + [switch_block.body.default]):
# Computed result is returned in at least one result
if len(switch_block.body.ranges) == 0:
# There is only a default, which is 'return computed result', so we're fine
none_result = expression.ConstantNumeric(0) # Return value does not matter
else:
# Add an extra action to return the computed value
extra_actions, none_result = create_return_action(expression.Variable(expression.ConstantNumeric(0x1C)),
feature, switch_block.name.value + "@return", 0x89)
action_list.extend(extra_actions)
used_ranges = []
for r in switch_block.body.ranges:
comment = str(r.min) + " .. " + str(r.max) + ": "
range_result, range_comment = parse_result(r.result.value, action_list, act6, offset, varaction2, none_result, switch_block.var_range)
comment += range_comment
offset += 2 # size of result
range_min, offset, check_min = parse_minmax(r.min, r.unit, action_list, act6, offset)
range_max, offset, check_max = parse_minmax(r.max, r.unit, action_list, act6, offset)
range_overlap = False
if check_min and check_max:
for existing_range in used_ranges:
if existing_range[0] <= range_min.value and range_max.value <= existing_range[1]:
generic.print_warning("Range overlaps with existing ranges so it'll never be reached", r.min.pos)
range_overlap = True
break
if not range_overlap:
used_ranges.append([range_min.value, range_max.value])
used_ranges.sort()
i = 0
while i + 1 < len(used_ranges):
if used_ranges[i + 1][0] <= used_ranges[i][1] + 1:
used_ranges[i][1] = max(used_ranges[i][1], used_ranges[i + 1][1])
used_ranges.pop(i + 1)
else:
i += 1
if not range_overlap:
varaction2.ranges.append(VarAction2Range(range_min, range_max, range_result, comment))
if len(switch_block.body.ranges) == 0 and \
(switch_block.body.default is None or switch_block.body.default.value is not None):
# Computed result is not returned, but there are no ranges
# Add one range, to avoid the nvar == 0 bear trap
offset += 10
varaction2.ranges.append(VarAction2Range(expression.ConstantNumeric(1), expression.ConstantNumeric(0),
expression.ConstantNumeric(0), "Bogus range to avoid nvar == 0"))
# Handle default result
if switch_block.body.default is not None:
# there is a default value
default_result = switch_block.body.default.value
else:
# Default to CB_FAILED
default_result = expression.SpriteGroupRef(expression.Identifier('CB_FAILED', None), [], None)
default, default_comment = parse_result(default_result, action_list, act6, offset, varaction2, none_result, switch_block.var_range)
varaction2.default_result = default
if switch_block.body.default is None:
varaction2.default_comment = 'No default specified -> fail callback'
elif switch_block.body.default.value is None:
varaction2.default_comment = 'Return computed value'
else:
varaction2.default_comment = 'default: ' + default_comment
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(varaction2)
switch_block.set_action2(varaction2, feature)
action6.free_parameters.restore()
return action_list
nml-0.4.5/nml/actions/action6.py 0000644 0005672 0005672 00000003637 13315644406 017632 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import free_number_list, generic
from nml.actions import base_action
free_parameters = free_number_list.FreeNumberList(list(range(0x40, 0x80)), "No free parameters available to use for internal computations.", "No unique free parameters available for internal computations.")
def print_stats():
"""
Print statistics about used ids.
"""
if free_parameters.stats[0] > 0:
generic.print_info("Concurrent ActionD registers: {}/{} ({})".format(free_parameters.stats[0], free_parameters.total_amount, str(free_parameters.stats[1])))
class Action6(base_action.BaseAction):
def __init__(self):
self.modifications = []
def modify_bytes(self, param, num_bytes, offset):
self.modifications.append( (param, num_bytes, offset) )
def write(self, file):
size = 2 + 5 * len(self.modifications)
file.start_sprite(size)
file.print_bytex(6)
file.newline()
for mod in self.modifications:
file.print_bytex(mod[0])
file.print_bytex(mod[1])
file.print_bytex(0xFF)
file.print_wordx(mod[2])
file.newline()
file.print_bytex(0xFF)
file.newline()
file.end_sprite()
def skip_action7(self):
return False
nml-0.4.5/nml/actions/action2production.py 0000644 0005672 0005672 00000013403 13315644406 021725 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions import action2, action2var, action6, actionD
from nml import expression, nmlop
class Action2Production(action2.Action2):
"""
Class corresponding to Action2Industries (=production CB)
@ivar version: Production CB version. Version 0 uses constants, version 1 uses registers.
@type version: C{int}
@ivar sub_in: Amounts (v0) or registers (v1) to subtract from incoming cargos.
@type sub_in: C{list} of (C{int} or L{VarAction2Var})
@ivar add_out: Amounts (v0) or registers (v1) to add to the output cargos.
@type add_out: C{list} of (C{int} or L{VarAction2Var})
@ivar again: Number (v0) or register (v1), production CB will be run again if nonzero.
@type again C{int} or L{VarAction2Var}
"""
def __init__(self, name, pos, version, sub_in, add_out, again):
action2.Action2.__init__(self, 0x0A, name, pos)
self.version = version
assert version == 0 or version == 1
self.sub_in = sub_in
assert len(self.sub_in) == 3
self.add_out = add_out
assert len(self.add_out) == 2
self.again = again
def prepare_output(self, sprite_num):
action2.Action2.prepare_output(self, sprite_num)
def write(self, file):
cargo_size = 2 if self.version == 0 else 1
size = 2 + 5 * cargo_size
action2.Action2.write_sprite_start(self, file, size)
file.print_bytex(self.version)
values = self.sub_in + self.add_out + [self.again]
# Read register numbers if needed
if self.version == 1: values = [val.parameter for val in values]
for val in values[:-1]:
file.print_varx(val, cargo_size)
file.print_bytex(values[-1])
file.newline()
file.end_sprite()
def get_production_actions(produce):
"""
Get the action list that implements the given produce-block in nfo.
@param produce: Produce-block to parse.
@type produce: L{Produce}
"""
action_list = []
act6 = action6.Action6()
action6.free_parameters.save()
result_list = []
varact2parser = action2var.Varaction2Parser(0x0A)
if all(x.supported_by_actionD(False) for x in produce.param_list):
version = 0
offset = 4
for i, param in enumerate(produce.param_list):
result, offset = actionD.write_action_value(param, action_list, act6, offset, 2 if i < 5 else 1)
result_list.append(result.value)
else:
version = 1
for i, param in enumerate(produce.param_list):
if isinstance(param, expression.StorageOp) and param.name == 'LOAD_TEMP' and \
isinstance(param.register, expression.ConstantNumeric):
# We can load a register directly
result_list.append(action2var.VarAction2Var(0x7D, 0, 0xFFFFFFFF, param.register.value))
else:
if len(varact2parser.var_list) != 0:
varact2parser.var_list.append(nmlop.VAL2)
varact2parser.var_list_size += 1
varact2parser.parse_expr(action2var.reduce_varaction2_expr(param, 0x0A))
store_tmp = action2var.VarAction2StoreTempVar()
result_list.append(action2var.VarAction2LoadTempVar(store_tmp))
varact2parser.var_list.append(nmlop.STO_TMP)
varact2parser.var_list.append(store_tmp)
varact2parser.var_list_size += store_tmp.get_size() + 1 # Add 1 for operator
if len(act6.modifications) > 0: action_list.append(act6)
prod_action = Action2Production(produce.name.value, produce.pos, version, result_list[0:3], result_list[3:5], result_list[5])
action_list.append(prod_action)
if len(varact2parser.var_list) == 0:
produce.set_action2(prod_action, 0x0A)
else:
# Create intermediate varaction2
varaction2 = action2var.Action2Var(0x0A, '{}@registers'.format(produce.name.value), produce.pos, 0x89)
varaction2.var_list = varact2parser.var_list
action_list.extend(varact2parser.extra_actions)
extra_act6 = action6.Action6()
for mod in varact2parser.mods:
extra_act6.modify_bytes(mod.param, mod.size, mod.offset + 4)
if len(extra_act6.modifications) > 0: action_list.append(extra_act6)
ref = expression.SpriteGroupRef(produce.name, [], None, prod_action)
varaction2.ranges.append(action2var.VarAction2Range(expression.ConstantNumeric(0), expression.ConstantNumeric(0), ref, ''))
varaction2.default_result = ref
varaction2.default_comment = ''
# Add two references (default + range)
action2.add_ref(ref, varaction2)
action2.add_ref(ref, varaction2)
produce.set_action2(varaction2, 0x0A)
action_list.append(varaction2)
action6.free_parameters.restore()
return action_list
def make_empty_production_action2(pos):
"""
Make an empty production action2
For use with failed callbacks
@param pos: Positional context.
@type pos: L{Position}
@return: The created production action2
@rtype: L{Action2Production}
"""
return Action2Production("@CB_FAILED_PROD", pos, 0, [0, 0, 0], [0, 0], 0)
nml-0.4.5/nml/actions/actionB.py 0000644 0005672 0005672 00000010127 13315644406 017636 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, generic, grfstrings
from nml.actions import base_action, action6, actionD
class ActionB(base_action.BaseAction):
def __init__(self, severity, lang, msg, data, extra_params):
self.severity = severity
self.lang = lang
self.msg = msg
self.data = data
self.extra_params = extra_params
def write(self, file):
size = 4
if not isinstance(self.msg, int): size += grfstrings.get_string_size(self.msg)
if self.data is not None:
size += grfstrings.get_string_size(self.data) + len(self.extra_params)
file.start_sprite(size)
file.print_bytex(0x0B)
self.severity.write(file, 1)
file.print_bytex(self.lang)
if isinstance(self.msg, int):
file.print_bytex(self.msg)
else:
file.print_bytex(0xFF)
file.print_string(self.msg)
if self.data is not None:
file.print_string(self.data)
for param in self.extra_params:
param.write(file, 1)
file.newline()
file.end_sprite()
def skip_action7(self):
return False
default_error_msg = {
'REQUIRES_TTDPATCH' : 0,
'REQUIRES_DOS_WINDOWS' : 1,
'USED_WITH' : 2,
'INVALID_PARAMETER' : 3,
'MUST_LOAD_BEFORE' : 4,
'MUST_LOAD_AFTER' : 5,
'REQUIRES_OPENTTD' : 6,
}
error_severity = {
'NOTICE' : 0,
'WARNING' : 1,
'ERROR' : 2,
'FATAL' : 3,
}
def parse_error_block(error):
action6.free_parameters.save()
action_list = []
act6 = action6.Action6()
severity = actionD.write_action_value(error.severity, action_list, act6, 1, 1)[0]
langs = [0x7F]
if isinstance(error.msg, expression.String):
custom_msg = True
msg_string = error.msg
grfstrings.validate_string(msg_string)
langs.extend(grfstrings.get_translations(msg_string))
for l in langs: assert l is not None
else:
custom_msg = False
msg = error.msg.reduce_constant().value
if error.data is not None:
error.data = error.data.reduce()
if isinstance(error.data, expression.String):
grfstrings.validate_string(error.data)
langs.extend(grfstrings.get_translations(error.data))
for l in langs: assert l is not None
elif not isinstance(error.data, expression.StringLiteral):
raise generic.ScriptError("Error parameter 3 'data' should be the identifier of a custom sting", error.data.pos)
params = []
for expr in error.params:
if isinstance(expr, expression.Parameter) and isinstance(expr.num, expression.ConstantNumeric):
params.append(expr.num)
else:
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(expr)
action_list.extend(tmp_param_actions)
params.append(expression.ConstantNumeric(tmp_param))
langs = list(set(langs))
langs.sort()
for lang in langs:
if custom_msg:
msg = grfstrings.get_translation(msg_string, lang)
if error.data is None:
data = None
elif isinstance(error.data, expression.StringLiteral):
data = error.data.value
else:
data = grfstrings.get_translation(error.data, lang)
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(ActionB(severity, lang, msg, data, params))
action6.free_parameters.restore()
return action_list
nml-0.4.5/nml/actions/action0properties.py 0000644 0005672 0005672 00000143740 13315644406 021741 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import itertools
from nml import generic, nmlop
from nml.expression import BinOp, ConstantNumeric, ConstantFloat, Array, StringLiteral, Identifier
tilelayout_names = {}
class BaseAction0Property(object):
"""
Base class for Action0 properties.
"""
def write(self, file):
"""
Write this property to the output given by file.
@param file: The outputfile we have to write to.
@type file: L{SpriteOutputBase}
"""
raise NotImplementedError('write is not implemented in {!r}'.format(type(self)))
def get_size(self):
"""
Get the number of bytes that this property will write to
the output.
@return: The size of this property in bytes.
@rtype: C{int}
"""
raise NotImplementedError('get_size is not implemented in {!r}'.format(type(self)))
class Action0Property(BaseAction0Property):
"""
Simple Action 0 property with a fixed size.
@ivar num: Number of the property.
@type num: C{int}
@ivar values: Value of the property for each id.
@type values: C{list} of L{ConstantNumeric}
@ivar size: Size of the storage, in bytes.
@type size: C{int}
"""
def __init__(self, num, value, size):
self.num = num
self.values = value if isinstance(value, list) else [value]
self.size = size
# Make sure the value fits in the size.
# Strings have their own check in parse_property
for val in self.values:
if not isinstance(val, StringLiteral):
biggest = 1 << (8 * size)
if val.value >= biggest:
raise generic.ScriptError("Action 0 property too large", val.pos)
elif val.value < 0 and val.value + (biggest // 2) < 0:
raise generic.ScriptError("Action 0 property too small", val.pos)
def write(self, file):
file.print_bytex(self.num)
for val in self.values:
val.write(file, self.size)
file.newline()
def get_size(self):
return self.size * len(self.values) + 1
# @var properties: A mapping of features to properties. This is a list
# with one item per feature. Entries should be a dictionary of properties,
# or C{None} if no properties are defined for that feature.
#
# Each property is a mapping of property name to its characteristics.
# These characteristics are either a dictionary or a list of
# dictionaries, the latter can be used to set multiple properties.
# First a short summary is given, then the recognized characteristics
# are outlined below in more detail.
#
# Summary: If 'string' or 'string_literal' is set, the value should be a
# string or literal string, else the value is a number. 'unit_type' and
# 'unit_conversion' are used to convert user-entered values to nfo values.
# If some more arithmetic is needed to convert the entered value into an nfo
# value, 'value_function' can be used. For even more complicated things,
# 'custom_function' can be used to create a special mapping of the value to nfo
# properties, else 'num' and 'size' are used to provide a 'normal' action0.
#
# 'string', if set, means that the value of the property should be a string.
# The value of characteristic indicates the string range to use (usually 0xD0 or 0xDC)
# If set to None, the string will use the ID of the item (used for vehicle names)
#
# 'string_literal', if set, indicates that the value of the property should
# be a literal (quoted) string. The value of the characteristic is equal to
# the required length (usually 4) of said literal string.
#
# 'unit_type' means that units of the given type (power, speed) can be applied
# to this property. A list of units can be found in ../unit.py. The value is then
# converted to a certain reference unit (for example m/s for speed)
# Leaving this unset means that no units (except 'nfo' which is an identity mapping)
# can be applied to this property.
#
# 'unit_conversion' defines a conversion factor between the value entered by
# the user and the resulting value in nfo. This is either an integer or a
# rational number entered as a 2-tuple (numerator, denominator).
# The entered value (possibly converted # to the appropriate reference unit,
# see 'unit_type' above) is multiplied by this factor and then rounded to an
# integer to provide the final value.
# This parameter is not required and defaults to 1.
#
# 'value_function' can be used to alter the mapping of nml values to nfo values
# it takes one argument (the value) and returns the new value to use
# Both the parameter and the return value are expressions that do not have to be
# constants
#
# 'custom_function' can be used to bypass the normal way of converting the
# name / value to an Action0Property. This function is normally called with one
# argument, which is the value of the property. For houses, there may be multiple
# values, passed as a vararg-list. It should return a list of Action0Property.
# To pass extra parameters to the function, a dose of lambda calculus can be used.
# Consult the code for examples.
#
# 'test_function' can be used to determine if the property should be set in the
# first place. It normally takes one argument (the value) and should return True
# if the property is to be set, False if it is to be ignored. Default is True
# For houses, there may be multiple values, passed as a vararg-list.
#
# 'num' is the Action0 property number of the action 0 property, as given by the
# nfo specs. If set to -1, no action0 property will be generated. If
# 'custom_function' is set, this value is not needed and can be left out.
#
# 'size' is the size (in bytes) of the resulting action 0 property. Valid
# values are 1 (byte), 2 (word) or 4 (dword). For other (or variable) sizes,
# 'custom_function' is needed. If 'custom_function' is set or 'num' is equal
# to -1, this parameter is not needed and can be left out.
#
# 'warning' is a string (optional) containing a warning message that will be
# shown if a property is used. Use for deprecating properties.
#
# 'first' (value doesn't matter) if the property should be set first (generally a substitute type)
properties = 0x12 * [None]
#
# Some helper functions that are used for multiple features
#
def two_byte_property(low_prop, high_prop, low_prop_info = {}, high_prop_info = {}):
"""
Decode a two byte value into two action 0 properties.
@param low_prop: Property number for the low 8 bits of the value.
@type low_prop: C{int}
@param high_prop: Property number for the high 8 bits of the value.
@type high_prop: C{int}
@param low_prop_info: Dictionary with additional property information for the low byte.
@type low_prop_info: C{dict}
@param high_prop_info: Dictionary with additional property information for the low byte.
@type high_prop_info: C{dict}
@return: Sequence of two dictionaries with property information (low part, high part).
@rtype: C{list} of C{dict}
"""
low_byte_info = {'num': low_prop, 'size': 1, 'value_function': lambda value: BinOp(nmlop.AND, value, ConstantNumeric(0xFF, value.pos), value.pos).reduce()}
high_byte_info = {'num': high_prop, 'size': 1, 'value_function': lambda value: BinOp(nmlop.SHIFT_RIGHT, value, ConstantNumeric(8, value.pos), value.pos).reduce()}
low_byte_info.update(low_prop_info)
high_byte_info.update(high_prop_info)
return [low_byte_info, high_byte_info]
def animation_info(value, loop_bit=8, max_frame=253):
"""
Convert animation info array of two elements to an animation info property.
The first is 0/1, and defines whether or not the animation loops. The second is the number of frames, at most 253 frames.
@param value: Array of animation info.
@type value: C{Array}
@param loop_bit: Bit the loop information is stored.
@type loop_bit: C{int}
@param max_frame: Max frames possible.
@type max_frame: C{int}
@return: Value to use for animation property.
@rtype: L{Expression}
"""
if not isinstance(value, Array) or len(value.values) != 2:
raise generic.ScriptError("animation_info must be an array with exactly 2 constant values", value.pos)
looping = value.values[0].reduce_constant().value
frames = value.values[1].reduce_constant().value
if looping not in (0, 1):
raise generic.ScriptError("First field of the animation_info array must be either 0 or 1", value.values[0].pos)
if frames < 1 or frames > max_frame:
raise generic.ScriptError("Second field of the animation_info array must be between 1 and " + str(max_frame), value.values[1].pos)
return ConstantNumeric((looping << loop_bit) + frames - 1)
def cargo_list(value, max_num_cargos):
"""
Encode an array of cargo types in a single property value. If less than the maximum
number of cargos are given the rest is filled up with 0xFF (=invalid cargo).
@param value: Array of cargo types.
@type value: C{Array}
@param max_num_cargos: The maximum number of cargos in the array.
@type max_num_cargos: C{int}
@param prop_num: Property number.
@type prop_num: C{int}
@param prop_size: Property size in bytes.
@type prop_size: C{int}
"""
if not isinstance(value, Array) or len(value.values) > max_num_cargos:
raise generic.ScriptError("Cargo list must be an array with no more than {:d} values".format(max_num_cargos), value.pos)
cargoes = value.values + [ConstantNumeric(0xFF, value.pos) for _ in range(max_num_cargos - len(value.values))]
ret = None
for i, cargo in enumerate(cargoes):
byte = BinOp(nmlop.AND, cargo, ConstantNumeric(0xFF, cargo.pos), cargo.pos)
if i == 0:
ret = byte
else:
byte = BinOp(nmlop.SHIFT_LEFT, byte, ConstantNumeric(i * 8, cargo.pos), cargo.pos)
ret = BinOp(nmlop.OR, ret, byte, cargo.pos)
return ret.reduce()
#
# General vehicle properties that apply to feature 0x00 .. 0x03
#
general_veh_props = {
'reliability_decay' : {'size': 1, 'num': 0x02},
'vehicle_life' : {'size': 1, 'num': 0x03},
'model_life' : {'size': 1, 'num': 0x04},
'climates_available' : {'size': 1, 'num': 0x06},
'loading_speed' : {'size': 1, 'num': 0x07},
'name' : {'num': -1, 'string': None},
}
def ottd_display_speed(value, divisor, unit):
return int(value.value / divisor) * 10 // 16 * unit.ottd_mul >> unit.ottd_shift
class CargotypeListProp(BaseAction0Property):
def __init__(self, prop_num, data):
# data is a list, each element belongs to an item ID
# Each element in the list is a list of cargo types
self.prop_num = prop_num
self.data = data
def write(self, file):
file.print_bytex(self.prop_num)
for elem in self.data:
file.print_byte(len(elem))
for i, val in enumerate(elem):
if i % 8 == 0: file.newline()
file.print_bytex(val)
file.newline()
def get_size(self):
total_len = 1 # Prop number
for elem in self.data:
# For each item ID to set, make space for all values + 1 for the length
total_len += len(elem) + 1
return total_len
def ctt_list(prop_num, *values):
# values may have multiple entries, if more than one item ID is set (e.g. multitile houses)
# Each value is an expression.Array of cargo types
for value in values:
if not isinstance(value, Array):
raise generic.ScriptError("Value of cargolist property must be an array", value.pos)
return [CargotypeListProp(prop_num, [[ctype.reduce_constant().value for ctype in single_item_array.values] for single_item_array in values])]
def vehicle_length(value):
if isinstance(value, ConstantNumeric):
generic.check_range(value.value, 1, 8, "vehicle length", value.pos)
return BinOp(nmlop.SUB, ConstantNumeric(8, value.pos), value, value.pos).reduce()
def zero_refit_mask(prop_num):
# Zero the refit mask, in addition to setting some other refit property
return {'size': 4, 'num': prop_num, 'value_function': lambda value: ConstantNumeric(0)}
#
# Feature 0x00 (Trains)
#
properties[0x00] = {
'track_type' : {'size': 1, 'num': 0x05},
'ai_special_flag' : {'size': 1, 'num': 0x08},
'speed' : {'size': 2, 'num': 0x09, 'unit_type': 'speed', 'unit_conversion': (5000, 1397), 'adjust_value': lambda val, unit: ottd_display_speed(val, 1, unit)},
# 09 doesn't exist
'power' : {'size': 2, 'num': 0x0B, 'unit_type': 'power'},
# 0A doesn't exist
'running_cost_factor' : {'size': 1, 'num': 0x0D},
'running_cost_base' : {'size': 4, 'num': 0x0E},
# 0F -11 don't exist
'sprite_id' : {'size': 1, 'num': 0x12},
'dual_headed' : {'size': 1, 'num': 0x13},
'cargo_capacity' : {'size': 1, 'num': 0x14},
'default_cargo_type' : {'size': 1, 'num': 0x15},
'weight' : two_byte_property(0x16, 0x24, {'unit_type': 'weight'}, {'unit_type': 'weight'}),
'cost_factor' : {'size': 1, 'num': 0x17},
'ai_engine_rank' : {'size': 1, 'num': 0x18},
'engine_class' : {'size': 1, 'num': 0x19},
# 1A (sort purchase list) is implemented elsewhere
'extra_power_per_wagon' : {'size': 2, 'num': 0x1B, 'unit_type': 'power'},
'refit_cost' : {'size': 1, 'num': 0x1C},
# 1D (refittable cargo types) is removed, it is zeroed when setting a different refit property
# 1E (callback flags) is not set by user
'tractive_effort_coefficient' : {'size': 1, 'num': 0x1F, 'unit_conversion': 255},
'air_drag_coefficient' : {'size': 1, 'num': 0x20, 'unit_conversion': 255},
'length' : {'size': 1, 'num': 0x21, 'value_function': vehicle_length},
# 22 has two names, to simplify docs
'visual_effect_and_powered' : {'size': 1, 'num': 0x22},
'effect_spawn_model_and_powered': {'size': 1, 'num': 0x22},
'extra_weight_per_wagon' : {'size': 1, 'num': 0x23, 'unit_type': 'weight'},
# 24 is high byte of 16 (weight)
'bitmask_vehicle_info' : {'size': 1, 'num': 0x25},
'retire_early' : {'size': 1, 'num': 0x26},
'misc_flags' : {'size': 1, 'num': 0x27},
'refittable_cargo_classes' : [{'size': 2, 'num': 0x28}, zero_refit_mask(0x1D)],
'non_refittable_cargo_classes' : [{'size': 2, 'num': 0x29}, zero_refit_mask(0x1D)],
'introduction_date' : {'size': 4, 'num': 0x2A},
'cargo_age_period' : {'size': 2, 'num': 0x2B},
'cargo_allow_refit' : [{'custom_function': lambda value: ctt_list(0x2C, value)}, zero_refit_mask(0x1D)],
'cargo_disallow_refit' : [{'custom_function': lambda value: ctt_list(0x2D, value)}, zero_refit_mask(0x1D)],
}
properties[0x00].update(general_veh_props)
#
# Feature 0x01 (Road Vehicles)
#
def roadveh_speed_prop(prop_info):
# prop 08 value is min(value, 255)
prop08_value = lambda value: BinOp(nmlop.MIN, value, ConstantNumeric(0xFF, value.pos), value.pos).reduce()
# prop 15 value is (value + 3) / 4
prop15_value = lambda value: BinOp(nmlop.DIV, BinOp(nmlop.ADD, value, ConstantNumeric(3, value.pos), value.pos), ConstantNumeric(4, value.pos), value.pos).reduce()
# prop 15 should not be set if value(prop08_value) <= 255. But as we test prop15 and prop15 = 0.25/prop08, test for 64:
prop15_test = lambda value: isinstance(value, ConstantNumeric) and value.value >= 0x40
prop08 = {'size': 1, 'num': 0x08, 'value_function': prop08_value}
prop15 = {'size': 1, 'num': 0x15, 'value_function': prop15_value, 'test_function': prop15_test}
for key in prop_info:
prop08[key] = prop15[key] = prop_info[key]
return [prop08, prop15]
properties[0x01] = {
'speed' : roadveh_speed_prop({'unit_type': 'speed', 'unit_conversion': (10000, 1397), 'adjust_value': lambda val, unit: ottd_display_speed(val, 2, unit)}),
'running_cost_factor' : {'size': 1, 'num': 0x09},
'running_cost_base' : {'size': 4, 'num': 0x0A},
# 0B -0D don't exist
'sprite_id' : {'size': 1, 'num': 0x0E},
'cargo_capacity' : {'size': 1, 'num': 0x0F},
'default_cargo_type' : {'size': 1, 'num': 0x10},
'cost_factor' : {'size': 1, 'num': 0x11},
'sound_effect' : {'size': 1, 'num': 0x12},
'power' : {'size': 1, 'num': 0x13, 'unit_type': 'power', 'unit_conversion': (1, 10)},
'weight' : {'size': 1, 'num': 0x14, 'unit_type': 'weight', 'unit_conversion': 4},
# 15 is set together with 08 (see above)
# 16 (refittable cargo types) is removed, it is zeroed when setting a different refit property
# 17 (callback flags) is not set by user
'tractive_effort_coefficient' : {'size': 1, 'num': 0x18, 'unit_conversion': 255},
'air_drag_coefficient' : {'size': 1, 'num': 0x19, 'unit_conversion': 255},
'refit_cost' : {'size': 1, 'num': 0x1A},
'retire_early' : {'size': 1, 'num': 0x1B},
'misc_flags' : {'size': 1, 'num': 0x1C},
'refittable_cargo_classes' : [{'size': 2, 'num': 0x1D}, zero_refit_mask(0x16)],
'non_refittable_cargo_classes' : [{'size': 2, 'num': 0x1E}, zero_refit_mask(0x16)],
'introduction_date' : {'size': 4, 'num': 0x1F},
# 20 (sort purchase list) is implemented elsewhere
# 21 has two names, to simplify docs
'visual_effect' : {'size': 1, 'num': 0x21},
'effect_spawn_model' : {'size': 1, 'num': 0x21},
'cargo_age_period' : {'size': 2, 'num': 0x22},
'length' : {'size': 1, 'num': 0x23, 'value_function': vehicle_length},
'cargo_allow_refit' : [{'custom_function': lambda value: ctt_list(0x24, value)}, zero_refit_mask(0x16)],
'cargo_disallow_refit' : [{'custom_function': lambda value: ctt_list(0x25, value)}, zero_refit_mask(0x16)],
}
properties[0x01].update(general_veh_props)
#
# Feature 0x02 (Ships)
#
def speed_fraction(value):
# Unit is already converted to 0 .. 255 range when we get here
if isinstance(value, ConstantNumeric) and not (0 <= value.value <= 255):
# Do not use check_range to provide better error message
raise generic.ScriptError("speed fraction must be in range 0 .. 1", value.pos)
return BinOp(nmlop.SUB, ConstantNumeric(255, value.pos), value, value.pos).reduce()
properties[0x02] = {
'sprite_id' : {'size': 1, 'num': 0x08},
'is_refittable' : {'size': 1, 'num': 0x09},
'cost_factor' : {'size': 1, 'num': 0x0A},
'speed' : {'size': 1, 'num': 0x0B, 'unit_type': 'speed', 'unit_conversion': (10000, 1397), 'adjust_value': lambda val, unit: ottd_display_speed(val, 2, unit)},
'default_cargo_type' : {'size': 1, 'num': 0x0C},
'cargo_capacity' : {'size': 2, 'num': 0x0D},
# 0E does not exist
'running_cost_factor' : {'size': 1, 'num': 0x0F},
'sound_effect' : {'size': 1, 'num': 0x10},
# 11 (refittable cargo types) is removed, it is zeroed when setting a different refit property
# 12 (callback flags) is not set by user
'refit_cost' : {'size': 1, 'num': 0x13},
'ocean_speed_fraction' : {'size': 1, 'num': 0x14, 'unit_conversion': 255, 'value_function': speed_fraction},
'canal_speed_fraction' : {'size': 1, 'num': 0x15, 'unit_conversion': 255, 'value_function': speed_fraction},
'retire_early' : {'size': 1, 'num': 0x16},
'misc_flags' : {'size': 1, 'num': 0x17},
'refittable_cargo_classes' : [{'size': 2, 'num': 0x18}, zero_refit_mask(0x11)],
'non_refittable_cargo_classes' : [{'size': 2, 'num': 0x19}, zero_refit_mask(0x11)],
'introduction_date' : {'size': 4, 'num': 0x1A},
# 1B (sort purchase list) is implemented elsewhere
# 1C has two names, to simplify docs
'visual_effect' : {'size': 1, 'num': 0x1C},
'effect_spawn_model' : {'size': 1, 'num': 0x1C},
'cargo_age_period' : {'size': 2, 'num': 0x1D},
'cargo_allow_refit' : [{'custom_function': lambda value: ctt_list(0x1E, value)}, zero_refit_mask(0x11)],
'cargo_disallow_refit' : [{'custom_function': lambda value: ctt_list(0x1F, value)}, zero_refit_mask(0x11)],
}
properties[0x02].update(general_veh_props)
#
# Feature 0x03 (Aircraft)
#
def aircraft_is_heli(value):
if isinstance(value, ConstantNumeric) and not value.value in (0, 2, 3):
raise generic.ScriptError("Invalid value for aircraft_type", value.pos)
return BinOp(nmlop.AND, value, ConstantNumeric(2, value.pos), value.pos).reduce()
def aircraft_is_large(value):
return BinOp(nmlop.AND, value, ConstantNumeric(1, value.pos), value.pos).reduce()
properties[0x03] = {
'sprite_id' : {'size': 1, 'num': 0x08},
'aircraft_type' : [{'size': 1, 'num': 0x09, 'value_function': aircraft_is_heli}, {'size': 1, 'num': 0x0A, 'value_function': aircraft_is_large}],
'cost_factor' : {'size': 1, 'num': 0x0B},
'speed' : {'size': 1, 'num': 0x0C, 'unit_type': 'speed', 'unit_conversion': (701, 2507), 'adjust_value': lambda val, unit: ottd_display_speed(val, 1 / (8 * 1.6), unit)},
'acceleration' : {'size': 1, 'num': 0x0D},
'running_cost_factor' : {'size': 1, 'num': 0x0E},
'passenger_capacity' : {'size': 2, 'num': 0x0F},
# 10 does not exist
'mail_capacity' : {'size': 1, 'num': 0x11},
'sound_effect' : {'size': 1, 'num': 0x12},
# 13 (refittable cargo types) is removed, it is zeroed when setting a different refit property
# 14 (callback flags) is not set by user
'refit_cost' : {'size': 1, 'num': 0x15},
'retire_early' : {'size': 1, 'num': 0x16},
'misc_flags' : {'size': 1, 'num': 0x17},
'refittable_cargo_classes' : [{'size': 2, 'num': 0x18}, zero_refit_mask(0x13)],
'non_refittable_cargo_classes' : [{'size': 2, 'num': 0x19}, zero_refit_mask(0x13)],
'introduction_date' : {'size': 4, 'num': 0x1A},
# 1B (sort purchase list) is implemented elsewhere
'cargo_age_period' : {'size': 2, 'num': 0x1C},
'cargo_allow_refit' : [{'custom_function': lambda value: ctt_list(0x1D, value)}, zero_refit_mask(0x13)],
'cargo_disallow_refit' : [{'custom_function': lambda value: ctt_list(0x1E, value)}, zero_refit_mask(0x13)],
'range' : {'size': 2, 'num': 0x1F},
}
properties[0x03].update(general_veh_props)
# TODO: Feature 0x04
#
# Feature 0x05 (Canals)
#
properties[0x05] = {
# 08 (callback flags) not set by user
'graphic_flags' : {'size': 1, 'num': 0x09},
}
# TODO: Feature 0x06
#
# Feature 0x07 (Houses)
#
def house_prop_0A(value):
# User sets an array [min_year, max_year] as property value
# House property 0A is set to ((max_year - 1920) << 8) | (min_year - 1920)
# With both bytes clamped to the 0 .. 255 range
if not isinstance(value, Array) or len(value.values) != 2:
raise generic.ScriptError("Availability years must be an array with exactly two values", value.pos)
min_year = BinOp(nmlop.SUB, value.values[0], ConstantNumeric(1920, value.pos), value.pos)
min_year = BinOp(nmlop.MAX, min_year, ConstantNumeric(0, value.pos), value.pos)
min_year = BinOp(nmlop.MIN, min_year, ConstantNumeric(255, value.pos), value.pos)
max_year = BinOp(nmlop.SUB, value.values[1], ConstantNumeric(1920, value.pos), value.pos)
max_year = BinOp(nmlop.MAX, max_year, ConstantNumeric(0, value.pos), value.pos)
max_year = BinOp(nmlop.MIN, max_year, ConstantNumeric(255, value.pos), value.pos)
max_year = BinOp(nmlop.SHIFT_LEFT, max_year, ConstantNumeric(8, value.pos), value.pos)
return BinOp(nmlop.OR, max_year, min_year, value.pos).reduce()
def house_prop_21_22(value, index):
# Take one of the values from the years_available array
if not isinstance(value, Array) or len(value.values) != 2:
raise generic.ScriptError("Availability years must be an array with exactly two values", value.pos)
return value.values[index]
def house_acceptance(value, index):
if not isinstance(value, Array) or len(value.values) > 3:
raise generic.ScriptError("accepted_cargos must be an array with no more than 3 values", value.pos)
if index < len(value.values):
cargo_amount_pair = value.values[index]
if not isinstance(cargo_amount_pair, Array) or len(cargo_amount_pair.values) != 2:
raise generic.ScriptError("Each element of accepted_cargos must be an array with two elements: cargoid and amount", cargo_amount_pair.pos)
return cargo_amount_pair.values[1]
else:
return ConstantNumeric(0, value.pos)
def house_accepted_cargo_types(value):
if not isinstance(value, Array) or len(value.values) > 3:
raise generic.ScriptError("accepted_cargos must be an array with no more than 3 values", value.pos)
cargoes = []
for i in range(3):
if i < len(value.values):
cargo_amount_pair = value.values[i]
if not isinstance(cargo_amount_pair, Array) or len(cargo_amount_pair.values) != 2:
raise generic.ScriptError("Each element of accepted_cargos must be an array with two elements: cargoid and amount", cargo_amount_pair.pos)
cargoes.append(cargo_amount_pair.values[0])
else:
cargoes.append(ConstantNumeric(0xFF, value.pos))
ret = None
for i, cargo in enumerate(cargoes):
byte = BinOp(nmlop.AND, cargo, ConstantNumeric(0xFF, cargo.pos), cargo.pos)
if i == 0:
ret = byte
else:
byte = BinOp(nmlop.SHIFT_LEFT, byte, ConstantNumeric(i * 8, cargo.pos), cargo.pos)
ret = BinOp(nmlop.OR, ret, byte, cargo.pos)
return ret.reduce()
def house_random_colours(value):
# User sets array with 4 values (range 0..15)
# Output is a dword, each byte being a value from the array
if not isinstance(value, Array) or len(value.values) != 4:
raise generic.ScriptError("Random colours must be an array with exactly four values", value.pos)
ret = None
for i, colour in enumerate(value.values):
if isinstance(colour, ConstantNumeric):
generic.check_range(colour.value, 0, 15, "Random house colours", colour.pos)
byte = BinOp(nmlop.AND, colour, ConstantNumeric(0xFF, colour.pos), colour.pos)
if i == 0:
ret = byte
else:
byte = BinOp(nmlop.SHIFT_LEFT, byte, ConstantNumeric(i * 8, colour.pos), colour.pos)
ret = BinOp(nmlop.OR, ret, byte, colour.pos)
return ret.reduce()
def house_available_mask(value):
# User sets [town_zones, climates] array
# Which is mapped to (town_zones | (climates & 0x800) | ((climates & 0xF) << 12))
if not isinstance(value, Array) or len(value.values) != 2:
raise generic.ScriptError("availability_mask must be an array with exactly 2 values", value.pos)
climates = BinOp(nmlop.AND, value.values[1], ConstantNumeric(0xF, value.pos), value.pos)
climates = BinOp(nmlop.SHIFT_LEFT, climates, ConstantNumeric(12, value.pos), value.pos)
above_snow = BinOp(nmlop.AND, value.values[1], ConstantNumeric(0x800, value.pos), value.pos)
ret = BinOp(nmlop.OR, climates, value.values[0], value.pos)
ret = BinOp(nmlop.OR, ret, above_snow, value.pos)
return ret.reduce()
# List of valid IDs of old house types
old_houses = {
0 : set(), # 1x1, see below
2 : set([74, 76, 87]), # 2x1
3 : set([7, 66, 68, 99]), # 1x2
4 : set([20, 32, 40]), # 2x2
}
# All houses not part of a multitile-house, are 1x1 houses
old_houses[0] = set(range(110)).difference( house + i for house in (itertools.chain(*list(old_houses.values()))) for i in range(4 if house in old_houses[4] else 2) )
def mt_house_old_id(value, num_ids, size_bit):
# For substitute / override properties
# Set value for tile i (0 .. 3) to (value + i)
# Also validate that the size of the old house matches
if isinstance(value, ConstantNumeric) and not value.value in old_houses[size_bit]:
raise generic.ScriptError("Substitute / override house type must have the same size as the newly defined house.", value.pos)
ret = [value]
for i in range(1, num_ids):
ret.append(BinOp(nmlop.ADD, value, ConstantNumeric(i, value.pos), value.pos).reduce())
return ret
def mt_house_prop09(value, num_ids, size_bit):
# Only bit 5 should be set for additional tiles
# Additionally, correctly set the size bit (0, 2, 3 or 4) for the first tile
if isinstance(value, ConstantNumeric) and (value.value & 0x1D) != 0:
raise generic.ScriptError("Invalid bits set in house property 'building_flags'.", value.pos)
ret = [BinOp(nmlop.OR, value, ConstantNumeric(1 << size_bit, value.pos), value.pos).reduce()]
for _i in range(1, num_ids):
ret.append(BinOp(nmlop.AND, value, ConstantNumeric(1 << 5, value.pos), value.pos).reduce())
return ret
def mt_house_mask(mask, value, num_ids, size_bit):
# Mask out the bits not present in the 'mask' parameter for additional tiles
ret = [value]
for _i in range(1, num_ids):
ret.append(BinOp(nmlop.AND, value, ConstantNumeric(mask, value.pos), value.pos).reduce())
return ret
def mt_house_zero(value, num_ids, size_bit):
return [value] + (num_ids - 1) * [ConstantNumeric(0, value.pos)]
def mt_house_same(value, num_ids, size_bit):
# Set to the same value for all tiles
return num_ids * [value]
def mt_house_class(value, num_ids, size_bit):
# Set class to 0xFF for additional tiles
return [value] + (num_ids - 1) * [ConstantNumeric(0xFF, value.pos)]
properties[0x07] = {
'substitute' : {'size': 1, 'num': 0x08, 'multitile_function': mt_house_old_id, 'first': None},
'building_flags' : two_byte_property(0x09, 0x19, {'multitile_function': mt_house_prop09}, {'multitile_function': lambda *args: mt_house_mask(0xFE, *args)}),
'years_available' : [{'size': 2, 'num': 0x0A, 'multitile_function': mt_house_zero, 'value_function': house_prop_0A},
{'size': 2, 'num': 0x21, 'multitile_function': mt_house_zero, 'value_function': lambda value: house_prop_21_22(value, 0)},
{'size': 2, 'num': 0x22, 'multitile_function': mt_house_zero, 'value_function': lambda value: house_prop_21_22(value, 1)}],
'population' : {'size': 1, 'num': 0x0B, 'multitile_function': mt_house_zero},
'mail_multiplier' : {'size': 1, 'num': 0x0C, 'multitile_function': mt_house_zero},
'accepted_cargos' : [{'size': 1, 'num': 0x0D, 'multitile_function': mt_house_same, 'value_function': lambda value: house_acceptance(value, 0)},
{'size': 1, 'num': 0x0E, 'multitile_function': mt_house_same, 'value_function': lambda value: house_acceptance(value, 1)},
{'size': 1, 'num': 0x0F, 'multitile_function': mt_house_same, 'value_function': lambda value: house_acceptance(value, 2)},
{'size': 4, 'num': 0x1E, 'multitile_function': mt_house_same, 'value_function': house_accepted_cargo_types}],
'local_authority_impact' : {'size': 2, 'num': 0x10, 'multitile_function': mt_house_same},
'removal_cost_multiplier' : {'size': 1, 'num': 0x11, 'multitile_function': mt_house_same},
'name' : {'size': 2, 'num': 0x12, 'string': 0xDC, 'multitile_function': mt_house_same},
'availability_mask' : {'size': 2, 'num': 0x13, 'multitile_function': mt_house_zero, 'value_function': house_available_mask},
# prop 14 (callback flags 1) is not set by user
'override' : {'size': 1, 'num': 0x15, 'multitile_function': mt_house_old_id},
'refresh_multiplier' : {'size': 1, 'num': 0x16, 'multitile_function': mt_house_same},
'random_colours' : {'size': 4, 'num': 0x17, 'multitile_function': mt_house_same, 'value_function': house_random_colours},
'probability' : {'size': 1, 'num': 0x18, 'multitile_function': mt_house_zero, 'unit_conversion': 16},
# prop 19 is the high byte of prop 09
'animation_info' : {'size': 1, 'num': 0x1A, 'multitile_function': mt_house_same, 'value_function': lambda value: animation_info(value, 7, 128)},
'animation_speed' : {'size': 1, 'num': 0x1B, 'multitile_function': mt_house_same},
'building_class' : {'size': 1, 'num': 0x1C, 'multitile_function': mt_house_class},
# prop 1D (callback flags 2) is not set by user
'minimum_lifetime' : {'size': 1, 'num': 0x1F, 'multitile_function': mt_house_zero},
'watched_cargo_types' : {'multitile_function': mt_house_same, 'custom_function': lambda *values: ctt_list(0x20, *values)}
# prop 21 -22 see above (years_available, prop 0A)
}
# Feature 0x08 (General Vars) is implemented elsewhere (e.g. basecost, snowline)
#
# Feature 0x09 (Industry Tiles)
#
def industrytile_cargos(value):
if not isinstance(value, Array) or len(value.values) > 3:
raise generic.ScriptError("accepted_cargos must be an array with no more than 3 values", value.pos)
prop_num = 0x0A
props = []
for cargo_amount_pair in value.values:
if not isinstance(cargo_amount_pair, Array) or len(cargo_amount_pair.values) != 2:
raise generic.ScriptError("Each element of accepted_cargos must be an array with two elements: cargoid and amount", cargo_amount_pair.pos)
cargo_id = cargo_amount_pair.values[0].reduce_constant().value
cargo_amount = cargo_amount_pair.values[1].reduce_constant().value
props.append(Action0Property(prop_num, ConstantNumeric((cargo_amount << 8) | cargo_id), 2))
prop_num += 1
while prop_num <= 0x0C:
props.append(Action0Property(prop_num, ConstantNumeric(0), 2))
prop_num += 1
return props
properties[0x09] = {
'substitute' : {'size': 1, 'num': 0x08, 'first': None},
'override' : {'size': 1, 'num': 0x09},
'accepted_cargos' : {'custom_function': industrytile_cargos}, # = prop 0A - 0C
'land_shape_flags' : {'size': 1, 'num': 0x0D},
# prop 0E (callback flags) is not set by user
'animation_info' : {'size': 2, 'num': 0x0F, 'value_function': animation_info},
'animation_speed' : {'size': 1, 'num': 0x10},
'animation_triggers' : {'size': 1, 'num': 0x11},
'special_flags' : {'size': 1, 'num': 0x12},
}
#
# Feature 0x0A (Industries)
#
class IndustryLayoutProp(BaseAction0Property):
def __init__(self, layout_list):
self.layout_list = layout_list
def write(self, file):
file.print_bytex(0x0A)
file.print_byte(len(self.layout_list))
# -6 because prop_num, num_layouts and size should not be included
file.print_dword(self.get_size() - 6)
file.newline()
for layout in self.layout_list:
layout.write(file)
file.newline()
def get_size(self):
size = 6
for layout in self.layout_list:
size += layout.get_size()
return size
def industry_layouts(value):
if not isinstance(value, Array) or not all(isinstance(x, Identifier) for x in value.values):
raise generic.ScriptError("layouts must be an array of layout names", value.pos)
layouts = []
for name in value.values:
if name.value not in tilelayout_names:
raise generic.ScriptError("Unknown layout name '{}'".format(name.value), name.pos)
layouts.append(tilelayout_names[name.value])
return [IndustryLayoutProp(layouts)]
def industry_prod_multiplier(value):
if not isinstance(value, Array) or len(value.values) > 2:
raise generic.ScriptError("Prod multiplier must be an array of up to two values", value.pos)
props = []
for i in range(0, 2):
val = value.values[i].reduce_constant() if i < len(value.values) else ConstantNumeric(0)
props.append(Action0Property(0x12 + i, val, 1))
return props
class RandomSoundsProp(BaseAction0Property):
def __init__(self, sound_list):
self.sound_list = sound_list
def write(self, file):
file.print_bytex(0x15)
file.print_byte(len(self.sound_list))
for sound in self.sound_list:
sound.write(file, 1)
file.newline()
def get_size(self):
return len(self.sound_list) + 2
def random_sounds(value):
if not isinstance(value, Array) or not all(isinstance(x, ConstantNumeric) for x in value.values):
raise generic.ScriptError("random_sound_effects must be an array with sounds effects", value.pos)
return [RandomSoundsProp(value.values)]
class ConflictingTypesProp(BaseAction0Property):
def __init__(self, types_list):
self.types_list = types_list
assert len(self.types_list) == 3
def write(self, file):
file.print_bytex(0x16)
for type in self.types_list:
type.write(file, 1)
file.newline()
def get_size(self):
return len(self.types_list) + 1
def industry_conflicting_types(value):
if not isinstance(value, Array):
raise generic.ScriptError("conflicting_ind_types must be an array of industry types", value.pos)
if len(value.values) > 3:
raise generic.ScriptError("conflicting_ind_types may have at most three entries", value.pos)
types_list = []
for val in value.values:
types_list.append(val.reduce_constant())
while len(types_list) < 3:
types_list.append(ConstantNumeric(0xFF))
return [ConflictingTypesProp(types_list)]
def industry_input_multiplier(value, prop_num):
if not isinstance(value, Array) or len(value.values) > 2:
raise generic.ScriptError("Input multiplier must be an array of up to two values", value.pos)
val1 = value.values[0].reduce() if len(value.values) > 0 else ConstantNumeric(0)
val2 = value.values[1].reduce() if len(value.values) > 1 else ConstantNumeric(0)
if not isinstance(val1, (ConstantNumeric, ConstantFloat)) or not isinstance(val2, (ConstantNumeric, ConstantFloat)):
raise generic.ScriptError("Expected a compile-time constant", value.pos)
generic.check_range(val1.value, 0, 256, "input_multiplier", val1.pos)
generic.check_range(val2.value, 0, 256, "input_multiplier", val2.pos)
mul1 = int(val1.value * 256)
mul2 = int(val2.value * 256)
return [Action0Property(prop_num, ConstantNumeric(mul1 | (mul2 << 16)), 4)]
properties[0x0A] = {
'substitute' : {'size': 1, 'num': 0x08, 'first': None},
'override' : {'size': 1, 'num': 0x09},
'layouts' : {'custom_function': industry_layouts}, # = prop 0A
'life_type' : {'size': 1, 'num': 0x0B},
'closure_msg' : {'size': 2, 'num': 0x0C, 'string': 0xDC},
'prod_increase_msg' : {'size': 2, 'num': 0x0D, 'string': 0xDC},
'prod_decrease_msg' : {'size': 2, 'num': 0x0E, 'string': 0xDC},
'fund_cost_multiplier' : {'size': 1, 'num': 0x0F},
'prod_cargo_types' : {'size': 2, 'num': 0x10, 'value_function': lambda value: cargo_list(value, 2)},
'accept_cargo_types' : {'size': 4, 'num': 0x11, 'value_function': lambda value: cargo_list(value, 3)},
'prod_multiplier' : {'custom_function': industry_prod_multiplier}, # = prop 12,13
'min_cargo_distr' : {'size': 1, 'num': 0x14},
'random_sound_effects' : {'custom_function': random_sounds}, # = prop 15
'conflicting_ind_types' : {'custom_function': industry_conflicting_types}, # = prop 16
'prob_random' : {'size': 1, 'num': 0x17}, # Obsolete, ambiguous name, use 'prob_map_gen' instead
'prob_map_gen' : {'size': 1, 'num': 0x17},
'prob_in_game' : {'size': 1, 'num': 0x18},
'map_colour' : {'size': 1, 'num': 0x19},
'spec_flags' : {'size': 4, 'num': 0x1A},
'new_ind_msg' : {'size': 2, 'num': 0x1B, 'string': 0xDC},
'input_multiplier_1' : {'custom_function': lambda value: industry_input_multiplier(value, 0x1C)},
'input_multiplier_2' : {'custom_function': lambda value: industry_input_multiplier(value, 0x1D)},
'input_multiplier_3' : {'custom_function': lambda value: industry_input_multiplier(value, 0x1E)},
'name' : {'size': 2, 'num': 0x1F, 'string': 0xDC},
'prospect_chance' : {'size': 4, 'num': 0x20, 'unit_conversion': 0xFFFFFFFF},
# prop 21, 22 (callback flags) are not set by user
'remove_cost_multiplier' : {'size': 4, 'num': 0x23},
'nearby_station_name' : {'size': 2, 'num': 0x24, 'string': 0xDC},
}
#
# Feature 0x0B (Cargos)
#
properties[0x0B] = {
'number' : {'num' : 0x08, 'size' : 1},
'type_name' : {'num' : 0x09, 'size' : 2, 'string' : 0xDC},
'unit_name' : {'num' : 0x0A, 'size' : 2, 'string' : 0xDC},
# Properties 0B, 0C are not used by OpenTTD
'type_abbreviation' : {'num' : 0x0D, 'size' : 2, 'string' : 0xDC},
'sprite' : {'num' : 0x0E, 'size' : 2},
'weight' : {'num' : 0x0F, 'size' : 1, 'unit_type' : 'weight', 'unit_conversion' : 16},
'penalty_lowerbound' : {'num' : 0x10, 'size' : 1},
'single_penalty_length' : {'num' : 0x11, 'size' : 1},
'price_factor' : {'num' : 0x12, 'size' : 4, 'unit_conversion' : (1 << 21, 10 * 20 * 255)}, # 10 units of cargo across 20 tiles, with time factor = 255
'station_list_colour' : {'num' : 0x13, 'size' : 1},
'cargo_payment_list_colour' : {'num' : 0x14, 'size' : 1},
'is_freight' : {'num' : 0x15, 'size' : 1},
'cargo_classes' : {'num' : 0x16, 'size' : 2},
'cargo_label' : {'num' : 0x17, 'size' : 4, 'string_literal': 4},
'town_growth_effect' : {'num' : 0x18, 'size' : 1},
'town_growth_multiplier' : {'num' : 0x19, 'size' : 2, 'unit_conversion' : 0x100},
# 1A (callback flags) is not set by user
'units_of_cargo' : {'num' : 0x1B, 'size' : 2, 'string' : 0xDC},
'items_of_cargo' : {'num' : 0x1C, 'size' : 2, 'string' : 0xDC},
'capacity_multiplier' : {'num' : 0x1D, 'size' : 2, 'unit_conversion' : 0x100},
}
# Feature 0x0C (Sound Effects) is implemented differently
#
# Feature 0x0D (Airports)
#
def airport_years(value):
if not isinstance(value, Array) or len(value.values) != 2:
raise generic.ScriptError("Availability years must be an array with exactly two values", value.pos)
min_year = value.values[0].reduce_constant()
max_year = value.values[1].reduce_constant()
return [Action0Property(0x0C, ConstantNumeric(max_year.value << 16 | min_year.value), 4)]
class AirportLayoutProp(BaseAction0Property):
def __init__(self, layout_list):
self.layout_list = layout_list
def write(self, file):
file.print_bytex(0x0A)
file.print_byte(len(self.layout_list))
# -6 because prop_num, num_layouts and size should not be included
file.print_dword(self.get_size() - 6)
file.newline()
for layout in self.layout_list:
file.print_bytex(layout.properties['rotation'].value)
layout.write(file)
file.newline()
def get_size(self):
size = 6
for layout in self.layout_list:
size += layout.get_size() + 1
return size
def airport_layouts(value):
if not isinstance(value, Array) or not all(isinstance(x, Identifier) for x in value.values):
raise generic.ScriptError("layouts must be an array of layout names", value.pos)
layouts = []
for name in value.values:
if name.value not in tilelayout_names:
raise generic.ScriptError("Unknown layout name '{}'".format(name.value), name.pos)
layout = tilelayout_names[name.value]
if 'rotation' not in layout.properties:
raise generic.ScriptError("Airport layouts must have the 'rotation' property", layout.pos)
if layout.properties['rotation'].value not in (0, 2, 4, 6):
raise generic.ScriptError("Airport layout rotation is not a valid direction.", layout.properties['rotation'].pos)
layouts.append(layout)
return [AirportLayoutProp(layouts)]
properties[0x0D] = {
'override' : {'size': 1, 'num': 0x08, 'first':None},
# 09 does not exist
'layouts' : {'custom_function': airport_layouts}, # = prop 0A
# 0B does not exist
'years_available' : {'custom_function': airport_years}, # = prop 0C
'ttd_airport_type' : {'size': 1, 'num': 0x0D},
'catchment_area' : {'size': 1, 'num': 0x0E},
'noise_level' : {'size': 1, 'num': 0x0F},
'name' : {'size': 2, 'num': 0x10, 'string': 0xDC},
'maintenance_cost' : {'size': 2, 'num': 0x11},
}
# Feature 0x0E (Signals) doesn't currently have any action0
#
# Feature 0x0F (Objects)
#
def object_size(value):
if not isinstance(value, Array) or len(value.values) != 2:
raise generic.ScriptError("Object size must be an array with exactly two values", value.pos)
sizex = value.values[0].reduce_constant()
sizey = value.values[1].reduce_constant()
if sizex.value < 1 or sizex.value > 15 or sizey.value < 1 or sizey.value > 15:
raise generic.ScriptError("The size of an object must be at least 1x1 and at most 15x15 tiles", value.pos)
return [Action0Property(0x0C, ConstantNumeric(sizey.value << 4 | sizex.value), 1)]
properties[0x0F] = {
'class' : {'size': 4, 'num': 0x08, 'first': None, 'string_literal': 4},
# strings might be according to specs be either 0xD0 or 0xD4
'classname' : {'size': 2, 'num': 0x09, 'string': 0xD0},
'name' : {'size': 2, 'num': 0x0A, 'string': 0xD0},
'climates_available' : {'size': 1, 'num': 0x0B},
'size' : {'custom_function': object_size}, # = prop 0C
'build_cost_multiplier' : {'size': 1, 'num': 0x0D},
'introduction_date' : {'size': 4, 'num': 0x0E},
'end_of_life_date' : {'size': 4, 'num': 0x0F},
'object_flags' : {'size': 2, 'num': 0x10},
'animation_info' : {'size': 2, 'num': 0x11, 'value_function': animation_info},
'animation_speed' : {'size': 1, 'num': 0x12},
'animation_triggers' : {'size': 2, 'num': 0x13},
'remove_cost_multiplier' : {'size': 1, 'num': 0x14},
# 15 (callback flags) is not set by user
'height' : {'size': 1, 'num': 0x16},
'num_views' : {'size': 1, 'num': 0x17},
'count_per_map256' : {'size': 1, 'num': 0x18},
}
#
# Feature 0x10 (Rail Types)
#
class RailtypeListProp(BaseAction0Property):
def __init__(self, prop_num, railtype_list):
self.prop_num = prop_num
self.railtype_list = railtype_list
def write(self, file):
file.print_bytex(self.prop_num)
file.print_byte(len(self.railtype_list))
for railtype in self.railtype_list:
railtype.write(file, 4)
file.newline()
def get_size(self):
return len(self.railtype_list) * 4 + 2
def railtype_list(value, prop_num):
if not isinstance(value, Array):
raise generic.ScriptError("Railtype list must be an array of literal strings", value.pos)
for val in value.values:
if not isinstance(val, StringLiteral): raise generic.ScriptError("Railtype list must be an array of literal strings", val.pos)
return [RailtypeListProp(prop_num, value.values)]
properties[0x10] = {
'label' : {'size': 4, 'num': 0x08, 'string_literal': 4}, # is allocated during reservation stage, setting label first is thus not needed
'toolbar_caption' : {'size': 2, 'num': 0x09, 'string': 0xDC},
'menu_text' : {'size': 2, 'num': 0x0A, 'string': 0xDC},
'build_window_caption' : {'size': 2, 'num': 0x0B, 'string': 0xDC},
'autoreplace_text' : {'size': 2, 'num': 0x0C, 'string': 0xDC},
'new_engine_text' : {'size': 2, 'num': 0x0D, 'string': 0xDC},
'compatible_railtype_list' : {'custom_function': lambda x: railtype_list(x, 0x0E)},
'powered_railtype_list' : {'custom_function': lambda x: railtype_list(x, 0x0F)},
'railtype_flags' : {'size': 1, 'num': 0x10},
'curve_speed_multiplier' : {'size': 1, 'num': 0x11},
'station_graphics' : {'size': 1, 'num': 0x12},
'construction_cost' : {'size': 2, 'num': 0x13},
'speed_limit' : {'size': 2, 'num': 0x14, 'unit_type': 'speed', 'unit_conversion': (5000, 1397)},
'acceleration_model' : {'size': 1, 'num': 0x15},
'map_colour' : {'size': 1, 'num': 0x16},
'introduction_date' : {'size': 4, 'num': 0x17},
'requires_railtype_list' : {'custom_function': lambda x: railtype_list(x, 0x18)},
'introduces_railtype_list' : {'custom_function': lambda x: railtype_list(x, 0x19)},
'sort_order' : {'size': 1, 'num': 0x1A},
'name' : {'size': 2, 'num': 0x1B, 'string': 0xDC},
'maintenance_cost' : {'size': 2, 'num': 0x1C},
'alternative_railtype_list': {'custom_function': lambda x: railtype_list(x, 0x1D)},
}
#
# Feature 0x11 (Airport Tiles)
#
properties[0x11] = {
'substitute' : {'size': 1, 'num': 0x08, 'first': None},
'override' : {'size': 1, 'num': 0x09},
# 0A - 0D don't exist (yet?)
# 0E (callback flags) is not set by user
'animation_info' : {'size': 2, 'num': 0x0F, 'value_function': animation_info},
'animation_speed' : {'size': 1, 'num': 0x10},
'animation_triggers' : {'size': 1, 'num': 0x11},
}
nml-0.4.5/nml/actions/action7.py 0000644 0005672 0005672 00000027603 13315644406 017632 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, nmlop, free_number_list, generic
from nml.actions import base_action, action6, actionD, action10
free_labels = free_number_list.FreeNumberList(list(range(0xFF, 0x0F, -1)), "No label available to use for large if-blocks and loops.", "No unique label available to use for large if-blocks and loops.")
def print_stats():
"""
Print statistics about used ids.
"""
if free_labels.stats[0] > 0:
generic.print_info("Concurrent Action10 labels: {}/{} ({})".format(free_labels.stats[0], free_labels.total_amount, str(free_labels.stats[1])))
class SkipAction(base_action.BaseAction):
def __init__(self, action_type, var, varsize, condtype, value, label):
self.action_type = action_type
self.label = label
self.var = var
self.varsize = varsize
self.condtype = condtype
self.value = value
self.label = label
if self.condtype[0] == 0 or self.condtype[0] == 1:
assert self.varsize == 1
def write(self, file):
size = 5 + self.varsize
file.start_sprite(size)
file.print_bytex(self.action_type)
file.print_bytex(self.var)
file.print_bytex(self.varsize)
file.print_bytex(self.condtype[0], self.condtype[1])
if self.varsize == 8:
#grfid + mask
file.print_dwordx(self.value & 0xFFFFFFFF)
file.print_dwordx(self.value >> 32)
else:
file.print_varx(self.value, self.varsize)
file.print_bytex(self.label)
file.newline()
file.end_sprite()
def skip_action7(self):
return self.action_type == 7
def skip_action9(self):
return self.action_type == 9 or self.label == 0
class UnconditionalSkipAction(SkipAction):
def __init__(self, action_type, label):
SkipAction.__init__(self, action_type, 0x9A, 1, (0, r'\71'), 0, label)
def op_to_cond_op(op):
#The operators are reversed as we want to skip if the expression is true
#while the nml-syntax wants to execute the block if the expression is true
if op == nmlop.CMP_NEQ: return (2, r'\7=')
if op == nmlop.CMP_EQ: return (3, r'\7!')
if op == nmlop.CMP_GE: return (4, r'\7<')
if op == nmlop.CMP_LE: return (5, r'\7>')
def parse_conditional(expr):
'''
Parse an expression and return enough information to use
that expression as a conditional statement.
Return value is a tuple with the following elements:
- Parameter number (as integer) to use in comparison or None for unconditional skip
- List of actions needed to set the given parameter to the correct value
- The type of comparison to be done
- The value to compare against (as integer)
- The size of the value (as integer)
'''
if expr is None:
return (None, [], (2, r'\7='), 0, 4)
if isinstance(expr, expression.BinOp):
if expr.op == nmlop.HASBIT or expr.op == nmlop.NOTHASBIT:
if isinstance(expr.expr1, expression.Parameter) and isinstance(expr.expr1.num, expression.ConstantNumeric):
param = expr.expr1.num.value
actions = []
else:
param, actions = actionD.get_tmp_parameter(expr.expr1)
if isinstance(expr.expr2, expression.ConstantNumeric):
bit_num = expr.expr2.value
else:
if isinstance(expr.expr2, expression.Parameter) and isinstance(expr.expr2.num, expression.ConstantNumeric):
param = expr.expr2.num.value
else:
param, tmp_action_list = actionD.get_tmp_parameter(expr.expr2)
actions.extend(tmp_action_list)
act6 = action6.Action6()
act6.modify_bytes(param, 1, 4)
actions.append(act6)
bit_num = 0
comp_type = (1, r'\70') if expr.op == nmlop.HASBIT else (0, r'\71')
return (param, actions, comp_type, bit_num , 1)
elif expr.op in (nmlop.CMP_EQ, nmlop.CMP_NEQ, nmlop.CMP_LE, nmlop.CMP_GE) \
and isinstance(expr.expr2, expression.ConstantNumeric):
if isinstance(expr.expr1, expression.Parameter) and isinstance(expr.expr1.num, expression.ConstantNumeric):
param = expr.expr1.num.value
actions = []
else:
param, actions = actionD.get_tmp_parameter(expr.expr1)
op = op_to_cond_op(expr.op)
return (param, actions, op, expr.expr2.value, 4)
if isinstance(expr, expression.Boolean):
expr = expr.expr
if isinstance(expr, expression.Not):
param, actions = actionD.get_tmp_parameter(expr.expr)
return (param, actions, (3, r'\7!'), 0, 4)
param, actions = actionD.get_tmp_parameter(expr)
return (param, actions, (2, r'\7='), 0, 4)
def cond_skip_actions(action_list, param, condtype, value, value_size, pos):
if len(action_list) == 0: return []
actions = []
start, length = 0, 0
# Whether to allow not-skipping, using action7 or using action9
skip_opts = (True, True, True)
# Add a sentinel value to the list to avoid code duplication
for action in action_list + [None]:
assert any(skip_opts)
if action is not None:
# Options allowed by the next action
act_opts = (not action.skip_needed(), action.skip_action7(), action.skip_action9())
else:
# There are no further actions, so be sure to finish the current block
act_opts = (False, False, False)
# Options still allowed, when including this action in the previous block
new_opts = tuple(all(tpl) for tpl in zip(skip_opts, act_opts))
# End this block in two cases:
# - There are no options to skip this action and the preceding block in one go
# - The existing block (with length > 0) needed no skipping, but this action does
if any(new_opts) and not (length > 0 and skip_opts[0] and not new_opts[0]):
# Append this action to the existing block
length += 1
skip_opts = new_opts
continue
# We need to create a new block
if skip_opts[0]:
# We can just choose to not skip the preceeding actions without harm
actions.extend(action_list[start:start+length])
else:
action_type = 7 if skip_opts[1] else 9
if length < 0x10:
# Lengths under 0x10 are handled without labels, to avoid excessive label usage
target = length
label = None
else:
target = free_labels.pop(pos)
label = action10.Action10(target)
actions.append(SkipAction(action_type, param, value_size, condtype, value, target))
actions.extend(action_list[start:start+length])
if label is not None: actions.append(label)
start = start + length
length = 1
skip_opts = act_opts
assert start == len(action_list)
return actions
recursive_cond_blocks = 0
def parse_conditional_block(cond_list):
global recursive_cond_blocks
recursive_cond_blocks += 1
if recursive_cond_blocks == 1:
# We only save a single state (at toplevel nml-blocks) because
# we don't know at the start of the block how many labels we need.
# Getting the same label for a block that was already used in a
# sub-block would be very bad, since the action7/9 would skip
# to the action10 of the sub-block.
free_labels.save()
blocks = []
for cond in cond_list.statements:
if isinstance(cond.expr, expression.ConstantNumeric):
if cond.expr.value == 0:
continue
else:
blocks.append({'expr': None, 'statements': cond.statements})
break
blocks.append({'expr': cond.expr, 'statements': cond.statements})
if blocks:
blocks[-1]['last_block'] = True
if len(blocks) == 1 and blocks[0]['expr'] is None:
action_list = []
for stmt in blocks[0]['statements']:
action_list.extend(stmt.get_action_list())
return action_list
action6.free_parameters.save()
if len(blocks) > 1:
# the skip all parameter is used to skip all blocks after one
# of the conditionals was true. We can't always skip directly
# to the end of the blocks since action7/action9 can't always
# be mixed
param_skip_all, action_list = actionD.get_tmp_parameter(expression.ConstantNumeric(0xFFFFFFFF))
else:
action_list = []
# use parse_conditional here, we also need to know if all generated
# actions (like action6) can be skipped safely
for block in blocks:
block['param_dst'], block['cond_actions'], block['cond_type'], block['cond_value'], block['cond_value_size'] = parse_conditional(block['expr'])
if not 'last_block' in block:
block['action_list'] = [actionD.ActionD(expression.ConstantNumeric(param_skip_all), expression.ConstantNumeric(0xFF), nmlop.ASSIGN, expression.ConstantNumeric(0), expression.ConstantNumeric(0))]
else:
block['action_list'] = []
for stmt in block['statements']:
block['action_list'].extend(stmt.get_action_list())
# Main problem: action10 can't be skipped by action9, so we're
# nearly forced to use action7, but action7 can't safely skip action6
# Solution: use temporary parameter, set to 0 for not skip, !=0 for skip.
# then skip every block of actions (as large as possible) with either
# action7 or action9, depending on which of the two works.
for i, block in enumerate(blocks):
param = block['param_dst']
if i == 0: action_list.extend(block['cond_actions'])
else:
action_list.extend(cond_skip_actions(block['cond_actions'], param_skip_all, (2, r'\7='), 0, 4, cond_list.pos))
if param is None:
param = param_skip_all
else:
action_list.append(actionD.ActionD(expression.ConstantNumeric(block['param_dst']), expression.ConstantNumeric(block['param_dst']), nmlop.AND, expression.ConstantNumeric(param_skip_all)))
action_list.extend(cond_skip_actions(block['action_list'], param, block['cond_type'], block['cond_value'], block['cond_value_size'], cond_list.pos))
if recursive_cond_blocks == 1:
free_labels.restore()
recursive_cond_blocks -= 1
action6.free_parameters.restore()
return action_list
def parse_loop_block(loop):
global recursive_cond_blocks
recursive_cond_blocks += 1
if recursive_cond_blocks == 1:
free_labels.save()
action6.free_parameters.save()
begin_label = free_labels.pop_unique(loop.pos)
action_list = [action10.Action10(begin_label)]
cond_param, cond_actions, cond_type, cond_value, cond_value_size = parse_conditional(loop.expr)
block_actions = []
for stmt in loop.statements:
block_actions.extend(stmt.get_action_list())
action_list.extend(cond_actions)
block_actions.append(UnconditionalSkipAction(9, begin_label))
action_list.extend(cond_skip_actions(block_actions, cond_param, cond_type, cond_value, cond_value_size, loop.pos))
if recursive_cond_blocks == 1:
free_labels.restore()
recursive_cond_blocks -= 1
action6.free_parameters.restore()
return action_list
nml-0.4.5/nml/actions/action8.py 0000644 0005672 0005672 00000002670 13315644406 017630 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import grfstrings
from nml.actions import base_action
class Action8(base_action.BaseAction):
def __init__(self, grfid, name, description):
self.grfid = grfid
self.name = name
self.description = description
def write(self, file):
name = grfstrings.get_translation(self.name)
desc = grfstrings.get_translation(self.description)
size = 6 + grfstrings.get_string_size(name) + grfstrings.get_string_size(desc)
file.start_sprite(size)
file.print_bytex(8)
# Write the grf version
file.print_bytex(8)
file.print_string(self.grfid.value, False, True)
file.print_string(name)
file.print_string(desc)
file.end_sprite()
def skip_action7(self):
return False
nml-0.4.5/nml/actions/action3_callbacks.py 0000644 0005672 0005672 00000041073 13315644406 021622 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, nmlop
callbacks = 0x12 * [{}]
# Possible values for 'purchase':
# 0 (or not set): not called from purchase list
# 1: called normally and from purchase list
# 2: only called from purchase list
# 'cbname': as 1) but if 'cbname' is set also, then 'cbname' overrides this
# in the purchase list. 'cbname' should have a value of 2 for 'purchase'
# Callbacks common to all vehicle types
general_vehicle_cbs = {
'default' : {'type': 'cargo', 'num': None},
'purchase' : {'type': 'cargo', 'num': 0xFF},
'random_trigger' : {'type': 'cb', 'num': 0x01}, # Almost undocumented, but really neccesary!
'loading_speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x07},
'cargo_subtype_text' : {'type': 'cb', 'num': 0x19, 'flag_bit': 5},
'additional_text' : {'type': 'cb', 'num': 0x23, 'purchase': 2},
'colour_mapping' : {'type': 'cb', 'num': 0x2D, 'flag_bit':6, 'purchase': 'purchase_colour_mapping'},
'purchase_colour_mapping' : {'type': 'cb', 'num': 0x2D, 'flag_bit':6, 'purchase': 2},
'start_stop' : {'type': 'cb', 'num': 0x31},
'every_32_days' : {'type': 'cb', 'num': 0x32},
'sound_effect' : {'type': 'cb', 'num': 0x33, 'flag_bit': 7},
'refit_cost' : {'type': 'cb', 'num': 0x15E, 'purchase': 1},
}
# Function to convert vehicle length to the actual property value, which is (8 - length)
def vehicle_length(value):
return expression.BinOp(nmlop.SUB, expression.ConstantNumeric(8, value.pos), value, value.pos)
# Trains
callbacks[0x00] = {
'visual_effect_and_powered' : {'type': 'cb', 'num': 0x10, 'flag_bit': 0},
'effect_spawn_model_and_powered' : {'type': 'cb', 'num': 0x10, 'flag_bit': 0},
'length' : {'type': 'cb', 'num': 0x36, 'var10': 0x21, 'value_function': vehicle_length},
'cargo_capacity' : [ {'type': 'cb', 'num': 0x15, 'flag_bit': 3},
{'type': 'cb', 'num': 0x36, 'var10': 0x14, 'purchase': 'purchase_cargo_capacity'}],
'purchase_cargo_capacity' : {'type': 'cb', 'num': 0x36, 'var10': 0x14, 'purchase': 2},
'articulated_part' : {'type': 'cb', 'num': 0x16, 'flag_bit': 4, 'purchase': 1}, # Don't add separate purchase CB here
'can_attach_wagon' : {'type': 'cb', 'num': 0x1D},
'speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x09, 'purchase': 'purchase_speed'},
'purchase_speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x09, 'purchase': 2},
'power' : {'type': 'cb', 'num': 0x36, 'var10': 0x0B, 'purchase': 'purchase_power'},
'purchase_power' : {'type': 'cb', 'num': 0x36, 'var10': 0x0B, 'purchase': 2},
'running_cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x0D, 'purchase': 'purchase_running_cost_factor'},
'purchase_running_cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x0D, 'purchase': 2},
'weight' : {'type': 'cb', 'num': 0x36, 'var10': 0x16, 'purchase': 'purchase_weight'},
'purchase_weight' : {'type': 'cb', 'num': 0x36, 'var10': 0x16, 'purchase': 2},
'cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x17, 'purchase': 2},
'tractive_effort_coefficient' : {'type': 'cb', 'num': 0x36, 'var10': 0x1F, 'purchase': 'purchase_tractive_effort_coefficient'},
'purchase_tractive_effort_coefficient' : {'type': 'cb', 'num': 0x36, 'var10': 0x1F, 'purchase': 2},
'bitmask_vehicle_info' : {'type': 'cb', 'num': 0x36, 'var10': 0x25},
'cargo_age_period' : {'type': 'cb', 'num': 0x36, 'var10': 0x2B},
'create_effect' : {'type': 'cb', 'num': 0x160},
}
callbacks[0x00].update(general_vehicle_cbs)
# Road vehicles
callbacks[0x01] = {
'visual_effect' : {'type': 'cb', 'num': 0x10, 'flag_bit': 0},
'effect_spawn_model' : {'type': 'cb', 'num': 0x10, 'flag_bit': 0},
'length' : {'type': 'cb', 'num': 0x36, 'var10': 0x23, 'value_function': vehicle_length},
'cargo_capacity' : [ {'type': 'cb', 'num': 0x15, 'flag_bit': 3},
{'type': 'cb', 'num': 0x36, 'var10': 0x0F, 'purchase': 'purchase_cargo_capacity'}],
'purchase_cargo_capacity' : {'type': 'cb', 'num': 0x36, 'var10': 0x0F, 'purchase': 2},
'articulated_part' : {'type': 'cb', 'num': 0x16, 'flag_bit': 4, 'purchase': 1}, # Don't add separate purchase CB here
'running_cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x09, 'purchase': 'purchase_running_cost_factor'},
'purchase_running_cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x09, 'purchase': 2},
'cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x11, 'purchase': 2},
'power' : {'type': 'cb', 'num': 0x36, 'var10': 0x13, 'purchase': 'purchase_power'},
'purchase_power' : {'type': 'cb', 'num': 0x36, 'var10': 0x13, 'purchase': 2},
'weight' : {'type': 'cb', 'num': 0x36, 'var10': 0x14, 'purchase': 'purchase_weight'},
'purchase_weight' : {'type': 'cb', 'num': 0x36, 'var10': 0x14, 'purchase': 2},
'speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x15, 'purchase': 'purchase_speed'},
'purchase_speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x15, 'purchase': 2},
'tractive_effort_coefficient' : {'type': 'cb', 'num': 0x36, 'var10': 0x18, 'purchase': 'purchase_tractive_effort_coefficient'},
'purchase_tractive_effort_coefficient' : {'type': 'cb', 'num': 0x36, 'var10': 0x18, 'purchase': 2},
'cargo_age_period' : {'type': 'cb', 'num': 0x36, 'var10': 0x22},
'create_effect' : {'type': 'cb', 'num': 0x160},
}
callbacks[0x01].update(general_vehicle_cbs)
# Ships
callbacks[0x02] = {
'visual_effect' : {'type': 'cb', 'num': 0x10, 'flag_bit': 0},
'effect_spawn_model' : {'type': 'cb', 'num': 0x10, 'flag_bit': 0},
'cargo_capacity' : [ {'type': 'cb', 'num': 0x15, 'flag_bit': 3},
{'type': 'cb', 'num': 0x36, 'var10': 0x0D, 'purchase': 'purchase_cargo_capacity'}],
'purchase_cargo_capacity' : {'type': 'cb', 'num': 0x36, 'var10': 0x0D, 'purchase': 2},
'cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x0A, 'purchase': 2},
'speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x0B, 'purchase': 'purchase_speed'},
'purchase_speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x0B, 'purchase': 2},
'running_cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x0F, 'purchase': 'purchase_running_cost_factor'},
'purchase_running_cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x0F, 'purchase': 2},
'cargo_age_period' : {'type': 'cb', 'num': 0x36, 'var10': 0x1D},
'create_effect' : {'type': 'cb', 'num': 0x160},
}
callbacks[0x02].update(general_vehicle_cbs)
# Aircraft
callbacks[0x03] = {
'passenger_capacity' : [ {'type': 'cb', 'num': 0x15, 'flag_bit': 3},
{'type': 'cb', 'num': 0x36, 'var10': 0x0F, 'purchase': 'purchase_passenger_capacity'}],
'purchase_passenger_capacity' : {'type': 'cb', 'num': 0x36, 'var10': 0x0F, 'purchase': 2},
'cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x0B, 'purchase': 2},
'speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x0C, 'purchase': 'purchase_speed'},
'purchase_speed' : {'type': 'cb', 'num': 0x36, 'var10': 0x0C, 'purchase': 2},
'running_cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x0E, 'purchase': 'purchase_running_cost_factor'},
'purchase_running_cost_factor' : {'type': 'cb', 'num': 0x36, 'var10': 0x0E, 'purchase': 2},
'mail_capacity' : {'type': 'cb', 'num': 0x36, 'var10': 0x11, 'purchase': 'purchase_mail_capacity'},
'purchase_mail_capacity' : {'type': 'cb', 'num': 0x36, 'var10': 0x11, 'purchase': 2},
'cargo_age_period' : {'type': 'cb', 'num': 0x36, 'var10': 0x1C},
'range' : {'type': 'cb', 'num': 0x36, 'var10': 0x1F, 'purchase': 'purchase_range'},
'purchase_range' : {'type': 'cb', 'num': 0x36, 'var10': 0x1F, 'purchase': 2},
'rotor' : {'type': 'override'},
}
callbacks[0x03].update(general_vehicle_cbs)
# Stations (0x04) are not yet fully implemented
callbacks[0x04] = {
'default' : {'type': 'cargo', 'num': None},
}
# Canals
callbacks[0x05] = {
'sprite_offset' : {'type': 'cb', 'num': 0x147, 'flag_bit': 0},
'default' : {'type': 'cargo', 'num': None},
}
# Bridges (0x06) have no action3
# Houses
callbacks[0x07] = {
'random_trigger' : {'type': 'cb', 'num': 0x01},
'construction_check' : {'type': 'cb', 'num': 0x17, 'flag_bit': 0, 'tiles': 'n'},
'anim_next_frame' : {'type': 'cb', 'num': 0x1A, 'flag_bit': 1},
'anim_control' : {'type': 'cb', 'num': 0x1B, 'flag_bit': 2},
'construction_anim' : {'type': 'cb', 'num': 0x1C, 'flag_bit': 3},
'colour' : {'type': 'cb', 'num': 0x1E, 'flag_bit': 4},
'cargo_amount_accept' : {'type': 'cb', 'num': 0x1F, 'flag_bit': 5},
'anim_speed' : {'type': 'cb', 'num': 0x20, 'flag_bit': 6},
'destruction' : {'type': 'cb', 'num': 0x21, 'flag_bit': 7, 'tiles': 'n'},
'cargo_type_accept' : {'type': 'cb', 'num': 0x2A, 'flag_bit': 8},
'cargo_production' : {'type': 'cb', 'num': 0x2E, 'flag_bit': 9},
'protection' : {'type': 'cb', 'num': 0x143, 'flag_bit': 10},
'watched_cargo_accepted' : {'type': 'cb', 'num': 0x148},
'name' : {'type': 'cb', 'num': 0x14D},
'foundations' : {'type': 'cb', 'num': 0x14E, 'flag_bit': 11},
'autoslope' : {'type': 'cb', 'num': 0x14F, 'flag_bit': 12},
'graphics_north' : {'type': 'cb', 'num': 0x00, 'tiles': 'n'},
'graphics_east' : {'type': 'cb', 'num': 0x00, 'tiles': 'e'},
'graphics_south' : {'type': 'cb', 'num': 0x00, 'tiles': 's'},
'graphics_west' : {'type': 'cb', 'num': 0x00, 'tiles': 'w'},
'default' : {'type': 'cargo', 'num': None},
}
# General variables (0x08) have no action3
# Industry tiles
callbacks[0x09] = {
'random_trigger' : {'type': 'cb', 'num': 0x01},
'anim_control' : {'type': 'cb', 'num': 0x25},
'anim_next_frame' : {'type': 'cb', 'num': 0x26, 'flag_bit': 0},
'anim_speed' : {'type': 'cb', 'num': 0x27, 'flag_bit': 1},
'cargo_amount_accept' : {'type': 'cb', 'num': 0x2B, 'flag_bit': 2}, # Should work like the industry CB, i.e. call multiple times
'cargo_type_accept' : {'type': 'cb', 'num': 0x2C, 'flag_bit': 3}, # Should work like the industry CB, i.e. call multiple times
'tile_check' : {'type': 'cb', 'num': 0x2F, 'flag_bit': 4},
'foundations' : {'type': 'cb', 'num': 0x30, 'flag_bit': 5},
'autoslope' : {'type': 'cb', 'num': 0x3C, 'flag_bit': 6},
'default' : {'type': 'cargo', 'num': None},
}
# Industries
callbacks[0x0A] = {
'construction_probability' : {'type': 'cb', 'num': 0x22, 'flag_bit': 0},
'produce_cargo_arrival' : {'type': 'cb', 'num': 0x00, 'flag_bit': 1, 'var18': 0},
'produce_256_ticks' : {'type': 'cb', 'num': 0x00, 'flag_bit': 2, 'var18': 1},
'location_check' : {'type': 'cb', 'num': 0x28, 'flag_bit': 3}, # We need a way to access all those special variables
'random_prod_change' : {'type': 'cb', 'num': 0x29, 'flag_bit': 4},
'monthly_prod_change' : {'type': 'cb', 'num': 0x35, 'flag_bit': 5},
'cargo_subtype_display' : {'type': 'cb', 'num': 0x37, 'flag_bit': 6},
'extra_text_fund' : {'type': 'cb', 'num': 0x38, 'flag_bit': 7},
'extra_text_industry' : {'type': 'cb', 'num': 0x3A, 'flag_bit': 8},
'control_special' : {'type': 'cb', 'num': 0x3B, 'flag_bit': 9},
'stop_accept_cargo' : {'type': 'cb', 'num': 0x3D, 'flag_bit': 10},
'colour' : {'type': 'cb', 'num': 0x14A, 'flag_bit': 11},
'cargo_input' : {'type': 'cb', 'num': 0x14B, 'flag_bit': 12},
'cargo_output' : {'type': 'cb', 'num': 0x14C, 'flag_bit': 13},
'build_prod_change' : {'type': 'cb', 'num': 0x15F, 'flag_bit': 14},
'default' : {'type': 'cargo', 'num': None},
}
# Cargos
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 = expression.BinOp(nmlop.MUL, value, expression.ConstantNumeric(329), value.pos)
return expression.BinOp(nmlop.DIV, value, expression.ConstantNumeric(256), value.pos)
callbacks[0x0B] = {
'profit' : {'type': 'cb', 'num': 0x39, 'flag_bit': 0, 'value_function': cargo_profit_value},
'station_rating' : {'type': 'cb', 'num': 0x145, 'flag_bit': 1},
'default' : {'type': 'cargo', 'num': None},
}
# Sound effects (0x0C) have no item-specific action3
# Airports
callbacks[0x0D] = {
'additional_text' : {'type': 'cb', 'num': 0x155},
'layout_name' : {'type': 'cb', 'num': 0x156},
'default' : {'type': 'cargo', 'num': None},
}
# New signals (0x0E) have no item-specific action3
# Objects
callbacks[0x0F] = {
'tile_check' : {'type': 'cb', 'num': 0x157, 'flag_bit': 0, 'purchase': 2},
'anim_next_frame' : {'type': 'cb', 'num': 0x158, 'flag_bit': 1},
'anim_control' : {'type': 'cb', 'num': 0x159},
'anim_speed' : {'type': 'cb', 'num': 0x15A, 'flag_bit': 2},
'colour' : {'type': 'cb', 'num': 0x15B, 'flag_bit': 3},
'additional_text' : {'type': 'cb', 'num': 0x15C, 'flag_bit': 4, 'purchase': 2},
'autoslope' : {'type': 'cb', 'num': 0x15D, 'flag_bit': 5},
'default' : {'type': 'cargo', 'num': None},
'purchase' : {'type': 'cargo', 'num': 0xFF},
}
# Railtypes
callbacks[0x10] = {
# No default here, it makes no sense
'gui' : {'type': 'cargo', 'num': 0x00},
'track_overlay' : {'type': 'cargo', 'num': 0x01},
'underlay' : {'type': 'cargo', 'num': 0x02},
'tunnels' : {'type': 'cargo', 'num': 0x03},
'catenary_wire' : {'type': 'cargo', 'num': 0x04},
'catenary_pylons' : {'type': 'cargo', 'num': 0x05},
'bridge_surfaces' : {'type': 'cargo', 'num': 0x06},
'level_crossings' : {'type': 'cargo', 'num': 0x07},
'depots' : {'type': 'cargo', 'num': 0x08},
'fences' : {'type': 'cargo', 'num': 0x09},
'tunnel_overlay' : {'type': 'cargo', 'num': 0x0A},
'signals' : {'type': 'cargo', 'num': 0x0B},
}
# Airport tiles
callbacks[0x11] = {
'foundations' : {'type': 'cb', 'num': 0x150, 'flag_bit': 5},
'anim_control' : {'type': 'cb', 'num': 0x152},
'anim_next_frame' : {'type': 'cb', 'num': 0x153, 'flag_bit': 0},
'anim_speed' : {'type': 'cb', 'num': 0x154, 'flag_bit': 1},
'default' : {'type': 'cargo', 'num': None},
}
nml-0.4.5/nml/actions/action2layout.py 0000644 0005672 0005672 00000055572 13315644406 021071 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, expression, nmlop
from nml.actions import action2, action6, actionD, action1, action2var, real_sprite
from nml.ast import general
class Action2Layout(action2.Action2):
def __init__(self, feature, name, pos, ground_sprite, sprite_list, param_registers):
action2.Action2.__init__(self, feature, name, pos)
assert ground_sprite.type == Action2LayoutSpriteType.GROUND
self.ground_sprite = ground_sprite
self.sprite_list = sprite_list
self.param_registers = param_registers
def resolve_tmp_storage(self):
for reg in self.param_registers:
if not self.tmp_locations:
raise generic.ScriptError("There are not enough registers available " +
"to perform all required computations in switch blocks. " +
"Please reduce the complexity of your code.", self.pos)
location = self.tmp_locations[0]
self.remove_tmp_location(location, False)
reg.set_register(location)
def write(self, file):
advanced = any(x.is_advanced_sprite() for x in self.sprite_list + [self.ground_sprite])
size = 5
if advanced: size += self.ground_sprite.get_registers_size()
for sprite in self.sprite_list:
if sprite.type == Action2LayoutSpriteType.CHILD:
size += 7
else:
size += 10
if advanced: size += sprite.get_registers_size()
if len(self.sprite_list) == 0:
size += 9
action2.Action2.write_sprite_start(self, file, size)
if advanced:
file.print_byte(0x40 | len(self.sprite_list))
else:
file.print_byte(len(self.sprite_list))
self.ground_sprite.write_sprite_number(file)
if advanced:
self.ground_sprite.write_flags(file)
self.ground_sprite.write_registers(file)
file.newline()
if len(self.sprite_list) == 0:
file.print_dwordx(0) #sprite number 0 == no sprite
for i in range(0, 5):
file.print_byte(0) #empty bounding box. Note that number of zeros is 5, not 6
else:
for sprite in self.sprite_list:
sprite.write_sprite_number(file)
if advanced: sprite.write_flags(file)
file.print_byte(sprite.get_param('xoffset').value)
file.print_byte(sprite.get_param('yoffset').value)
if sprite.type == Action2LayoutSpriteType.CHILD:
file.print_bytex(0x80)
else:
#normal building sprite
file.print_byte(sprite.get_param('zoffset').value)
file.print_byte(sprite.get_param('xextent').value)
file.print_byte(sprite.get_param('yextent').value)
file.print_byte(sprite.get_param('zextent').value)
if advanced: sprite.write_registers(file)
file.newline()
file.end_sprite()
class Action2LayoutSpriteType(object):
GROUND = 0
BUILDING = 1
CHILD = 2
#these keywords are used to identify a ground/building/childsprite
layout_sprite_types = {
'ground' : Action2LayoutSpriteType.GROUND,
'building' : Action2LayoutSpriteType.BUILDING,
'childsprite' : Action2LayoutSpriteType.CHILD,
}
class Action2LayoutSprite(object):
def __init__(self, feature, type, pos = None, extra_dicts = []):
self.feature = feature
self.type = type
self.pos = pos
self.extra_dicts = extra_dicts
self.params = {
'sprite' : {'value': None, 'validator': self._validate_sprite},
'recolour_mode' : {'value': 0, 'validator': self._validate_recolour_mode},
'palette' : {'value': expression.ConstantNumeric(0), 'validator': self._validate_palette},
'always_draw' : {'value': 0, 'validator': self._validate_always_draw},
'xoffset' : {'value': expression.ConstantNumeric(0), 'validator': self._validate_bounding_box},
'yoffset' : {'value': expression.ConstantNumeric(0), 'validator': self._validate_bounding_box},
'zoffset' : {'value': expression.ConstantNumeric(0), 'validator': self._validate_bounding_box},
'xextent' : {'value': expression.ConstantNumeric(16), 'validator': self._validate_bounding_box},
'yextent' : {'value': expression.ConstantNumeric(16), 'validator': self._validate_bounding_box},
'zextent' : {'value': expression.ConstantNumeric(16), 'validator': self._validate_bounding_box},
'hide_sprite' : {'value': None, 'validator': self._validate_hide_sprite}, # Value not used
}
for i in self.params:
self.params[i]['is_set'] = False
self.params[i]['register'] = None
self.sprite_from_action1 = False
self.palette_from_action1 = False
def is_advanced_sprite(self):
if self.palette_from_action1: return True
return len(self.get_all_registers()) != 0
def get_registers_size(self):
# Number of registers to write
size = len(self.get_all_registers())
# Add 2 for the flags
size += 2
return size
def write_flags(self, file):
flags = 0
if self.get_register('hide_sprite') is not None:
flags |= 1 << 0
if self.get_register('sprite') is not None:
flags |= 1 << 1
if self.get_register('palette') is not None:
flags |= 1 << 2
if self.palette_from_action1:
flags |= 1 << 3
# for building sprites: bit 4 => xoffset+yoffset, bit 5 => zoffset (x and y always set totgether)
# for child sprites: bit 4 => xoffset, bit 5 => yoffset
if self.type == Action2LayoutSpriteType.BUILDING:
assert (self.get_register('xoffset') is not None) == (self.get_register('yoffset') is not None)
if self.get_register('xoffset') is not None:
flags |= 1 << 4
nextreg = 'zoffset' if self.type == Action2LayoutSpriteType.BUILDING else 'yoffset'
if self.get_register(nextreg) is not None:
flags |= 1 << 5
file.print_wordx(flags)
def write_register(self, file, name):
register = self.get_register(name)[0]
file.print_bytex(register.parameter)
def write_registers(self, file):
if self.is_set('hide_sprite'):
self.write_register(file, 'hide_sprite')
if self.get_register('sprite') is not None:
self.write_register(file, 'sprite')
if self.get_register('palette') is not None:
self.write_register(file, 'palette')
if self.get_register('xoffset') is not None:
self.write_register(file, 'xoffset')
if self.get_register('yoffset') is not None:
self.write_register(file, 'yoffset')
if self.get_register('zoffset') is not None:
self.write_register(file, 'zoffset')
def write_sprite_number(self, file):
num = self.get_sprite_number()
if isinstance(num, expression.ConstantNumeric):
num.write(file, 4)
else:
file.print_dwordx(0)
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_sprite = False
sprite = self.get_param('sprite')
if isinstance(sprite, expression.ConstantNumeric):
sprite_num |= sprite.value
else:
add_sprite = True
add_palette = False
palette = self.get_param('palette')
if isinstance(palette, expression.ConstantNumeric):
sprite_num |= palette.value << 16
else:
add_palette = True
expr = expression.ConstantNumeric(sprite_num, sprite.pos)
if add_sprite:
expr = expression.BinOp(nmlop.ADD, sprite, expr, sprite.pos)
if add_palette:
expr = expression.BinOp(nmlop.ADD, palette, expr, sprite.pos)
return expr.reduce()
def get_param(self, name):
assert name in self.params
return self.params[name]['value']
def is_set(self, name):
assert name in self.params
return self.params[name]['is_set']
def get_register(self, name):
assert name in self.params
return self.params[name]['register']
def get_all_registers(self):
return [self.get_register(name) for name in sorted(self.params) if self.get_register(name) is not None]
def create_register(self, name, value):
if isinstance(value, expression.StorageOp) and value.name == "LOAD_TEMP" and isinstance(value.register, expression.ConstantNumeric):
store_tmp = None
load_tmp = action2var.VarAction2Var(0x7D, 0, 0xFFFFFFFF, value.register.value)
else:
store_tmp = action2var.VarAction2StoreTempVar()
load_tmp = action2var.VarAction2LoadTempVar(store_tmp)
self.params[name]['register'] = (load_tmp, store_tmp, value)
def set_param(self, name, value):
assert isinstance(name, expression.Identifier)
assert isinstance(value, expression.Expression)
name = name.value
if not name in self.params:
raise generic.ScriptError("Unknown sprite parameter '{}'".format(name), value.pos)
if self.is_set(name):
raise generic.ScriptError("Sprite parameter '{}' can be set only once per sprite.".format(name), value.pos)
self.params[name]['value'] = self.params[name]['validator'](name, value)
self.params[name]['is_set'] = True
def resolve_spritegroup_ref(self, sg_ref):
"""
Resolve a reference to a (sprite/palette) sprite group
@param sg_ref: Reference to a sprite group
@type sg_ref: L{SpriteGroupRef}
@return: Sprite number (index of action1 set) to use
@rtype: L{Expression}
"""
spriteset = action2.resolve_spritegroup(sg_ref.name)
if len(sg_ref.param_list) == 0:
offset = None
elif len(sg_ref.param_list) == 1:
id_dicts = [(spriteset.labels, lambda val, pos: expression.ConstantNumeric(val, pos))]
offset = action2var.reduce_varaction2_expr(sg_ref.param_list[0], self.feature, self.extra_dicts + id_dicts)
if isinstance(offset, expression.ConstantNumeric):
generic.check_range(offset.value, 0, len(real_sprite.parse_sprite_data(spriteset)) - 1, "offset within spriteset", sg_ref.pos)
else:
raise generic.ScriptError("Expected 0 or 1 parameter, got " + str(len(sg_ref.param_list)), sg_ref.pos)
num = action1.get_action1_index(spriteset)
generic.check_range(num, 0, (1 << 14) - 1, "sprite", sg_ref.pos)
return expression.ConstantNumeric(num), offset
def _validate_sprite(self, name, value):
if isinstance(value, expression.SpriteGroupRef):
self.sprite_from_action1 = True
val, offset = self.resolve_spritegroup_ref(value)
if offset is not None:
self.create_register(name, offset)
return val
else:
self.sprite_from_action1 = False
if isinstance(value, expression.ConstantNumeric):
generic.check_range(value.value, 0, (1 << 14) - 1, "sprite", value.pos)
return value
self.create_register(name, value)
return expression.ConstantNumeric(0)
def _validate_recolour_mode(self, name, value):
if not isinstance(value, expression.ConstantNumeric):
raise generic.ScriptError("Expected a compile-time constant.", value.pos)
if not value.value in (0, 1, 2):
raise generic.ScriptError("Value of 'recolour_mode' must be RECOLOUR_NONE, RECOLOUR_TRANSPARENT or RECOLOUR_REMAP.")
return value.value
def _validate_palette(self, name, value):
if isinstance(value, expression.SpriteGroupRef):
self.palette_from_action1 = True
val, offset = self.resolve_spritegroup_ref(value)
if offset is not None:
self.create_register(name, offset)
return val
else:
if isinstance(value, expression.ConstantNumeric):
generic.check_range(value.value, 0, (1 << 14) - 1, "palette", value.pos)
self.palette_from_action1 = False
return value
def _validate_always_draw(self, name, value):
if not isinstance(value, expression.ConstantNumeric):
raise generic.ScriptError("Expected a compile-time constant number.", value.pos)
# Not valid for ground sprites, raise error
if self.type == Action2LayoutSpriteType.GROUND:
raise generic.ScriptError("'always_draw' may not be set for groundsprites, these are always drawn anyways.", value.pos)
if value.value not in (0, 1):
raise generic.ScriptError("Value of 'always_draw' should be 0 or 1", value.pos)
return value.value
def _validate_bounding_box(self, name, value):
if self.type == Action2LayoutSpriteType.GROUND:
raise generic.ScriptError(name + " can not be set for ground sprites", value.pos)
elif self.type == Action2LayoutSpriteType.CHILD:
if name not in ('xoffset', 'yoffset'):
raise generic.ScriptError(name + " can not be set for child sprites", value.pos)
if isinstance(value, expression.ConstantNumeric):
generic.check_range(value.value, 0, 255, name, value.pos)
return value
else:
assert self.type == Action2LayoutSpriteType.BUILDING
if name in ('xoffset', 'yoffset', 'zoffset'):
if isinstance(value, expression.ConstantNumeric):
generic.check_range(value.value, -128, 127, name, value.pos)
return value
else:
assert name in ('xextent', 'yextent', 'zextent')
if not isinstance(value, expression.ConstantNumeric):
raise generic.ScriptError("Value of '{}' must be a compile-time constant number.".format(name), value.pos)
generic.check_range(value.value, 0, 255, name, value.pos)
return value
# Value must be written to a register
self.create_register(name, value)
if self.type == Action2LayoutSpriteType.BUILDING:
# For building sprites, x and y registers are always written together
if name == 'xoffset' and self.get_register('yoffset') is None:
self.create_register('yoffset', expression.ConstantNumeric(0))
if name == 'yoffset' and self.get_register('xoffset') is None:
self.create_register('xoffset', expression.ConstantNumeric(0))
return expression.ConstantNumeric(0)
def _validate_hide_sprite(self, name, value):
value = expression.Not(value)
try:
value = value.reduce()
except:
pass
self.create_register(name, value)
return None
def get_layout_action2s(spritelayout, feature, spr_pos):
"""
@param spr_pos: Position information of the sprite view.
@type spr_pos: L{Position}
"""
ground_sprite = None
building_sprites = []
actions = []
if feature not in action2.features_sprite_layout:
raise generic.ScriptError("Sprite layouts are not supported for feature '{}'.".format(general.feature_name(feature)))
# Allocate registers
param_map = {}
param_registers = []
for i, param in enumerate(spritelayout.param_list):
reg = action2var.VarAction2LayoutParam()
param_registers.append(reg)
param_map[param.value] = reg
param_map = (param_map, lambda value, pos: action2var.VarAction2LoadLayoutParam(value))
spritelayout.register_map[feature] = param_registers
# Reduce all expressions, can't do that earlier as feature is not known
all_sprite_sets = []
layout_sprite_list = [] # Create a new structure
for layout_sprite in spritelayout.layout_sprite_list:
param_list = []
layout_sprite_list.append( (layout_sprite.type, layout_sprite.pos, param_list) )
for param in layout_sprite.param_list:
param_val = action2var.reduce_varaction2_expr(param.value, feature, [param_map])
param_list.append( (param.name, param_val) )
if isinstance(param_val, expression.SpriteGroupRef):
spriteset = action2.resolve_spritegroup(param_val.name)
if not spriteset.is_spriteset():
raise generic.ScriptError("Expected a reference to a spriteset.", param_val.pos)
all_sprite_sets.append(spriteset)
actions.extend(action1.add_to_action1(all_sprite_sets, feature, spritelayout.pos))
temp_registers = []
for type, pos, param_list in layout_sprite_list:
if type.value not in layout_sprite_types:
raise generic.ScriptError("Invalid sprite type '{}' encountered. Expected 'ground', 'building', or 'childsprite'.".format(type.value), type.pos)
sprite = Action2LayoutSprite(feature, layout_sprite_types[type.value], pos, [param_map])
for name, value in param_list:
sprite.set_param(name, value)
temp_registers.extend(sprite.get_all_registers())
if sprite.type == Action2LayoutSpriteType.GROUND:
if ground_sprite is not None:
raise generic.ScriptError("Sprite layout can have no more than one ground sprite", spritelayout.pos)
ground_sprite = sprite
else:
building_sprites.append(sprite)
if ground_sprite is None:
if len(building_sprites) == 0:
#no sprites defined at all, that's not very much.
raise generic.ScriptError("Sprite layout requires at least one sprite", spr_pos)
#set to 0 for no ground sprite
ground_sprite = Action2LayoutSprite(feature, Action2LayoutSpriteType.GROUND)
ground_sprite.set_param(expression.Identifier('sprite'), expression.ConstantNumeric(0))
action6.free_parameters.save()
act6 = action6.Action6()
offset = 4
sprite_num = ground_sprite.get_sprite_number()
sprite_num, offset = actionD.write_action_value(sprite_num, actions, act6, offset, 4)
offset += ground_sprite.get_registers_size()
for sprite in building_sprites:
sprite_num = sprite.get_sprite_number()
sprite_num, offset = actionD.write_action_value(sprite_num, actions, act6, offset, 4)
offset += sprite.get_registers_size()
offset += 3 if sprite.type == Action2LayoutSpriteType.CHILD else 6
if len(act6.modifications) > 0:
actions.append(act6)
layout_action = Action2Layout(feature, spritelayout.name.value + " - feature {:02X}".format(feature), spritelayout.pos, ground_sprite, building_sprites, param_registers)
actions.append(layout_action)
if temp_registers:
varact2parser = action2var.Varaction2Parser(feature)
for register_info in temp_registers:
reg, expr = register_info[1], register_info[2]
if reg is None: continue
varact2parser.parse_expr(expr)
varact2parser.var_list.append(nmlop.STO_TMP)
varact2parser.var_list.append(reg)
varact2parser.var_list.append(nmlop.VAL2)
varact2parser.var_list_size += reg.get_size() + 2
# Only continue if we actually needed any new registers
if temp_registers and varact2parser.var_list:
#Remove the last VAL2 operator
varact2parser.var_list.pop()
varact2parser.var_list_size -= 1
actions.extend(varact2parser.extra_actions)
extra_act6 = action6.Action6()
for mod in varact2parser.mods:
extra_act6.modify_bytes(mod.param, mod.size, mod.offset + 4)
if len(extra_act6.modifications) > 0: actions.append(extra_act6)
varaction2 = action2var.Action2Var(feature, "{}@registers - feature {:02X}".format(spritelayout.name.value, feature), spritelayout.pos, 0x89)
varaction2.var_list = varact2parser.var_list
ref = expression.SpriteGroupRef(spritelayout.name, [], None, layout_action)
varaction2.ranges.append(action2var.VarAction2Range(expression.ConstantNumeric(0), expression.ConstantNumeric(0), ref, ''))
varaction2.default_result = ref
varaction2.default_comment = ''
# Add two references (default + range)
# Make sure that registers allocated here are not used by the spritelayout
action2.add_ref(ref, varaction2, True)
action2.add_ref(ref, varaction2, True)
spritelayout.set_action2(varaction2, feature)
actions.append(varaction2)
else:
spritelayout.set_action2(layout_action, feature)
action6.free_parameters.restore()
return actions
def make_empty_layout_action2(feature, pos):
"""
Make an empty layout action2
For use with failed callbacks
@param feature: Feature of the sprite layout to create
@type feature: C{int}
@param pos: Positional context.
@type pos: L{Position}
@return: The created sprite layout action2
@rtype: L{Action2Layout}
"""
ground_sprite = Action2LayoutSprite(feature, Action2LayoutSpriteType.GROUND)
ground_sprite.set_param(expression.Identifier('sprite'), expression.ConstantNumeric(0))
return Action2Layout(feature, "@CB_FAILED_LAYOUT{:02X}".format(feature), pos, ground_sprite, [], [])
nml-0.4.5/nml/actions/action2var_variables.py 0000644 0005672 0005672 00000126252 13315644406 022366 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import expression, nmlop, generic
# Use feature 0x12 for towns (accessible via station/house/industry parent scope)
varact2vars = 0x13 * [{}]
varact2vars60x = 0x13 * [{}]
# feature number: 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12
varact2parent_scope = [0x00, 0x01, 0x02, 0x03, 0x12, None, 0x12, 0x12, None, 0x0A, 0x12, None, None, None, None, 0x12, None, None, None]
def default_60xvar(name, args, pos, info):
"""
Function to convert arguments into a variable parameter.
This function handles the default case of one argument.
@param name: Name of the variable
@type name: C{str}
@param args: List of passed arguments
@type args: C{list} of L{Expression}
@param pos: Position information
@type pos: L{Position}
@param info: Information of the variable, as found in the dictionary
@type info: C{dict}
@return: A tuple of two values:
- Parameter to use for the 60+x variable
- List of possible extra parameters that need to be passed via registers
@rtype: C{tuple} of (L{Expression}, C{list} C{tuple} of (C{int}, L{Expression}))
"""
if len(args) != 1:
raise generic.ScriptError("'{}'() requires one argument, encountered {:d}".format(name, len(args)), pos)
return (args[0], [])
# Some commonly used functions that apply some modification to the raw variable value
# To pass extra parameters, lambda calculus may be used
def value_sign_extend(var, info):
#r = (x ^ m) - m; with m being (1 << (num_bits -1))
m = expression.ConstantNumeric(1 << (info['size'] - 1))
return expression.BinOp(nmlop.SUB, expression.BinOp(nmlop.XOR, var, m, var.pos), m, var.pos)
def value_mul_div(mul, div):
return lambda var, info: expression.BinOp(nmlop.DIV,
expression.BinOp(nmlop.MUL, var, expression.ConstantNumeric(mul, var.pos), var.pos),
expression.ConstantNumeric(div, var.pos), var.pos)
def value_add_constant(const):
return lambda var, info: expression.BinOp(nmlop.ADD, var, expression.ConstantNumeric(const, var.pos), var.pos)
def value_equals(const):
return lambda var, info: expression.BinOp(nmlop.CMP_EQ, var, expression.ConstantNumeric(const, var.pos), var.pos)
# Commonly used functions to let a variable accept an (x, y)-offset as parameters
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 = expression.BinOp(nmlop.AND, args[0], expression.ConstantNumeric(0xF), args[0].pos)
y = expression.BinOp(nmlop.AND, args[1], expression.ConstantNumeric(0xF), args[1].pos)
# Shift y left by four
y = expression.BinOp(nmlop.SHIFT_LEFT, y, expression.ConstantNumeric(4), y.pos)
param = expression.BinOp(nmlop.ADD, x, y, x.pos)
#Make sure to reduce the result
return ( param.reduce(), [] )
def signed_tile_offset(name, args, pos, info):
return tile_offset(name, args, pos, info, -8, 7)
def unsigned_tile_offset(name, args, pos, info):
return tile_offset(name, args, pos, info, 0, 15)
#
# Global variables, usable for all features
#
varact2_globalvars = {
'current_month' : {'var': 0x02, 'start': 0, 'size': 8},
'current_day_of_month' : {'var': 0x02, 'start': 8, 'size': 5},
'is_leapyear' : {'var': 0x02, 'start': 15, 'size': 1},
'current_day_of_year' : {'var': 0x02, 'start': 16, 'size': 9},
'traffic_side' : {'var': 0x06, 'start': 0, 'size': 8},
'animation_counter' : {'var': 0x0A, 'start': 0, 'size': 16},
'current_callback' : {'var': 0x0C, 'start': 0, 'size': 16},
'extra_callback_info1' : {'var': 0x10, 'start': 0, 'size': 32},
'game_mode' : {'var': 0x12, 'start': 0, 'size': 8},
'extra_callback_info2' : {'var': 0x18, 'start': 0, 'size': 32},
'display_options' : {'var': 0x1B, 'start': 0, 'size': 6},
'last_computed_result' : {'var': 0x1C, 'start': 0, 'size': 32},
'snowline_height' : {'var': 0x20, 'start': 0, 'size': 8},
'difficulty_level' : {'var': 0x22, 'start': 0, 'size': 8},
'current_date' : {'var': 0x23, 'start': 0, 'size': 32},
'current_year' : {'var': 0x24, 'start': 0, 'size': 32},
}
#
# Vehicles (features 0x00 - 0x03)
# A few variables have an implementation that differs per vehicle type
# These are to be found below
#
varact2vars_vehicles = {
'grfid' : {'var': 0x25, 'start': 0, 'size': 32},
'position_in_consist' : {'var': 0x40, 'start': 0, 'size': 8},
'position_in_consist_from_end' : {'var': 0x40, 'start': 8, 'size': 8},
'num_vehs_in_consist' : {'var': 0x40, 'start': 16, 'size': 8, 'value_function': value_add_constant(1)}, # Zero-based, add 1 to make sane
'position_in_vehid_chain' : {'var': 0x41, 'start': 0, 'size': 8},
'position_in_vehid_chain_from_end' : {'var': 0x41, 'start': 8, 'size': 8},
'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: expression.BinOp(nmlop.AND, var, expression.ConstantNumeric(0x0F0F, var.pos), var.pos).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},
'current_railtype' : {'var': 0x4A, 'start': 0, 'size': 8},
'vehicle_is_potentially_powered' : {'var': 0x4A, 'start': 8, '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},
'direction' : {'var': 0x9F, 'start': 0, 'size': 8},
'vehicle_is_hidden' : {'var': 0xB2, 'start': 0, 'size': 1},
'vehicle_is_stopped' : {'var': 0xB2, 'start': 1, 'size': 1},
'vehicle_is_crashed' : {'var': 0xB2, 'start': 7, 'size': 1},
'cargo_capacity' : {'var': 0xBA, 'start': 0, 'size': 16},
'cargo_count' : {'var': 0xBC, 'start': 0, 'size': 16},
'age_in_days' : {'var': 0xC0, 'start': 0, 'size': 16},
'max_age_in_days' : {'var': 0xC2, 'start': 0, 'size': 16},
'vehicle_type_id' : {'var': 0xC6, 'start': 0, 'size': 16},
'vehicle_is_flipped' : {'var': 0xC8, 'start': 1, 'size': 1},
'breakdowns_since_last_service' : {'var': 0xCA, 'start': 0, 'size': 8},
'vehicle_is_broken' : {'var': 0xCB, 'start': 0, 'size': 8, 'value_function': value_equals(1)},
'reliability' : {'var': 0xCE, 'start': 0, 'size': 16, 'value_function': value_mul_div(101, 0x10000)},
'cargo_subtype' : {'var': 0xF2, 'start': 0, 'size': 8},
'vehicle_is_unloading' : {'var': 0xFE, 'start': 1, 'size': 1},
'vehicle_is_powered' : {'var': 0xFE, 'start': 5, 'size': 1},
'vehicle_is_not_powered' : {'var': 0xFE, 'start': 6, 'size': 1},
'vehicle_is_reversed' : {'var': 0xFE, 'start': 8, 'size': 1},
'built_during_preview' : {'var': 0xFE, 'start': 10, 'size': 1},
}
# Vehicle-type-specific variables
varact2vars_trains = {
#0x4786 / 0x10000 is an approximation of 3.5790976, the conversion factor
#for train speed
'max_speed' : {'var': 0x98, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x4786, 0x10000)},
'current_speed' : {'var': 0xB4, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x4786, 0x10000)},
'current_max_speed' : {'var': 0x4C, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x4786, 0x10000)},
'vehicle_is_in_depot' : {'var': 0xE2, 'start': 7, 'size': 1},
}
varact2vars_trains.update(varact2vars_vehicles)
varact2vars_roadvehs = {
#0x23C3 / 0x10000 is an approximation of 7.1581952, the conversion factor
#for road vehicle speed
'max_speed' : {'var': 0x98, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x23C3, 0x10000)},
'current_speed' : {'var': 0xB4, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x23C3, 0x10000)},
'current_max_speed' : {'var': 0x4C, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x23C3, 0x10000)},
'vehicle_is_in_depot' : {'var': 0xE2, 'start': 0, 'size': 8, 'value_function': value_equals(0xFE)},
}
varact2vars_roadvehs.update(varact2vars_vehicles)
varact2vars_ships = {
#0x23C3 / 0x10000 is an approximation of 7.1581952, the conversion factor
#for ship speed
'max_speed' : {'var': 0x98, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x23C3, 0x10000)},
'current_speed' : {'var': 0xB4, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x23C3, 0x10000)},
'current_max_speed' : {'var': 0x4C, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x23C3, 0x10000)},
'vehicle_is_in_depot' : {'var': 0xE2, 'start': 7, 'size': 1},
}
varact2vars_ships.update(varact2vars_vehicles)
varact2vars_aircraft = {
#0x3939 / 0x1000 is an approximation of 0.279617, the conversion factor
#Note that the denominator has one less zero here!
#for aircraft speed
'max_speed' : {'var': 0x98, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x3939, 0x1000)},
'current_speed' : {'var': 0xB4, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x3939, 0x1000)},
'current_max_speed' : {'var': 0x4C, 'start': 0, 'size': 16, 'value_function': value_mul_div(0x3939, 0x1000)},
'vehicle_is_in_depot' : {'var': 0xE6, 'start': 0, 'size': 8, 'value_function': value_equals(0)},
}
varact2vars_aircraft.update(varact2vars_vehicles)
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 = expression.BinOp(nmlop.AND, args[0], expression.ConstantNumeric(0xFF, pos), pos).reduce()
return (ret, [])
varact2vars60x_vehicles = {
'count_veh_id' : {'var': 0x60, 'start': 0, 'size': 8},
'other_veh_curv_info' : {'var': 0x62, 'start': 0, 'size': 4, 'param_function':signed_byte_parameter, 'value_function':value_sign_extend},
'other_veh_is_hidden' : {'var': 0x62, 'start': 7, 'size': 1, 'param_function':signed_byte_parameter},
'other_veh_x_offset' : {'var': 0x62, 'start': 8, 'size': 8, 'param_function':signed_byte_parameter, 'value_function':value_sign_extend},
'other_veh_y_offset' : {'var': 0x62, 'start': 16, 'size': 8, 'param_function':signed_byte_parameter, 'value_function':value_sign_extend},
'other_veh_z_offset' : {'var': 0x62, 'start': 24, 'size': 8, 'param_function':signed_byte_parameter, 'value_function':value_sign_extend},
}
#
# Stations (feature 0x04)
#
# 'Base station' variables are shared between stations and airports
varact2vars_base_stations = {
# Var 48 doesn't work with newcargos, do not use
'had_vehicle_of_type' : {'var': 0x8A, 'start': 1, 'size': 5}, # Only read bits 1-5
'is_waypoint' : {'var': 0x8A, 'start': 6, 'size': 1},
'facilities' : {'var': 0xF0, 'start': 0, 'size': 8},
'airport_type' : {'var': 0xF1, 'start': 0, 'size': 8},
# Variables F2, F3, F6 (roadstop, airport flags) are next to useless
# Also, their values are not the same as in TTDP / spec
# Therefore, these are not implemented
'build_date' : {'var': 0xFA, 'start': 0, 'size': 16, 'value_function': value_add_constant(701265)}
}
varact2vars60x_base_stations = {
'cargo_amount_waiting' : {'var': 0x60, 'start': 0, 'size': 32},
'cargo_time_since_pickup' : {'var': 0x61, 'start': 0, 'size': 32},
'cargo_rating' : {'var': 0x62, 'start': 0, 'size': 32, 'value_function': value_mul_div(101, 256)},
'cargo_time_en_route' : {'var': 0x63, 'start': 0, 'size': 32},
'cargo_last_vehicle_speed' : {'var': 0x64, 'start': 0, 'size': 8},
'cargo_last_vehicle_age' : {'var': 0x64, 'start': 8, 'size': 8},
'cargo_accepted' : {'var': 0x65, 'start': 3, 'size': 1},
'cargo_accepted_ever' : {'var': 0x69, 'start': 0, 'size': 1},
'cargo_accepted_last_month' : {'var': 0x69, 'start': 1, 'size': 1},
'cargo_accepted_this_month' : {'var': 0x69, 'start': 2, 'size': 1},
'cargo_accepted_bigtick' : {'var': 0x69, 'start': 3, 'size': 1},
}
varact2vars_stations = {
# Vars 40, 41, 46, 47, 49 are implemented as 60+x vars,
# except for the 'tile type' part which is always the same anyways
'tile_type' : {'var': 0x40, 'start': 24, 'size': 4},
'terrain_type' : {'var': 0x42, 'start': 0, 'size': 8},
'track_type' : {'var': 0x42, 'start': 8, '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},
'pbs_reserved' : {'var': 0x44, 'start': 0, 'size': 1},
'pbs_reserved_or_disabled' : {'var': 0x44, 'start': 1, 'size': 1},
'pbs_enabled' : {'var': 0x44, 'start': 2, 'size': 1},
'rail_continuation' : {'var': 0x45, 'start': 0, 'size': 8},
'rail_present' : {'var': 0x45, 'start': 8, 'size': 8},
'animation_frame' : {'var': 0x4A, 'start': 0, 'size': 8},
}
varact2vars_stations.update(varact2vars_base_stations)
# Mapping of param values for platform_xx vars to variable numbers
mapping_platform_param = {
(0, False) : 0x40,
(1, False) : 0x41,
(0, True) : 0x46,
(1, True) : 0x47,
(2, False) : 0x49,
}
def platform_info_param(name, args, pos, info):
if len(args) != 1:
raise generic.ScriptError("'{}'() requires one argument, encountered {:d}".format(name, len(args)), pos)
if (not isinstance(args[0], expression.ConstantNumeric)) or args[0].value not in (0, 1, 2):
raise generic.ScriptError("Invalid argument for '{}'(), must be one of PLATFORM_SAME_XXX.".format(name), pos)
is_middle = 'middle' in info and info['middle']
if is_middle and args[0].value == 2:
raise generic.ScriptError("Invalid argument for '{}'(), PLATFORM_SAME_DIRECTION is not supported here.".format(name), pos)
# Temporarily store variable number in the param, this will be fixed in the value_function
return (expression.ConstantNumeric(mapping_platform_param[(args[0].value, is_middle)]), [])
def platform_info_fix_var(var, info):
# Variable to use was temporarily stored in the param
# Fix this now
var.num = var.param
var.param = None
return var
varact2vars60x_stations = {
'nearby_tile_animation_frame' : {'var': 0x66, 'start': 0, 'size': 32, 'param_function': signed_tile_offset},
'nearby_tile_slope' : {'var': 0x67, 'start': 0, 'size': 5, 'param_function': signed_tile_offset},
'nearby_tile_is_water' : {'var': 0x67, 'start': 9, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_terrain_type' : {'var': 0x67, 'start': 10, 'size': 3, 'param_function': signed_tile_offset},
'nearby_tile_water_class' : {'var': 0x67, 'start': 13, 'size': 2, 'param_function': signed_tile_offset},
'nearby_tile_height' : {'var': 0x67, 'start': 16, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_class' : {'var': 0x67, 'start': 24, 'size': 4, 'param_function': signed_tile_offset},
'nearby_tile_is_station' : {'var': 0x68, 'start': 0, 'size': 32, 'param_function': signed_tile_offset, 'value_function': lambda var, info: expression.BinOp(nmlop.CMP_NEQ, var, expression.ConstantNumeric(0xFFFFFFFF, var.pos), var.pos)},
'nearby_tile_station_id' : {'var': 0x68, 'start': 0, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_same_grf' : {'var': 0x68, 'start': 8, 'size': 2, 'param_function': signed_tile_offset, 'value_function': value_equals(0)},
'nearby_tile_other_grf' : {'var': 0x68, 'start': 8, 'size': 2, 'param_function': signed_tile_offset, 'value_function': value_equals(1)},
'nearby_tile_original_gfx' : {'var': 0x68, 'start': 8, 'size': 2, 'param_function': signed_tile_offset, 'value_function': value_equals(2)},
'nearby_tile_same_station' : {'var': 0x68, 'start': 10, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_perpendicular' : {'var': 0x68, 'start': 11, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_platform_type' : {'var': 0x68, 'start': 12, 'size': 2, 'param_function': signed_tile_offset},
# 'var' will be set in the value_function, depending on parameter
'platform_length' : {'var': 0x00, 'start': 16, 'size': 4, 'param_function': platform_info_param, 'value_function': platform_info_fix_var},
'platform_count' : {'var': 0x00, 'start': 20, 'size': 4, 'param_function': platform_info_param, 'value_function': platform_info_fix_var},
'platform_position_from_start' : {'var': 0x00, 'start': 0, 'size': 4, 'param_function': platform_info_param, 'value_function': platform_info_fix_var},
'platform_position_from_end' : {'var': 0x00, 'start': 4, 'size': 4, 'param_function': platform_info_param, 'value_function': platform_info_fix_var},
'platform_number_from_start' : {'var': 0x00, 'start': 8, 'size': 4, 'param_function': platform_info_param, 'value_function': platform_info_fix_var},
'platform_number_from_end' : {'var': 0x00, 'start': 12, 'size': 4, 'param_function': platform_info_param, 'value_function': platform_info_fix_var},
'platform_position_from_middle' : {'var': 0x00, 'start': 0, 'size': 4, 'param_function': platform_info_param, 'middle': True, # 'middle' is used by platform_info_param
'value_function': lambda var, info: value_sign_extend(platform_info_fix_var(var, info), info)},
'platform_number_from_middle' : {'var': 0x00, 'start': 4, 'size': 4, 'param_function': platform_info_param, 'middle': True, # 'middle' is used by platform_info_param
'value_function': lambda var, info: value_sign_extend(platform_info_fix_var(var, info), info)},
}
varact2vars60x_stations.update(varact2vars60x_base_stations)
#
# Canals (feature 0x05)
#
varact2vars_canals = {
'tile_height' : {'var': 0x80, 'start': 0, 'size': 8},
'terrain_type' : {'var': 0x81, 'start': 0, 'size': 8},
'dike_map' : {'var': 0x82, 'start': 0, 'size': 8},
'random_bits' : {'var': 0x83, 'start': 0, 'size': 8},
}
# Canals have no 60+X variables
#
# Bridges (feature 0x06) have no variational action2
#
#
# Houses (feature 0x07)
#
def house_same_class(var, info):
# Just using var 44 fails for non-north house tiles, as these have no class
# Therefore work around it using var 61
# Load ID of the north tile from register FF bits 24..31, and use that as param for var 61
north_tile = expression.Variable(expression.ConstantNumeric(0x7D), expression.ConstantNumeric(24),
expression.ConstantNumeric(0xFF), expression.ConstantNumeric(0xFF), var.pos)
var61 = expression.Variable(expression.ConstantNumeric(0x7B), expression.ConstantNumeric(info['start']),
expression.ConstantNumeric((1 << info['size']) - 1), expression.ConstantNumeric(0x61), var.pos)
return expression.BinOp(nmlop.VAL2, north_tile, var61, var.pos)
varact2vars_houses = {
'construction_state' : {'var': 0x40, 'start': 0, 'size': 2},
'pseudo_random_bits' : {'var': 0x40, 'start': 2, 'size': 2},
'age' : {'var': 0x41, 'start': 0, 'size': 8},
'town_zone' : {'var': 0x42, 'start': 0, 'size': 8},
'terrain_type' : {'var': 0x43, 'start': 0, 'size': 8},
'same_house_count_town' : {'var': 0x44, 'start': 0, 'size': 8},
'same_house_count_map' : {'var': 0x44, 'start': 8, 'size': 8},
'same_class_count_town' : {'var': 0xFF, 'start': 16, 'size': 8, 'value_function': house_same_class}, # 'var' is unused
'same_class_count_map' : {'var': 0xFF, 'start': 24, 'size': 8, 'value_function': house_same_class}, # 'var' is unused
'generating_town' : {'var': 0x45, 'start': 0, 'size': 1},
'animation_frame' : {'var': 0x46, 'start': 0, 'size': 8},
'x_coordinate' : {'var': 0x47, 'start': 0, 'size': 16},
'y_coordinate' : {'var': 0x47, 'start': 16, 'size': 16},
'random_bits' : {'var': 0x5F, 'start': 8, 'size': 8},
'relative_x' : {'var': 0x7D, 'start': 0, 'size': 8, 'param': 0xFF},
'relative_y' : {'var': 0x7D, 'start': 8, 'size': 8, 'param': 0xFF},
'relative_pos' : {'var': 0x7D, 'start': 0, 'size': 16, 'param': 0xFF},
'house_tile' : {'var': 0x7D, 'start': 16, 'size': 8, 'param': 0xFF},
'house_type_id' : {'var': 0x7D, 'start': 24, 'size': 8, 'param': 0xFF},
}
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] = expression.BinOp(nmlop.AND, offs, expression.ConstantNumeric(0xFF, pos), pos).reduce()
# Register 0x100 should be set to xoffset | (yoffset << 8)
reg100 = expression.BinOp(nmlop.OR, expression.BinOp(nmlop.MUL, offsets[1], expression.ConstantNumeric(256, pos), pos), offsets[0], pos).reduce()
else:
reg100 = expression.ConstantNumeric(0, pos)
return (args[0], [(0x100, reg100)])
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 = expression.BinOp(nmlop.AND, args[0], expression.ConstantNumeric(0x3F, pos), pos)
criterion = expression.BinOp(nmlop.AND, args[1], expression.ConstantNumeric(0x03, pos), pos)
criterion = expression.BinOp(nmlop.MUL, criterion, expression.ConstantNumeric(0x40, pos), pos)
retval = expression.BinOp(nmlop.OR, criterion, radius, pos).reduce()
return (retval, [])
varact2vars60x_houses = {
'old_house_count_town' : {'var': 0x60, 'start': 0, 'size': 8},
'old_house_count_map' : {'var': 0x60, 'start': 8, 'size': 8},
'other_house_count_town' : {'var': 0x61, 'start': 0, 'size': 8},
'other_house_count_map' : {'var': 0x61, 'start': 8, 'size': 8},
'other_class_count_town' : {'var': 0x61, 'start': 16, 'size': 8},
'other_class_count_map' : {'var': 0x61, 'start': 24, 'size': 8},
'nearby_tile_slope' : {'var': 0x62, 'start': 0, 'size': 5, 'param_function': signed_tile_offset},
'nearby_tile_is_water' : {'var': 0x62, 'start': 9, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_terrain_type' : {'var': 0x62, 'start': 10, 'size': 3, 'param_function': signed_tile_offset},
'nearby_tile_water_class' : {'var': 0x62, 'start': 13, 'size': 2, 'param_function': signed_tile_offset},
'nearby_tile_height' : {'var': 0x62, 'start': 16, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_class' : {'var': 0x62, 'start': 24, 'size': 4, 'param_function': signed_tile_offset},
'nearby_tile_animation_frame' : {'var': 0x63, 'start': 0, 'size': 8, 'param_function': signed_tile_offset},
'cargo_accepted_nearby_ever' : {'var': 0x64, 'start': 0, 'size': 1, 'param_function': cargo_accepted_nearby},
'cargo_accepted_nearby_last_month' : {'var': 0x64, 'start': 1, 'size': 1, 'param_function': cargo_accepted_nearby},
'cargo_accepted_nearby_this_month' : {'var': 0x64, 'start': 2, 'size': 1, 'param_function': cargo_accepted_nearby},
'cargo_accepted_nearby_last_bigtick' : {'var': 0x64, 'start': 3, 'size': 1, 'param_function': cargo_accepted_nearby},
'cargo_accepted_nearby_watched' : {'var': 0x64, 'start': 4, 'size': 1, 'param_function': cargo_accepted_nearby},
'nearest_house_matching_criterion' : {'var': 0x65, 'start': 0, 'size': 8, 'param_function': nearest_house_matching_criterion},
'nearby_tile_house_id' : {'var': 0x66, 'start': 0, 'size': 16, 'param_function': signed_tile_offset, 'value_function': value_sign_extend},
'nearby_tile_house_class' : {'var': 0x66, 'start': 16, 'size': 16, 'param_function': signed_tile_offset, 'value_function': value_sign_extend},
'nearby_tile_house_grfid' : {'var': 0x67, 'start': 0, 'size': 32, 'param_function': signed_tile_offset},
}
#
# Global variables (feature 0x08) have no variational action2
#
#
# Industry tiles (feature 0x09)
#
varact2vars_industrytiles = {
'construction_state' : {'var': 0x40, 'start': 0, 'size': 2},
'terrain_type' : {'var': 0x41, 'start': 0, 'size': 8},
'town_zone' : {'var': 0x42, 'start': 0, 'size': 3},
'relative_x' : {'var': 0x43, 'start': 0, 'size': 8},
'relative_y' : {'var': 0x43, 'start': 8, 'size': 8},
'relative_pos' : {'var': 0x43, 'start': 0, 'size': 16},
'animation_frame' : {'var': 0x44, 'start': 0, 'size': 8},
'random_bits' : {'var': 0x5F, 'start': 8, 'size': 8},
}
varact2vars60x_industrytiles = {
'nearby_tile_slope' : {'var': 0x60, 'start': 0, 'size': 5, 'param_function': signed_tile_offset},
'nearby_tile_is_same_industry' : {'var': 0x60, 'start': 8, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_is_water' : {'var': 0x60, 'start': 9, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_terrain_type' : {'var': 0x60, 'start': 10, 'size': 3, 'param_function': signed_tile_offset},
'nearby_tile_water_class' : {'var': 0x60, 'start': 13, 'size': 2, 'param_function': signed_tile_offset},
'nearby_tile_height' : {'var': 0x60, 'start': 16, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_class' : {'var': 0x60, 'start': 24, 'size': 4, 'param_function': signed_tile_offset},
'nearby_tile_animation_frame' : {'var': 0x61, 'start': 0, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_industrytile_id' : {'var': 0x62, 'start': 0, 'size': 16, 'param_function': signed_tile_offset},
}
#
# Industries (feature 0x0A)
#
varact2vars_industries = {
'waiting_cargo_1' : {'var': 0x40, 'start': 0, 'size': 16},
'waiting_cargo_2' : {'var': 0x41, 'start': 0, 'size': 16},
'waiting_cargo_3' : {'var': 0x42, 'start': 0, 'size': 16},
'water_distance' : {'var': 0x43, 'start': 0, 'size': 32},
'layout_num' : {'var': 0x44, 'start': 0, 'size': 8},
# bits 0 .. 16 are either useless or already covered by var A7
'founder_type' : {'var': 0x45, 'start': 16, 'size': 2},
'founder_colour1' : {'var': 0x45, 'start': 24, 'size': 4},
'founder_colour2' : {'var': 0x45, 'start': 28, 'size': 4},
'build_date' : {'var': 0x46, 'start': 0, 'size': 32},
'random_bits' : {'var': 0x5F, 'start': 8, 'size': 16},
'produced_cargo_waiting_1' : {'var': 0x8A, 'start': 0, 'size': 16},
'produced_cargo_waiting_2' : {'var': 0x8C, 'start': 0, 'size': 16},
'production_rate_1' : {'var': 0x8E, 'start': 0, 'size': 8},
'production_rate_2' : {'var': 0x8F, 'start': 0, 'size': 8},
'production_level' : {'var': 0x93, 'start': 0, 'size': 8},
'produced_this_month_1' : {'var': 0x94, 'start': 0, 'size': 16},
'produced_this_month_2' : {'var': 0x96, 'start': 0, 'size': 16},
'transported_this_month_1' : {'var': 0x98, 'start': 0, 'size': 16},
'transported_this_month_2' : {'var': 0x9A, 'start': 0, 'size': 16},
'transported_last_month_pct_1' : {'var': 0x9C, 'start': 0, 'size': 8, 'value_function': value_mul_div(101, 256)},
'transported_last_month_pct_2' : {'var': 0x9D, 'start': 0, 'size': 8, 'value_function': value_mul_div(101, 256)},
'produced_last_month_1' : {'var': 0x9E, 'start': 0, 'size': 16},
'produced_last_month_2' : {'var': 0xA0, 'start': 0, 'size': 16},
'transported_last_month_1' : {'var': 0xA2, 'start': 0, 'size': 16},
'transported_last_month_2' : {'var': 0xA4, 'start': 0, 'size': 16},
'founder' : {'var': 0xA7, 'start': 0, 'size': 8},
'colour' : {'var': 0xA8, 'start': 0, 'size': 8},
'counter' : {'var': 0xAA, 'start': 0, 'size': 16},
'build_type' : {'var': 0xB3, 'start': 0, 'size': 2},
'last_accept_date' : {'var': 0xB4, 'start': 0, 'size': 16, 'value_function': value_add_constant(701265)},
}
def industry_count(name, args, pos, info):
if len(args) < 1 or len(args) > 2:
raise generic.ScriptError("'{}'() requires between 1 and 2 argument(s), encountered {:d}".format(name, len(args)), pos)
grfid = expression.ConstantNumeric(0xFFFFFFFF) if len(args) == 1 else args[1]
extra_params = [(0x100, grfid)]
return (args[0], extra_params)
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, expression.BinOp(nmlop.AND, args[1], expression.ConstantNumeric(0xFF)).reduce()) )
return (args[0], extra_params)
def industry_town_count(name, args, pos, info):
if len(args) < 1 or len(args) > 2:
raise generic.ScriptError("'{}'() requires between 1 and 2 argument(s), encountered {:d}".format(name, len(args)), pos)
grfid = expression.ConstantNumeric(0xFFFFFFFF) if len(args) == 1 else args[1]
extra_params = []
extra_params.append( (0x100, grfid) )
extra_params.append( (0x101, expression.ConstantNumeric(0x0100)) )
return (args[0], extra_params)
varact2vars60x_industries = {
'nearby_tile_industry_tile_id' : {'var': 0x60, 'start': 0, 'size': 16, 'param_function': unsigned_tile_offset},
'nearby_tile_random_bits' : {'var': 0x61, 'start': 0, 'size': 8, 'param_function': unsigned_tile_offset},
'nearby_tile_slope' : {'var': 0x62, 'start': 0, 'size': 5, 'param_function': unsigned_tile_offset},
'nearby_tile_is_water' : {'var': 0x62, 'start': 9, 'size': 1, 'param_function': unsigned_tile_offset},
'nearby_tile_terrain_type' : {'var': 0x62, 'start': 10, 'size': 3, 'param_function': unsigned_tile_offset},
'nearby_tile_water_class' : {'var': 0x62, 'start': 13, 'size': 2, 'param_function': unsigned_tile_offset},
'nearby_tile_height' : {'var': 0x62, 'start': 16, 'size': 8, 'param_function': unsigned_tile_offset},
'nearby_tile_class' : {'var': 0x62, 'start': 24, 'size': 4, 'param_function': unsigned_tile_offset},
'nearby_tile_animation_frame' : {'var': 0x63, 'start': 0, 'size': 8, 'param_function': unsigned_tile_offset},
'town_manhattan_dist' : {'var': 0x65, 'start': 0, 'size': 16, 'param_function': signed_tile_offset},
'town_zone' : {'var': 0x65, 'start': 16, 'size': 8, 'param_function': signed_tile_offset},
'town_euclidean_dist' : {'var': 0x66, 'start': 0, 'size': 32, 'param_function': signed_tile_offset},
'industry_count' : {'var': 0x67, 'start': 16, 'size': 8, 'param_function': industry_count},
'industry_distance' : {'var': 0x67, 'start': 0, 'size': 16, 'param_function': industry_count},
'industry_layout_count' : {'var': 0x68, 'start': 16, 'size': 8, 'param_function': industry_layout_count},
'industry_layout_distance' : {'var': 0x68, 'start': 0, 'size': 16, 'param_function': industry_layout_count},
'industry_town_count' : {'var': 0x68, 'start': 16, 'size': 8, 'param_function': industry_town_count},
}
#
# Cargos (feature 0x0B) have no own varaction2 variables
# Sounds (feature 0x0C) have no variational action2
#
#
# Airports (feature 0x0D)
#
varact2vars_airports = {
'layout' : {'var': 0x40, 'start': 0, 'size': 32},
}
varact2vars_airports.update(varact2vars_base_stations)
varact2vars60x_airports = varact2vars60x_base_stations
#
# New Signals (feature 0x0E) are not implemented in OpenTTD
#
#
# Objects (feature 0x0F)
#
varact2vars_objects = {
'relative_x' : {'var': 0x40, 'start': 0, 'size': 8},
'relative_y' : {'var': 0x40, 'start': 8, 'size': 8},
'relative_pos' : {'var': 0x40, 'start': 0, 'size': 16},
'terrain_type' : {'var': 0x41, 'start': 0, 'size': 3},
'tile_slope' : {'var': 0x41, 'start': 8, 'size': 5},
'build_date' : {'var': 0x42, 'start': 0, 'size': 32},
'animation_frame' : {'var': 0x43, 'start': 0, 'size': 8},
'company_colour' : {'var': 0x43, 'start': 0, 'size': 8},
'owner' : {'var': 0x44, 'start': 0, 'size': 8},
'town_manhattan_dist' : {'var': 0x45, 'start': 0, 'size': 16},
'town_zone' : {'var': 0x45, 'start': 16, 'size': 8},
'town_euclidean_dist' : {'var': 0x46, 'start': 0, 'size': 32},
'view' : {'var': 0x48, 'start': 0, 'size': 8},
'random_bits' : {'var': 0x5F, 'start': 8, 'size': 8},
}
varact2vars60x_objects = {
'nearby_tile_object_type' : {'var': 0x60, 'start': 0, 'size': 16, 'param_function': signed_tile_offset},
'nearby_tile_object_view' : {'var': 0x60, 'start': 16, 'size': 4, 'param_function': signed_tile_offset},
'nearby_tile_random_bits' : {'var': 0x61, 'start': 0, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_slope' : {'var': 0x62, 'start': 0, 'size': 5, 'param_function': signed_tile_offset},
'nearby_tile_is_same_object' : {'var': 0x62, 'start': 8, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_is_water' : {'var': 0x62, 'start': 9, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_terrain_type' : {'var': 0x62, 'start': 10, 'size': 3, 'param_function': signed_tile_offset},
'nearby_tile_water_class' : {'var': 0x62, 'start': 13, 'size': 2, 'param_function': signed_tile_offset},
'nearby_tile_height' : {'var': 0x62, 'start': 16, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_class' : {'var': 0x62, 'start': 24, 'size': 4, 'param_function': signed_tile_offset},
'nearby_tile_animation_frame' : {'var': 0x63, 'start': 0, 'size': 8, 'param_function': signed_tile_offset},
'object_count' : {'var': 0x64, 'start': 16, 'size': 8, 'param_function': industry_count},
'object_distance' : {'var': 0x64, 'start': 0, 'size': 16, 'param_function': industry_count},
}
#
# Railtypes (feature 0x10)
#
varact2vars_railtype = {
'terrain_type' : {'var': 0x40, 'start': 0, 'size': 8},
'enhanced_tunnels' : {'var': 0x41, 'start': 0, 'size': 8},
'level_crossing_status' : {'var': 0x42, 'start': 0, 'size': 8},
'build_date' : {'var': 0x43, 'start': 0, 'size': 32},
'town_zone' : {'var': 0x44, 'start': 0, 'size': 8},
'random_bits' : {'var': 0x5F, 'start': 8, 'size': 2},
}
# Railtypes have no 60+x variables
#
# Airport tiles (feature 0x11)
#
varact2vars_airporttiles = {
'terrain_type' : {'var': 0x41, 'start': 0, 'size': 8},
'town_radius_group' : {'var': 0x42, 'start': 0, 'size': 3},
'relative_x' : {'var': 0x43, 'start': 0, 'size': 8},
'relative_y' : {'var': 0x43, 'start': 8, 'size': 8},
'relative_pos' : {'var': 0x43, 'start': 0, 'size': 16},
'animation_frame' : {'var': 0x44, 'start': 0, 'size': 8},
}
varact2vars60x_airporttiles = {
'nearby_tile_slope' : {'var': 0x60, 'start': 0, 'size': 5, 'param_function': signed_tile_offset},
'nearby_tile_is_same_airport' : {'var': 0x60, 'start': 8, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_is_water' : {'var': 0x60, 'start': 9, 'size': 1, 'param_function': signed_tile_offset},
'nearby_tile_terrain_type' : {'var': 0x60, 'start': 10, 'size': 3, 'param_function': signed_tile_offset},
'nearby_tile_water_class' : {'var': 0x60, 'start': 13, 'size': 2, 'param_function': signed_tile_offset},
'nearby_tile_height' : {'var': 0x60, 'start': 16, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_class' : {'var': 0x60, 'start': 24, 'size': 4, 'param_function': signed_tile_offset},
'nearby_tile_animation_frame' : {'var': 0x61, 'start': 0, 'size': 8, 'param_function': signed_tile_offset},
'nearby_tile_airporttile_id' : {'var': 0x62, 'start': 0, 'size': 16, 'param_function': signed_tile_offset},
}
#
# Towns are not a true feature, but accessible via the parent scope of e.g. industries, stations
#
varact2vars_towns = {
'is_city' : {'var': 0x40, 'start': 0, 'size': 1},
'cities_enabled' : {'var': 0x40, 'start': 1, 'size': 1, 'value_function': lambda var, info: expression.Not(var, var.pos)},
'population' : {'var': 0x82, 'start': 0, 'size': 16},
'has_church' : {'var': 0x92, 'start': 1, 'size': 1},
'has_stadium' : {'var': 0x92, 'start': 2, 'size': 1},
'town_zone_0_radius_square' : {'var': 0x94, 'start': 0, 'size': 16},
'town_zone_1_radius_square' : {'var': 0x96, 'start': 0, 'size': 16},
'town_zone_2_radius_square' : {'var': 0x98, 'start': 0, 'size': 16},
'town_zone_3_radius_square' : {'var': 0x9A, 'start': 0, 'size': 16},
'town_zone_4_radius_square' : {'var': 0x9C, 'start': 0, 'size': 16},
'num_houses' : {'var': 0xB6, 'start': 0, 'size': 16},
'percent_transported_passengers' : {'var': 0xCA, 'start': 0, 'size': 8, 'value_function': value_mul_div(101, 256)},
'percent_transported_mail' : {'var': 0xCB, 'start': 0, 'size': 8, 'value_function': value_mul_div(101, 256)},
}
varact2vars[0x00] = varact2vars_trains
varact2vars60x[0x00] = varact2vars60x_vehicles
varact2vars[0x01] = varact2vars_roadvehs
varact2vars60x[0x01] = varact2vars60x_vehicles
varact2vars[0x02] = varact2vars_ships
varact2vars60x[0x02] = varact2vars60x_vehicles
varact2vars[0x03] = varact2vars_aircraft
varact2vars60x[0x03] = varact2vars60x_vehicles
varact2vars[0x04] = varact2vars_stations
varact2vars60x[0x04] = varact2vars60x_stations
varact2vars[0x05] = varact2vars_canals
varact2vars[0x07] = varact2vars_houses
varact2vars60x[0x07] = varact2vars60x_houses
varact2vars[0x09] = varact2vars_industrytiles
varact2vars60x[0x09] = varact2vars60x_industrytiles
varact2vars[0x0A] = varact2vars_industries
varact2vars60x[0x0A] = varact2vars60x_industries
varact2vars[0x0D] = varact2vars_airports
varact2vars60x[0x0D] = varact2vars60x_airports
varact2vars[0x0F] = varact2vars_objects
varact2vars60x[0x0F] = varact2vars60x_objects
varact2vars[0x10] = varact2vars_railtype
varact2vars[0x11] = varact2vars_airporttiles
varact2vars60x[0x11] = varact2vars60x_airporttiles
varact2vars[0x12] = varact2vars_towns
nml-0.4.5/nml/actions/action1.py 0000644 0005672 0005672 00000022061 13315644406 017615 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from nml.actions import base_action, real_sprite
"""
Maximum number of sprites per block.
This can be increased by switching to extended Action1.
"""
max_sprite_block_size = 0xFF
class Action1(base_action.BaseAction):
"""
Class representing an Action1
@ivar feature: Feature of this action1
@type feature: C{int}
@ivar num_sets: Number of (sprite) sets that follow this action 1.
@type num_sets: C{int}
@ivar num_ent: Number of sprites per set (e.g. (usually) 8 for vehicles)
@type num_ent: C{int}
"""
def __init__(self, feature, num_sets, num_ent):
self.feature = feature
self.num_sets = num_sets
self.num_ent = num_ent
def write(self, file):
# * 01
file.start_sprite(6)
file.print_bytex(1)
file.print_bytex(self.feature)
file.print_byte(self.num_sets)
file.print_varx(self.num_ent, 3)
file.newline()
file.end_sprite()
class SpritesetCollection(base_action.BaseAction):
"""
A collection that contains multiple spritesets. All spritesets will be
written to the same Action1, so they need to have the same number of sprites.
@ivar feature: The feature number the action1 will get.
@type feature: C{int}
@ivar num_sprites_per_spriteset: The number of sprites in each spriteset.
@type num_sprites_per_spriteset: C{int}
@ivar spritesets: A mapping from spritesets to indices. This allows for
quick lookup of whether a spriteset is already in this
collection. The indices are unique integers in the
range 0 .. len(spritesets) - 1.
@type spritesets: C{dict} mapping L{SpriteSet} to C{int}.
"""
def __init__(self, feature, num_sprites_per_spriteset):
self.feature = feature
self.num_sprites_per_spriteset = num_sprites_per_spriteset
self.spritesets = {}
def skip_action7(self):
return False
def skip_action9(self):
return False
def skip_needed(self):
return False
def can_add(self, spritesets, feature):
"""
Test whether the given list of spritesets can be added to this collection.
@param spritesets: The list of spritesets to test for addition.
@type spritesets: C{list} of L{SpriteSet}
@param feature: The feature of the given spritesets.
@type feature: C{int}
@return: True iff the given spritesets can be added to this collection.
@rtype: C{bool}
"""
assert len(spritesets) <= max_sprite_block_size
if feature != self.feature:
return False
for spriteset in spritesets:
if len(real_sprite.parse_sprite_data(spriteset)) != self.num_sprites_per_spriteset:
return False
num_new_sets = sum(1 for x in spritesets if x not in self.spritesets)
return len(self.spritesets) + num_new_sets <= max_sprite_block_size
def add(self, spritesets):
"""
Add a list of spritesets to this collection.
@param spritesets: The list of spritesets to add.
@type spritesets: C{list} of L{SpriteSet}
@pre: can_add(spritesets, self.feature).
"""
assert self.can_add(spritesets, self.feature)
for spriteset in spritesets:
if spriteset not in self.spritesets:
self.spritesets[spriteset] = len(self.spritesets)
def get_index(self, spriteset):
"""
Get the index of the given spriteset in the final action1.
@param spriteset: The spriteset to get the index of.
@type spriteset: L{SpriteSet}.
@pre: The spriteset must have been previously added to this
collection via #add.
"""
assert spriteset in self.spritesets
return self.spritesets[spriteset]
def get_action_list(self):
"""
Create a list of actions needed to write this collection to the output. This
will generate a single Action1 and as many realsprites as needed.
@return: A list of actions needed to represet this collection in a GRF.
@rtype: C{list} of L{BaseAction}
"""
actions = [Action1(self.feature, len(self.spritesets), self.num_sprites_per_spriteset)]
for idx in range(len(self.spritesets)):
for spriteset, spriteset_offset in list(self.spritesets.items()):
if idx == spriteset_offset:
actions.extend(real_sprite.parse_sprite_data(spriteset))
break
return actions
"""
Statistics about spritesets.
The 1st field of type C{int} contains the largest block of consecutive spritesets.
The 2nd field of type L{Position} contains a positional reference to the largest block of consecutive spritesets.
"""
spriteset_stats = (0, None)
def print_stats():
"""
Print statistics about used ids.
"""
if spriteset_stats[0] > 0:
# NML uses as many concurrent spritesets as possible to prevent sprite duplication.
# So, instead of the actual amount, we rather print the biggest unsplittable block, since that is what matters.
generic.print_info("Concurrent spritesets: {}/{} ({})".format(spriteset_stats[0], max_sprite_block_size, str(spriteset_stats[1])))
"""
The collection which was previoulsy used. add_to_action1 will try to reuse this
collection as long as possible to reduce the duplication of sprites. As soon
as a spriteset with a different feature or amount of sprites is added a new
collection will be created.
"""
last_spriteset_collection = None
def add_to_action1(spritesets, feature, pos):
"""
Add a list of spritesets to a spriteset collection. This will try to reuse
one collection as long as possible and create a new one when needed.
@param spritesets: List of spritesets that will be used by the next action2.
@type spritesets: C{list} of L{SpriteSet}
@param feature: Feature of the spritesets.
@type feature: C{int}
@param pos: Position reference to source.
@type pos: L{Position}
@return: List of collections that needs to be added to the global action list.
@rtype: C{list} of L{SpritesetCollection}.
"""
if not spritesets:
return []
setsize = len(real_sprite.parse_sprite_data(spritesets[0]))
for spriteset in spritesets:
if setsize != len(real_sprite.parse_sprite_data(spriteset)):
raise generic.ScriptError("Using spritesets with different sizes in a single sprite group / layout is not possible", pos)
global spriteset_stats
if spriteset_stats[0] < len(spritesets):
spriteset_stats = (len(spritesets), pos)
global last_spriteset_collection
actions = []
if last_spriteset_collection is None or not last_spriteset_collection.can_add(spritesets, feature):
last_spriteset_collection = SpritesetCollection(feature, len(real_sprite.parse_sprite_data(spritesets[0])))
actions.append(last_spriteset_collection)
last_spriteset_collection.add(spritesets)
return actions
def get_action1_index(spriteset):
"""
Get the index of a spriteset in the action1. The given spriteset must have
been added in the last call to #add_to_action1. Any new calls to
#add_to_action1 may or may not allocate a new spriteset collection and as
such make previous spritesets inaccessible.
@param spriteset: The spriteset to get the index of.
@type spriteset: L{SpriteSet}.
@return: The index in the action1 of the given spriteset.
@rtype: C{int}
"""
assert last_spriteset_collection is not None
return last_spriteset_collection.get_index(spriteset)
def make_cb_failure_action1(feature):
"""
Create an action1 that may be used for a callback failure
If the last action1 is of the correct feature, no new action1 is needed
Else, add a new action1 with 1 spriteset containing 0 sprites
@param feature: Feature of the requested action 1
@type feature: C{int}
@return: List of actions to append (if any) and action1 index to use
@rtype: C{tuple} of (C{list} of L{BaseAction}, C{int})
"""
global last_spriteset_collection
if last_spriteset_collection is not None and last_spriteset_collection.feature == feature:
actions = []
else:
last_spriteset_collection = None
actions = [Action1(feature, 1, 0)]
return (actions, 0) # Index is currently always 0, but will change with ext. A1
nml-0.4.5/nml/actions/action0.py 0000644 0005672 0005672 00000115046 13315644406 017622 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml.actions.action0properties import BaseAction0Property, Action0Property, properties, two_byte_property
from nml import generic, expression, nmlop, grfstrings
from nml.actions import base_action, action4, action6, actionD, action7
from nml.ast import general
class BlockAllocation(object):
"""
Administration of allocated blocks of a size, in a range of addresses.
Blocks always start at address C{0}, but the first available freely usable
address may be further.
The allocation information is kept in L{allocated}. It has the following cases
- An address does not exist in the dictionary.
The address is free for use.
- An address exists, with an integer number as size.
At this address a block has been allocated with that size.
- An address exists, with value C{None}.
The address is part of an allocated block, but not the start.
@ivar first: First freely usable address.
@type first: C{int}
@ivar last: Last freely usable address.
@type last: C{int}
@ivar name: Name for debug output.
@type name: C{str}
@ivar dynamic_allocation: True, if ids are allocated. False, if they refer to static entities.
@type dynamic_allocation: C{bool}
@ivar allocated: Mapping of allocated blocks.
@type allocated: C{dict} of C{int} to allocation information.
@ivar filled: Mapping of block size to smallest address that may contain free space.
Serves as a cache to speed up searches.
@type filled: C{dict} of C{int}
"""
def __init__(self, first, last, name, dynamic_allocation=True):
self.first = first
self.last = last
self.name = name
self.dynamic_allocation = dynamic_allocation
self.allocated = {}
self.filled = {}
def get_num_allocated(self):
"""
Return number of allocated ids.
"""
if self.dynamic_allocation:
return len(self.allocated)
else:
return 0
def get_max_allocated(self):
"""
Return maximum number of allocateable ids.
"""
if self.dynamic_allocation:
return self.last - self.first + 1
else:
return 0
def in_range(self, addr, length):
"""
Does a block starting at the provided address with the given length in
the available address space?
@param addr: First address of the block.
@type addr: C{int}
@param length: Number of addresses in the block.
@type length: C{int}
@return: Whether the block fits enitrely in the available address space.
@rtype: C{bool}
"""
return addr >= 0 and addr + length - 1 <= self.last
def get_size(self, addr):
"""
Get the size of the block allocated at the given address.
@param addr: Address being queried.
@type addr: C{int}
@return: Size of the allocated block, if one was allocated here, else C{None}.
@rtype: C{int} or C{None}
"""
return self.allocated.get(addr)
def is_address_free(self, addr):
"""
Is the space at the given address still available?
( cheaper variant of C{self.get_last_used(addr, 1) is None} )
@param addr: Address being queried.
@type addr: C{int}
@return: Whether the space at the provided address is available.
@rtype: C{bool}
"""
return addr not in self.allocated
def get_last_used(self, addr, length):
"""
Check whether a block of addresses is used.
@param addr: First address of the block.
@type addr: C{int}
@param length: Number of addresses in the block.
@type length: C{int}
@return: The last used address in the block, or C{None} if all addresses are free.
@rtype: C{int} or C{None}
@precond: Addresses of the range should be within the available address space.
"""
for idx in range(addr + length - 1, addr - 1, -1):
if idx in self.allocated: return idx
return None
def mark_used(self, addr, length):
"""
Mark an area as being allocated.
@param addr: First address of the block.
@type addr: C{int}
@param length: Number of addresses in the block.
@type length: C{int}
@precond: Addresses of the block should be within the freely available address space.
@precond: No address in the block may have been allocated.
"""
self.allocated[addr] = length
for idx in range(addr + 1, addr + length):
self.allocated[idx] = None
def find_unused(self, length):
"""
Find an area of unused space.
@param length: Number of addresses to find.
@type length: C{int}
@return: Address in the freely available address space for the new block,
or C{None} if no space is available.
@rtype: C{int} or C{None}
"""
idx = self.filled.get(length)
if idx is None:
# Never searched before with this block size.
# Start at the biggest offset previously discovered with a smaller block size.
smaller_filleds = [min_f for sz, min_f in self.filled.items() if sz < length]
idx = self.first if len(smaller_filleds) == 0 else max(smaller_filleds)
last_idx = self.last - length + 1
while idx < last_idx:
last_used = self.get_last_used(idx, length)
if last_used is None:
self.filled[length] = idx + length
return idx
idx = last_used + 1
return None
# Available IDs for each feature.
# Maximum allowed id (houses and indtiles in principle allow up to 511, but action3 does not accept extended bytes).
used_ids = [
BlockAllocation(116, 0xFFFF, "Train"),
BlockAllocation( 88, 0xFFFF, "Road Vehicle"),
BlockAllocation( 11, 0xFFFF, "Ship"),
BlockAllocation( 41, 0xFFFF, "Aircraft"),
BlockAllocation( 0, 255, "Station"),
BlockAllocation( 0, 8, "Canal", False),
BlockAllocation( 0, 15, "Bridge", False),
BlockAllocation( 0, 255, "House"),
BlockAllocation( 0, -1, "Global", False),
BlockAllocation( 0, 255, "Industry Tile"),
BlockAllocation( 0, 127, "Industry"),
BlockAllocation( 0, 63, "Cargo"),
BlockAllocation( 0, -1, "Sound"),
BlockAllocation( 0, 127, "Airport"),
BlockAllocation( 0, -1, "Signal", False),
BlockAllocation( 0, 255, "Object"),
BlockAllocation( 0, 15, "Railtype"),
BlockAllocation( 0, 255, "Airport Tile"),
]
def print_stats():
"""
Print statistics about used ids.
"""
for feature in used_ids:
used = feature.get_num_allocated()
if used > 0 and feature.dynamic_allocation:
generic.print_info("{} items: {}/{}".format(feature.name, used, feature.get_max_allocated()))
def mark_id_used(feature, id, num_ids):
"""
Mark a range of ids as used.
@param feature: Feature of the ids.
@type feature: C{int}
@param id: First id to be marked.
@type id: C{int}
@param num_ids: Number of ids to mark as used, starting with \a id.
@type num_ids: C{int}
"""
used_ids[feature].mark_used(id, num_ids)
def check_id_range(feature, id, num_ids, pos):
"""
Check that the id is valid, and is either free or points to an already allocated block.
@param feature: Feature of the ids.
@type feature: C{int}
@param id: Base id number.
@type id: C{int}
@param num_ids: Number of ids to test, starting with \a id.
@type num_ids: C{int}
@param pos: Source position of the check, for reporting errors.
@type pos: L{Position}
@return: Whether the block was allocated already.
@rtype: C{bool}
"""
blk_alloc = used_ids[feature]
# Check that IDs are valid and in range.
if not blk_alloc.in_range(id, num_ids):
msg = "Item ID must be in range 0..{:d}, encountered {:d}..{:d}."
msg = msg.format(blk_alloc.last, id, id + num_ids - 1)
raise generic.ScriptError(msg, pos)
# ID already defined, but with the same size: OK
if blk_alloc.get_size(id) == num_ids: return True
# All IDs free: no problem.
if blk_alloc.get_last_used(id, num_ids) is None: return False
# No space at the indicated position, report an error.
if blk_alloc.get_size(id) is not None:
# ID already defined with a different size: error.
raise generic.ScriptError("Item with ID {:d} has already been defined, but with a different size.".format(id), pos)
if blk_alloc.is_address_free(id):
# First item id free -> any of the additional tile ids must be blocked.
msg = "This multi-tile house requires that item IDs {:d}..{:d} are free, but they are not."
msg = msg.format(id, id + num_ids - 1)
raise generic.ScriptError(msg, pos)
# ID already defined as part of a multi-tile house.
raise generic.ScriptError("Item ID {:d} has already used as part of a multi-tile house.".format(id), pos)
def get_free_id(feature, num_ids, pos):
"""
Find an id to allocate a range of \a num_ids ids in a feature.
@param feature: Feature of the ids.
@type feature: C{int}
@param num_ids: Number of ids to allocate.
@type num_ids: C{int}
@param pos: Position information.
@type pos: L{Position}
"""
blk_alloc = used_ids[feature]
addr = blk_alloc.find_unused(num_ids)
if addr is None:
msg = "Unable to allocate ID for item, no more free IDs available (maximum is {:d})"
msg = msg.format(blk_alloc.last)
raise generic.ScriptError(msg, pos)
blk_alloc.mark_used(addr, num_ids)
return addr
# Number of tiles for various house sizes
house_sizes = {
0 : 1, # 1x1
2 : 2, # 2x1
3 : 2, # 1x2
4 : 4, # 2x2
}
def adjust_value(value, org_value, unit, ottd_convert_func):
"""
Make sure that the property value written to the NewGRF will match exactly
the value as quoted
@param value: The value to check, converted to base units
@type value: L{Expression}
@param org_value: The original value as written in the input file
@type org_value: L{Expression}
@param unit: The unit of the org_value
@type unit: L{Unit} or C{None}
@return: The adjusted value
@rtype: L{Expression}
"""
while ottd_convert_func(value, unit) > org_value.value:
value = expression.ConstantNumeric(int(value.value - 1), value.pos)
lower_value = value
while ottd_convert_func(value, unit) < org_value.value:
value = expression.ConstantNumeric(int(value.value + 1), value.pos)
higher_value = value
if abs(ottd_convert_func(lower_value, unit) - org_value.value) < abs(ottd_convert_func(higher_value, unit) - org_value.value):
return lower_value
return higher_value
class Action0(base_action.BaseAction):
"""
Representation of NFO action 0. Action 0 is used to set properties on all
kinds of objects. It can set any number of properties on a list of
consecutive IDs. Each property is defined by a unique (per feature) integer.
@ivar feature: Feature number to set properties for.
@type feature: C{int}
@ivar id: First ID to set properties for
@type id: C{int}
@ivar prop_list: List of all properties that are to be set.
@type prop_list: C{list} of L{BaseAction0Property}
@ivar num_ids: Number of IDs to set properties for.
@type num_ids: C{int} or C{None}
"""
def __init__(self, feature, id):
self.feature = feature
self.id = id
self.prop_list = []
self.num_ids = None
def prepare_output(self, sprite_num):
if self.num_ids is None: self.num_ids = 1
def write(self, file):
size = 7
for prop in self.prop_list:
assert isinstance(prop, BaseAction0Property), type(prop)
if isinstance(prop, Action0Property):
assert len(prop.values) == self.num_ids
size += prop.get_size()
file.start_sprite(size)
file.print_bytex(0)
file.print_bytex(self.feature)
file.print_byte(len(self.prop_list))
file.print_bytex(self.num_ids)
file.print_bytex(0xFF)
file.print_wordx(self.id)
file.newline()
for prop in self.prop_list:
prop.write(file)
file.end_sprite()
def create_action0(feature, id, act6, action_list):
"""
Create an action0 with variable id
@param feature: Feature of the action0
@type feature: C{int}
@param id: ID of the corresponding item
@type id: L{Expression}
@param act6: Action6 to add any modifications to
@type act6: L{Action6}
@param action_list: Action list to append any extra actions to
@type action_list: C{list} of L{BaseAction}
@return: A tuple of (resulting action0, offset to use for action6)
@rtype: C{tuple} of (L{Action0}, C{int})
"""
id, offset = actionD.write_action_value(id, action_list, act6, 5, 2)
action0 = Action0(feature, id.value)
return (action0, offset)
def get_property_info_list(feature, name):
"""
Find information on a single property, based on feature and name/number
@param feature: Feature of the associated item
@type feature: C{int}
@param name: Name (or number) of the property
@type name: L{Identifier} or L{ConstantNumeric}
@return: A list of dictionaries with property information
@rtype: C{list} of C{dict}
"""
global properties
#Validate feature
assert feature in range (0, len(properties)) #guaranteed by item
if properties[feature] is None:
raise generic.ScriptError("Setting properties for feature '{}' is not possible, no properties are defined.".format(general.feature_name(feature)), name.pos)
if isinstance(name, expression.Identifier):
if not name.value in properties[feature]: raise generic.ScriptError("Unknown property name: " + name.value, name.pos)
prop_info_list = properties[feature][name.value]
if not isinstance(prop_info_list, list): prop_info_list = [prop_info_list]
elif isinstance(name, expression.ConstantNumeric):
for p in properties[feature]:
prop_info_list = properties[feature][p]
if not isinstance(prop_info_list, list): prop_info_list = [prop_info_list]
# Only non-compound properties may be set by number
if len(prop_info_list) == 1 and 'num' in prop_info_list[0] and prop_info_list[0]['num'] == name.value:
break
else:
raise generic.ScriptError("Unknown property number: " + str(name), name.pos)
else: assert False
for prop_info in prop_info_list:
if 'warning' in prop_info:
generic.print_warning(prop_info['warning'], name.pos)
return prop_info_list
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 not 'unit_type' 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 = expression.BinOp(nmlop.MUL, value, expression.ConstantNumeric(mul, value.pos), value.pos)
value = expression.BinOp(nmlop.ADD, value, expression.ConstantNumeric(int(div / 2), value.pos), value.pos)
value = expression.BinOp(nmlop.DIV, value, expression.ConstantNumeric(div, value.pos), value.pos)
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 parse_property(prop_info, value_list, feature, id):
"""
Parse a single property
@param prop_info: A dictionary with property information
@type prop_info: C{dict}
@param value_list: List of values for the property, with unit conversion applied
@type value_list: C{list} of L{Expression}
@param feature: Feature of the associated item
@type feature: C{int}
@param id: ID of the associated item
@type id: L{Expression}
@return: A tuple containing the following:
- List of properties to add to the action 0
- List of actions to prepend
- List of modifications to apply via action 6
- List of actions to append
@rtype: C{tuple} of (C{list} of L{Action0Property}, C{list} of L{BaseAction}, C{list} of 3-C{tuple}, C{list} of L{BaseAction})
"""
action_list = []
action_list_append = []
mods = []
if 'custom_function' in prop_info:
props = prop_info['custom_function'](*value_list)
else:
# First process each element in the value_list
final_values = []
for i, value in enumerate(value_list):
if 'string_literal' in prop_info and (isinstance(value, expression.StringLiteral) or prop_info['string_literal'] != 4):
# Parse non-string exprssions just like integers. User will have to take care of proper value.
# This can be used to set a label (=string of length 4) to the value of a parameter.
if not isinstance(value, expression.StringLiteral): raise generic.ScriptError("Value for property {:d} must be a string literal".format(prop_info['num']), value.pos)
if len(value.value) != prop_info['string_literal']:
raise generic.ScriptError("Value for property {:d} must be of length {:d}".format(prop_info['num'], prop_info['string_literal']), value.pos)
elif isinstance(value, expression.ConstantNumeric):
pass
elif isinstance(value, expression.Parameter) and isinstance(value.num, expression.ConstantNumeric):
mods.append( (value.num.value, prop_info['size'], i * prop_info['size'] + 1) )
value = expression.ConstantNumeric(0)
elif isinstance(value, expression.String):
if not 'string' in prop_info: raise generic.ScriptError("String used as value for non-string property: " + str(prop_info['num']), value.pos)
string_range = prop_info['string']
stringid, string_actions = action4.get_string_action4s(feature, string_range, value, id)
value = expression.ConstantNumeric(stringid)
action_list_append.extend(string_actions)
else:
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(value)
mods.append((tmp_param, prop_info['size'], i * prop_info['size'] + 1))
action_list.extend(tmp_param_actions)
value = expression.ConstantNumeric(0)
final_values.append(value)
# Now, write a single Action0 Property with all of these values
if prop_info['num'] != -1:
props = [Action0Property(prop_info['num'], final_values, prop_info['size'])]
else:
props = []
return (props, action_list, mods, action_list_append)
def validate_prop_info_list(prop_info_list, pos_list, feature):
"""
Perform various checks on a list of properties in a property-block
- make sure that properties that should appear first (substitute type) appear first
@param prop_info_list: List of dictionaries with property information
@type prop_info_list: C{list} of C{dict}
@param pos_list: List containing position information (order matches prop_info_list)
@type pos_list: C{list} of L{Position}
@param feature: Feature of the associated item
@type feature: C{int}
"""
global properties
first_warnings = [(info, pos_list[i]) for i, info in enumerate(prop_info_list) if 'first' in info and i != 0]
for info, pos in first_warnings:
for prop_name, prop_info in list(properties[feature].items()):
if info == prop_info or (isinstance(prop_info, list) and info in prop_info):
generic.print_warning("Property '{}' should be set before all other properties and graphics.".format(prop_name), pos)
break
def parse_property_block(prop_list, feature, id, size):
"""
Parse a property block to an action0 (with possibly various other actions)
@param prop_list: List of properties to parse
@type prop_list: C{list} of L{Property}
@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 (for houses only)
@type size: L{ConstantNumeric} or C{None}
@return: List of resulting actions
@rtype: C{list} of L{BaseAction}
"""
action6.free_parameters.save()
action_list = []
action_list_append = []
act6 = action6.Action6()
action0, offset = create_action0(feature, id, act6, action_list)
if feature == 0x07:
size_bit = size.value if size is not None else 0
action0.num_ids = house_sizes[size_bit]
else:
size_bit = None
action0.num_ids = 1
prop_info_list = []
value_list_list = []
pos_list = []
for prop in prop_list:
new_prop_info_list = get_property_info_list(feature, prop.name)
prop_info_list.extend(new_prop_info_list)
value_list_list.extend(parse_property_value(prop_info, prop.value, prop.unit, size_bit) for prop_info in new_prop_info_list)
pos_list.extend(prop.name.pos for i in prop_info_list)
validate_prop_info_list(prop_info_list, pos_list, feature)
for prop_info, value_list in zip(prop_info_list, value_list_list):
if 'test_function' in prop_info and not prop_info['test_function'](*value_list): continue
properties, extra_actions, mods, extra_append_actions = parse_property(prop_info, value_list, feature, id)
action_list.extend(extra_actions)
action_list_append.extend(extra_append_actions)
for mod in mods:
act6.modify_bytes(mod[0], mod[1], mod[2] + offset)
for p in properties:
offset += p.get_size()
action0.prop_list.extend(properties)
if len(act6.modifications) > 0: action_list.append(act6)
if len(action0.prop_list) != 0:
action_list.append(action0)
action_list.extend(action_list_append)
action6.free_parameters.restore()
return action_list
class IDListProp(BaseAction0Property):
def __init__(self, prop_num, id_list):
self.prop_num = prop_num
self.id_list = id_list
def write(self, file):
file.print_bytex(self.prop_num)
for i, id_val in enumerate(self.id_list):
if i > 0 and i % 5 == 0: file.newline()
file.print_string(id_val.value, False, True)
file.newline()
def get_size(self):
return len(self.id_list) * 4 + 1
def get_cargolist_action(cargo_list):
action0 = Action0(0x08, 0)
action0.prop_list.append(IDListProp(0x09, cargo_list))
action0.num_ids = len(cargo_list)
return [action0]
def get_railtypelist_action(railtype_list):
action6.free_parameters.save()
act6 = action6.Action6()
action_list = []
action0, offset = create_action0(0x08, expression.ConstantNumeric(0), act6, action_list)
id_table = []
offset += 1 # Skip property number
for railtype in railtype_list:
if isinstance(railtype, expression.StringLiteral):
id_table.append(railtype)
offset+=4
continue
param, extra_actions = actionD.get_tmp_parameter(expression.ConstantNumeric(expression.parse_string_to_dword(railtype[-1])))
action_list.extend(extra_actions)
for idx in range(len(railtype)-2, -1, -1):
val = expression.ConstantNumeric(expression.parse_string_to_dword(railtype[idx]))
action_list.append(action7.SkipAction(0x09, 0x00, 4, (0x0D, None), val.value, 1))
action_list.append(actionD.ActionD(expression.ConstantNumeric(param), expression.ConstantNumeric(0xFF), nmlop.ASSIGN, expression.ConstantNumeric(0xFF), val))
act6.modify_bytes(param, 4, offset)
id_table.append(expression.StringLiteral(r"\00\00\00\00", None))
offset += 4
action0.prop_list.append(IDListProp(0x12, id_table))
action0.num_ids = len(railtype_list)
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(action0)
action6.free_parameters.restore()
return action_list
class ByteListProp(BaseAction0Property):
def __init__(self, prop_num, data):
self.prop_num = prop_num
self.data = data
def write(self, file):
file.print_bytex(self.prop_num)
file.newline()
for i, data_val in enumerate(self.data):
if i > 0 and i % 8 == 0: file.newline()
file.print_bytex(ord(data_val))
file.newline()
def get_size(self):
return len(self.data) + 1
def get_snowlinetable_action(snowline_table):
assert len(snowline_table) == 12*32
action6.free_parameters.save()
action_list = []
tmp_param_map = {} #Cache for tmp parameters
act6 = action6.Action6()
act0, offset = create_action0(0x08, expression.ConstantNumeric(0), act6, action_list)
act0.num_ids = 1
offset += 1 # Skip property number
data_table = []
idx = 0
while idx < len(snowline_table):
val = snowline_table[idx]
if isinstance(val, expression.ConstantNumeric):
data_table.append(val.value)
idx += 1
continue
if idx + 3 >= len(snowline_table):
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(val)
tmp_param_map[val] = tmp_param
act6.modify_bytes(tmp_param, 1, offset + idx)
action_list.extend(tmp_param_actions)
data_table.append(0)
idx += 1
continue
# Merge the next 4 values together in a single parameter.
val2 = expression.BinOp(nmlop.SHIFT_LEFT, snowline_table[idx + 1], expression.ConstantNumeric(8))
val3 = expression.BinOp(nmlop.SHIFT_LEFT, snowline_table[idx + 2], expression.ConstantNumeric(16))
val4 = expression.BinOp(nmlop.SHIFT_LEFT, snowline_table[idx + 3], expression.ConstantNumeric(24))
expr = expression.BinOp(nmlop.OR, val, val2)
expr = expression.BinOp(nmlop.OR, expr, val3)
expr = expression.BinOp(nmlop.OR, expr, val4)
expr = expr.reduce()
#Cache lookup, saves some ActionDs
if expr in tmp_param_map:
tmp_param, tmp_param_actions = tmp_param_map[expr], []
else:
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(expr)
tmp_param_map[expr] = tmp_param
act6.modify_bytes(tmp_param, 4, offset + idx)
action_list.extend(tmp_param_actions)
data_table.extend([0, 0, 0, 0])
idx += 4
act0.prop_list.append(ByteListProp(0x10, ''.join([chr(x) for x in data_table])))
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(act0)
action6.free_parameters.restore()
return action_list
def get_basecost_action(basecost):
action6.free_parameters.save()
action_list = []
tmp_param_map = {} #Cache for tmp parameters
#We want to avoid writing lots of action0s if possible
i = 0
while i < len(basecost.costs):
cost = basecost.costs[i]
act6 = action6.Action6()
act0, offset = create_action0(0x08, cost.name, act6, action_list)
first_id = cost.name.value if isinstance(cost.name, expression.ConstantNumeric) else None
num_ids = 1 #Number of values that will be written in one go
values = []
#try to capture as much values as possible
while True:
cost = basecost.costs[i]
if isinstance(cost.value, expression.ConstantNumeric):
values.append(cost.value)
else:
#Cache lookup, saves some ActionDs
if cost.value in tmp_param_map:
tmp_param, tmp_param_actions = tmp_param_map[cost.value], []
else:
tmp_param, tmp_param_actions = actionD.get_tmp_parameter(cost.value)
tmp_param_map[cost.value] = tmp_param
act6.modify_bytes(tmp_param, 1, offset + num_ids)
action_list.extend(tmp_param_actions)
values.append(expression.ConstantNumeric(0))
#check if we can append the next to this one (it has to be consecutively numbered)
if first_id is not None and (i + 1) < len(basecost.costs):
nextcost = basecost.costs[i+1]
if isinstance(nextcost.name, expression.ConstantNumeric) and nextcost.name.value == first_id + num_ids:
num_ids += 1
i += 1
#Yes We Can, continue the loop to append this value to the list and try further
continue
# No match, so stop it and write an action0
break
act0.prop_list.append(Action0Property(0x08, values, 1))
act0.num_ids = num_ids
if len(act6.modifications) > 0: action_list.append(act6)
action_list.append(act0)
i += 1
action6.free_parameters.restore()
return action_list
class LanguageTranslationTable(BaseAction0Property):
def __init__(self, num, name_list, extra_names):
self.num = num
self.mappings = []
for name, idx in list(name_list.items()):
self.mappings.append( (idx, name) )
if name in extra_names:
for extra_name in extra_names[name]:
self.mappings.append( (idx, extra_name) )
# Sort to keep the output deterministic
self.mappings.sort()
def write(self, file):
file.print_bytex(self.num)
for mapping in self.mappings:
file.print_bytex(mapping[0])
file.print_string(mapping[1])
file.print_bytex(0)
file.newline()
def get_size(self):
size = 2
for mapping in self.mappings:
size += 1 + grfstrings.get_string_size(mapping[1])
return size
def get_language_translation_tables(lang):
action0 = Action0(0x08, lang.langid)
if lang.genders is not None:
action0.prop_list.append(LanguageTranslationTable(0x13, lang.genders, lang.gender_map))
if lang.cases is not None:
action0.prop_list.append(LanguageTranslationTable(0x14, lang.cases, lang.case_map))
if lang.plural is not None:
action0.prop_list.append(Action0Property(0x15, expression.ConstantNumeric(lang.plural), 1))
if len(action0.prop_list) > 0:
return [action0]
return []
disable_info = {
# Vehicles: set climates_available to 0
0x00 : {'num': 116, 'props': [{'num': 0x06, 'size': 1, 'value': 0}]},
0x01 : {'num': 88, 'props': [{'num': 0x06, 'size': 1, 'value': 0}]},
0x02 : {'num': 11, 'props': [{'num': 0x06, 'size': 1, 'value': 0}]},
0x03 : {'num': 41, 'props': [{'num': 0x06, 'size': 1, 'value': 0}]},
# Houses / industries / airports: Set substitute_type to FF
0x07 : {'num': 110, 'props': [{'num': 0x08, 'size': 1, 'value': 0xFF}]},
0x0A : {'num': 37, 'props': [{'num': 0x08, 'size': 1, 'value': 0xFF}]},
0x0D : {'num': 10, 'props': [{'num': 0x08, 'size': 1, 'value': 0xFF}]},
# Cargos: Set bitnum to FF and label to 0
0x0B : {'num': 27, 'props': [{'num': 0x08, 'size': 1, 'value': 0xFF}, {'num': 0x17, 'size': 4, 'value': 0}]},
}
def get_disable_actions(disable):
"""
Get the action list for a disable_item block
@param disable: Disable block
@type disable: L{DisableItem}
@return: A list of resulting actions
@rtype: C{list} of L{BaseAction}
"""
feature = disable.feature.value
if feature not in disable_info:
raise generic.ScriptError("disable_item() is not available for feature {:d}.".format(feature), disable.pos)
if disable.first_id is None:
# No ids set -> disable all
assert disable.last_id is None
first = 0
num = disable_info[feature]['num']
else:
first = disable.first_id.value
if disable.last_id is None:
num = 1
else:
num = disable.last_id.value - first + 1
act0 = Action0(feature, first)
act0.num_ids = num
for prop in disable_info[feature]['props']:
act0.prop_list.append(Action0Property(prop['num'], num * [expression.ConstantNumeric(prop['value'])], prop['size']))
return [act0]
class EngineOverrideProp(BaseAction0Property):
def __init__(self, source, target):
self.source = source
self.target = target
def write(self, file):
file.print_bytex(0x11)
file.print_dwordx(self.source)
file.print_dwordx(self.target)
file.newline()
def get_size(self):
return 9
def get_engine_override_action(override):
act0 = Action0(0x08, 0)
act0.num_ids = 1
act0.prop_list.append(EngineOverrideProp(override.source_grfid, override.grfid))
return [act0]
def parse_sort_block(feature, vehid_list):
prop_num = [0x1A, 0x20, 0x1B, 0x1B]
action_list = []
if len(vehid_list) >= 2:
last = vehid_list[0]
idx = len(vehid_list) - 1
while idx >= 0:
cur = vehid_list[idx]
prop = Action0Property(prop_num[feature], [last], 3)
action_list.append(Action0(feature, cur.value))
action_list[-1].prop_list.append(prop)
last = cur
idx -= 1
return action_list
callback_flag_properties = {
0x00: {'size': 1, 'num': 0x1E},
0x01: {'size': 1, 'num': 0x17},
0x02: {'size': 1, 'num': 0x12},
0x03: {'size': 1, 'num': 0x14},
0x05: {'size': 1, 'num': 0x08},
0x07: two_byte_property(0x14, 0x1D),
0x09: {'size': 1, 'num': 0x0E},
0x0A: two_byte_property(0x21, 0x22),
0x0B: {'size': 1, 'num': 0x1A},
0x0F: {'size': 2, 'num': 0x15},
0x11: {'size': 1, 'num': 0x0E},
}
def get_callback_flags_actions(feature, id, flags):
"""
Get a list of actions to set the callback flags of a certain item
@param feature: Feature of the item
@type feature: C{int}
@param id: ID of the item
@type id: L{Expression}
@param flags: Value of the 'callback_flags' property
@type flags: C{int}
@return: A list of actions
@rtype: C{list} of L{BaseAction}
"""
assert isinstance(id, expression.ConstantNumeric)
act0, offset = create_action0(feature, id, None, None)
act0.num_ids = 1
assert feature in callback_flag_properties
prop_info_list = callback_flag_properties[feature]
if not isinstance(prop_info_list, list): prop_info_list = [prop_info_list]
for prop_info in prop_info_list:
act0.prop_list.append(Action0Property(prop_info['num'], parse_property_value(prop_info, expression.ConstantNumeric(flags)), prop_info['size']))
return [act0]
def get_volume_actions(volume_list):
"""
Get a list of actions to set sound volumes
@param volume_list: List of (sound id, volume) tuples, sorted in ascending order
@type volume_list: C{list} of (C{int}, C{int})-tuples
@return: A list of actions
@rtype: C{list} of L{BaseAction}
"""
action_list = []
first_id = None # First ID in a series of consecutive IDs
value_series = [] # Series of values to write in a single action
for id, volume in volume_list:
if first_id is None:
first_id = id
continue_series = first_id + len(value_series) == id
if continue_series:
value_series.append(volume)
if not continue_series or id == volume_list[-1][0]:
# Write action for this series
act0, offset = create_action0(0x0C, expression.ConstantNumeric(first_id), None, None)
act0.num_ids = len(value_series)
act0.prop_list.append(Action0Property(0x08, [expression.ConstantNumeric(v) for v in value_series], 1))
action_list.append(act0)
# start new series, if needed
if not continue_series:
first_id = id
value_series = [volume]
return action_list
nml-0.4.5/nml/__init__.py 0000644 0005672 0005672 00000001243 13315644406 016355 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
nml-0.4.5/nml/version_info.py 0000644 0005672 0005672 00000010534 13315644406 017321 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
# The following version determination code is a greatly simplified version
# of the mercurial repo code. The version is stored in nml/__version__.py
# get_numeric_version is used only for the purpose of packet creation,
# in all other cases use get_nml_version()
import subprocess, os
def get_child_output(cmd):
"""
Run a child process, and collect the generated output.
@param cmd: Command to execute.
@type cmd: C{list} of C{str}
@return: Generated output of the command, split on whitespace.
@rtype: C{list} of C{str}
"""
return subprocess.check_output(cmd, universal_newlines = True).split()
def get_hg_version():
path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
version = ''
if os.path.isdir(os.path.join(path,'.hg')):
# we want to return to where we were. So save the old path
try:
version_list = get_child_output(['hg', '-R', path, 'id', '-n', '-t', '-i'])
except OSError as e:
print("Mercurial checkout found but cannot determine its version. Error({0}): {1}".format(e.errno, e.strerror))
return version
if version_list[1].endswith('+'):
modified = 'M'
else:
modified = ''
# Test whether we have a tag (=release version) and add it, if found
if len(version_list) > 2 and version_list[2] != 'tip' and modified == '':
# Tagged release
version = version_list[2]
else:
# Branch or modified version
hash = version_list[0].rstrip('+')
# Get the date of the commit of the current NML version in days since January 1st 2000
ctimes = get_child_output(["hg", "-R", path, "parent", "--template='{date|hgdate} {date|shortdate}\n'"])
ctime = (int((ctimes[0].split("'"))[1]) - 946684800) // (60 * 60 * 24)
cversion = str(ctime)
# Combine the version string
version = "v{}{}:{} from {}".format(cversion, modified, hash, ctimes[2].split("'", 1)[0])
return version
def get_lib_versions():
versions = {}
#PIL
try:
from PIL import Image
versions["PIL"] = Image.VERSION
except ImportError:
try:
import Image
versions["PIL"] = Image.VERSION
except ImportError:
versions["PIL"] = "Not found!"
#PLY
try:
from ply import lex
versions["PLY"] = lex.__version__
except ImportError:
versions["PLY"] = "Not found!"
return versions
def get_nml_version():
# first try whether we find an nml repository. Use that version, if available
version = get_hg_version()
if version:
return version
# no repository was found. Return the version which was saved upon built
try:
from nml import __version__
version = __version__.version
except ImportError:
version = 'unknown'
return version
def get_cli_version():
#version string for usage in command line
result = get_nml_version() + "\n"
result += "Library versions encountered:\n"
for lib, lib_ver in list(get_lib_versions().items()):
result += lib + ": " + lib_ver + "\n"
return result[0:-1] #strip trailing newline
def get_and_write_version():
version = get_nml_version()
if version:
try:
path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
f = open(os.path.join(path, "nml", "__version__.py"), "w")
f.write('# this file is autogenerated by setup.py\n')
f.write('version = "{}"\n'.format(version))
f.close()
return version.split()[0]
except IOError:
print("Version file NOT written")
nml-0.4.5/nml/output_base.py 0000644 0005672 0005672 00000022407 13315644406 017155 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
"""
Abstract base classes that implements common functionality for output classes
"""
import array, io
class OutputBase(object):
"""
Base class for output to a data file.
The file is opened with L{open}. Once that is done, data can be written
using the L{file} data member. When finished writing, the file should be
closed with L{close}.
Derived classes should implement L{open_file} to perform the actual opening
of the file. L{assemble_file} is called to actually compose the output from
possible multiple sources.
@ivar filename: Name of the data file.
@type filename: C{str}
@ivar file: Memory output file handle, if opened.
@type file: C{file} or C{None}
"""
def __init__(self, filename):
self.filename = filename
self.file = None
def open(self):
"""
Open the output file. Data gets stored in-memory.
"""
raise NotImplementedError("Implement me in {}".format(type(self)))
def open_file(self):
"""
Construct the file handle of the disk output file.
@return: File handle of the opened file.
@rtype: C{file}
"""
raise NotImplementedError("Implement me in {}".format(type(self)))
def assemble_file(self, real_file):
"""
File is about to be closed, last chance to append data.
@param real_file: Actual output stream.
@type real_file: C{io.IOBase}
"""
real_file.write(self.file.getvalue())
def discard(self):
"""
Close the memory file, without writing to disk.
"""
if isinstance(self.file, io.IOBase):
self.file.close()
self.file = None
def close(self):
"""
Close the memory file, copy collected output to the real file.
"""
real_file = self.open_file()
self.assemble_file(real_file)
real_file.close()
self.discard()
def skip_sprite_checks(self):
"""
Return whether sprites need detailed parsing.
"""
return False
class SpriteOutputBase(OutputBase):
"""
Base class for output to a sprite file.
@ivar in_sprite: Set to true if we are currently outputting a sprite.
Outputting anything when not in a sprite causes an assert.
@type in_sprite: C{bool}
@ivar byte_count: Number of bytes written in the current sprite.
@type byte_count: C{int}
@ivar expected_count: Number of bytes expected in the current sprite.
@type expected_count: C{int}
"""
def __init__(self, filename):
OutputBase.__init__(self, filename)
self.in_sprite = False
self.expected_count = 0
self.byte_count = 0
def close(self):
assert not self.in_sprite
OutputBase.close(self)
def prepare_byte(self, value):
"""
Normalize the provided value to an unsigned byte value.
@param value: Value to normalize.
@type value: C{int}
@return: Normalized value (0..255).
@rtype: C{int}
@precond: Must be outputting a sprite.
"""
assert self.in_sprite
if -0x80 <= value < 0 : value += 0x100
assert value >= 0 and value <= 0xFF
self.byte_count += 1
return value
def prepare_word(self, value):
"""
Normalize the provided value to an unsigned word value.
@param value: Value to normalize.
@type value: C{int}
@return: Normalized value (0..65535).
@rtype: C{int}
@precond: Must be outputting a sprite.
"""
assert self.in_sprite
if -0x8000 <= value < 0: value += 0x10000
assert value >= 0 and value <= 0xFFFF
self.byte_count += 2
return value
def prepare_dword(self, value):
"""
Normalize the provided value to an unsigned double word value.
@param value: Value to normalize.
@type value: C{int}
@return: Normalized value (0..0xFFFFFFFF).
@rtype: C{int}
@precond: Must be outputting a sprite.
"""
assert self.in_sprite
if -0x80000000 <= value < 0: value += 0x100000000
assert value >= 0 and value <= 0xFFFFFFFF
self.byte_count += 4
return value
def print_varx(self, value, size):
"""
Print a variable sized value.
@param value: Value to output.
@type value: C{int}
@param size: Size of the output (1..4), 3 means extended byte.
@type size: C{int}
"""
if size == 1:
self.print_bytex(value)
elif size == 2:
self.print_wordx(value)
elif size == 3:
self.print_bytex(0xFF)
self.print_wordx(value)
elif size == 4:
self.print_dwordx(value)
else:
assert False
def print_bytex(self, byte, pretty_print = None):
"""
Output an unsigned byte.
@param byte: Value to output.
@type byte: C{int}
"""
raise NotImplementedError("Implement print_bytex() in {}".format(type(self)))
def print_wordx(self, byte):
"""
Output an unsigned word (2 bytes).
@param byte: Value to output.
@type byte: C{int}
"""
raise NotImplementedError("Implement print_wordx() in {}".format(type(self)))
def print_dwordx(self, byte):
"""
Output an unsigned double word (4 bytes).
@param byte: Value to output.
@type byte: C{int}
"""
raise NotImplementedError("Implement print_dwordx() in {}".format(type(self)))
def newline(self, msg = "", prefix = "\t"):
"""
Output a line separator, prefixed with L{prefix}, C{"// "}, and the
L{msg}, if the latter is not empty.
@param msg: Optional message to output first.
@type msg: C{str}
@param prefix: Additional white space in front of the comment.
@type prefix: C{str}
"""
raise NotImplementedError("Implement newline() in {}".format(type(self)))
def comment(self, msg):
"""
Output a textual comment at a line by itself.
@param msg: Comment message.
@type msg: C{str}
@note: Only use if no bytes have been written to the current line.
"""
raise NotImplementedError("Implement comment() in {}".format(type(self)))
def start_sprite(self, expected_size):
"""
Note to the output stream that a sprite is about to be written.
@param expected_size: Expected size of the sprite data.
@type expected_size: C{int}
"""
assert not self.in_sprite
self.in_sprite = True
self.expected_count = expected_size
self.byte_count = 0
def end_sprite(self):
"""
Note to the output stream that a sprite has been written. The number of
bytes denoted as expected size with the L{start_sprite} call, should
have been written.
"""
assert self.in_sprite
self.in_sprite = False
self.newline()
assert self.expected_count == self.byte_count, "Expected {:d} bytes to be written to sprite, got {:d}".format(self.expected_count, self.byte_count)
class TextOutputBase(OutputBase):
"""
Base class for textual output.
"""
def __init__(self, filename):
OutputBase.__init__(self, filename)
def open(self):
self.file = io.StringIO()
class BinaryOutputBase(SpriteOutputBase):
"""
Class for binary output.
"""
def __init__(self, filename):
SpriteOutputBase.__init__(self, filename)
def open(self):
self.file = array.array('B')
def newline(self, msg = "", prefix = "\t"):
pass
def print_data(self, data):
"""
Print a chunck of data in one go
@param data: Data to output
@type data: C{array}
"""
self.byte_count += len(data)
self.file.extend(data)
def wb(self, byte):
self.file.append(byte)
def print_byte(self, value):
value = self.prepare_byte(value)
self.wb(value)
def print_bytex(self, value, pretty_print = None):
self.print_byte(value)
def print_word(self, value):
value = self.prepare_word(value)
self.wb(value & 0xFF)
self.wb(value >> 8)
def print_wordx(self, value):
self.print_word(value)
def print_dword(self, value):
value = self.prepare_dword(value)
self.wb(value & 0xFF)
self.wb((value >> 8) & 0xFF)
self.wb((value >> 16) & 0xFF)
self.wb(value >> 24)
def print_dwordx(self, value):
self.print_dword(value)
nml-0.4.5/nml/nmlop.py 0000644 0005672 0005672 00000026356 13315644406 015757 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import operator
from .expression.base_expression import Type, ConstantNumeric, ConstantFloat
from nml import generic
class Operator(object):
"""
Operator in an expression.
@ivar act2_supports: Whether the operator is supported by action 2.
@type act2_supports: C{bool}
@ivar act2_str: NFO text representation of the operator in action 2.
@type act2_str: C{str} or C{None}
@ivar act2_num: Numeric value of the operator in action 2.
@type act2_num: C{int} or C{None}
@ivar actd_supports: Whether the operator is supported by action D.
@type actd_supports: C{bool}
@ivar actd_str: NFO text representation of the operator in action D.
@type actd_str: C{str} or C{None}
@ivar actd_num: Numeric value of the operator in action D.
@type actd_num: C{int} or C{None}
@ivar returns_boolean: Whether the operator gives a boolean result.
@type returns_boolean: C{bool}
@ivar token: Infix text to use for the string representation of the operator.
@type token: C{None} or C{str}
@ivar prefix_text: Prefix text to use for the string representation of the operator, if available.
@type prefix_text: C{None} or C{str}
"""
def __init__(self,
act2_supports = False, act2_str = None, act2_num = None,
actd_supports = False, actd_str = None, actd_num = None,
returns_boolean = False,
token = None, prefix_text = None,
compiletime_func = None,
validate_func = None):
self.act2_supports = act2_supports
self.act2_str = act2_str
self.act2_num = act2_num
self.actd_supports = actd_supports
self.actd_str = actd_str
self.actd_num = actd_num
self.returns_boolean = returns_boolean
self.token = token
self.prefix_text = prefix_text
self.compiletime_func = compiletime_func
self.validate_func = validate_func
def to_string(self, expr1, expr2):
"""
Convert expression to readable string form.
@param expr1: Left child expression text.
@type expr1: C{str}
@param expr2: Right child expression text.
@type expr2: C{str}
@return: Text representation of the operator with its child expressions.
@rtype: C{str}
"""
if self.prefix_text is not None:
return '{}({}, {})'.format(self.prefix_text, expr1, expr2)
else: # Infix notation.
return '({} {} {})'.format(expr1, self.token, expr2)
def unsigned_rshift(a, b):
if a < 0:
a += 0x100000000
return generic.truncate_int32(a >> b)
def unsigned_rrotate(a, b):
if a < 0:
a += 0x100000000
return generic.truncate_int32((a >> b) | (a << (32 - b)))
def validate_func_int(expr1, expr2, pos):
if expr1.type() != Type.INTEGER or expr2.type() != Type.INTEGER:
raise generic.ScriptError("Binary operator requires both operands to be integers.", pos)
def validate_func_float(expr1, expr2, pos):
if expr1.type() not in (Type.INTEGER, Type.FLOAT) or expr2.type() not in (Type.INTEGER, Type.FLOAT):
raise generic.ScriptError("Binary operator requires both operands to be integers or floats.", pos)
# If one is a float, the other must be constant since we can't handle floats at runtime
if (expr1.type() == Type.FLOAT and not isinstance(expr2, (ConstantNumeric, ConstantFloat))) or \
(expr2.type() == Type.FLOAT and not isinstance(expr1, (ConstantNumeric, ConstantFloat))):
raise generic.ScriptError("Floating-point operations are only possible when both operands are compile-time constants.", pos)
def validate_func_add(expr1, expr2, pos):
if (expr1.type() == Type.STRING_LITERAL) ^ (expr2.type() == Type.STRING_LITERAL):
raise generic.ScriptError("Concatenating a string literal and a number is not possible.", pos)
if expr1.type() != Type.STRING_LITERAL:
validate_func_float(expr1, expr2, pos)
def validate_func_div_mod(expr1, expr2, pos):
validate_func_float(expr1, expr2, pos)
if isinstance(expr2, (ConstantNumeric, ConstantFloat)) and expr2.value == 0:
raise generic.ScriptError("Division and modulo require the right hand side to be nonzero.", pos)
def validate_func_rhs_positive(expr1, expr2, pos):
validate_func_int(expr1, expr2, pos)
if isinstance(expr2, ConstantNumeric) and expr2.value < 0:
raise generic.ScriptError("Right hand side of the operator may not be a negative number.", pos)
ADD = Operator(
act2_supports = True, act2_str = r'\2+', act2_num = 0,
actd_supports = True, actd_str = r'\D+', actd_num = 1,
token = '+',
compiletime_func = operator.add,
validate_func = validate_func_add,
)
SUB = Operator(
act2_supports = True, act2_str = r'\2-', act2_num = 1,
actd_supports = True, actd_str = r'\D-', actd_num = 2,
token = '-',
compiletime_func = operator.sub,
validate_func = validate_func_float,
)
DIV = Operator(
act2_supports = True, act2_str = r'\2/', act2_num = 6,
actd_supports = True, actd_str = r'\D/', actd_num = 10,
token = '/',
compiletime_func = lambda a, b: a // b if isinstance(a, int) and isinstance(b, int) else a / b,
validate_func = validate_func_div_mod,
)
MOD = Operator(
act2_supports = True, act2_str = r'\2%', act2_num = 7,
actd_supports = True, actd_str = r'\D%', actd_num = 12,
token = '%',
compiletime_func = operator.mod,
validate_func = validate_func_div_mod,
)
MUL = Operator(
act2_supports = True, act2_str = r'\2*', act2_num = 10,
actd_supports = True, actd_str = r'\D*', actd_num = 4,
token = '*',
compiletime_func = operator.mul,
validate_func = validate_func_float,
)
AND = Operator(
act2_supports = True, act2_str = r'\2&', act2_num = 11,
actd_supports = True, actd_str = r'\D&', actd_num = 7,
token = '&',
compiletime_func = operator.and_,
validate_func = validate_func_int,
)
OR = Operator(
act2_supports = True, act2_str = r'\2|', act2_num = 12,
actd_supports = True, actd_str = r'\D|', actd_num = 8,
token = '|',
compiletime_func = operator.or_,
validate_func = validate_func_int,
)
XOR = Operator(
act2_supports = True, act2_str = r'\2^', act2_num = 13,
actd_supports = True,
token = '^',
compiletime_func = operator.xor,
validate_func = validate_func_int,
)
CMP_EQ = Operator(
act2_supports = True,
actd_supports = True,
returns_boolean = True,
token = '==',
compiletime_func = operator.eq,
validate_func = validate_func_float,
)
CMP_NEQ = Operator(
act2_supports = True,
actd_supports = True,
returns_boolean = True,
token = '!=',
compiletime_func = operator.ne,
validate_func = validate_func_float,
)
CMP_LE = Operator(
act2_supports = True,
actd_supports = True,
returns_boolean = True,
token = '<=',
compiletime_func = operator.le,
validate_func = validate_func_float,
)
CMP_GE = Operator(
act2_supports = True,
actd_supports = True,
returns_boolean = True,
token = '>=',
compiletime_func = operator.ge,
validate_func = validate_func_float,
)
CMP_LT = Operator(
act2_supports = True,
actd_supports = True,
returns_boolean = True,
token = '<',
compiletime_func = operator.lt,
validate_func = validate_func_float,
)
CMP_GT = Operator(
act2_supports = True,
actd_supports = True,
returns_boolean = True,
token = '>',
compiletime_func = operator.gt,
validate_func = validate_func_float,
)
MIN = Operator(
act2_supports = True, act2_str = r'\2<', act2_num = 2,
actd_supports = True,
compiletime_func = min,
prefix_text = "min",
validate_func = validate_func_float,
)
MAX = Operator(
act2_supports = True, act2_str = r'\2>', act2_num = 3,
actd_supports = True,
compiletime_func = max,
prefix_text = "max",
validate_func = validate_func_float,
)
STO_TMP = Operator(
act2_supports = True, act2_str = r'\2sto', act2_num = 14,
prefix_text = "STORE_TEMP",
validate_func = validate_func_rhs_positive,
)
STO_PERM = Operator(
act2_supports = True, act2_str = r'\2psto', act2_num = 16,
prefix_text = "STORE_PERM",
validate_func = validate_func_rhs_positive,
)
SHIFT_LEFT = Operator(
act2_supports = True, act2_str = r'\2<<', act2_num = 20,
actd_supports = True, actd_str = r'\D<<', actd_num = 6,
token = '<<',
compiletime_func = operator.lshift,
validate_func = validate_func_rhs_positive,
)
SHIFT_RIGHT = Operator(
act2_supports = True, act2_str = r'\2>>', act2_num = 22,
actd_supports = True,
token = '>>',
compiletime_func = operator.rshift,
validate_func = validate_func_rhs_positive,
)
SHIFTU_RIGHT = Operator(
act2_supports = True, act2_str = r'\2u>>', act2_num = 21,
actd_supports = True,
token = '>>>',
compiletime_func = unsigned_rshift,
validate_func = validate_func_rhs_positive,
)
HASBIT = Operator(
act2_supports = True,
actd_supports = True,
returns_boolean = True,
prefix_text = "hasbit",
compiletime_func = lambda a, b: (a & (1 << b)) != 0,
validate_func = validate_func_rhs_positive,
)
#A few operators that are generated internally but can't be directly written in nml
NOTHASBIT = Operator(
act2_supports = True,
actd_supports = True,
returns_boolean = True,
prefix_text = "!hasbit",
compiletime_func = lambda a, b: (a & (1 << b)) == 0,
)
VAL2 = Operator(
act2_supports = True, act2_str = r'\2r', act2_num = 15,
compiletime_func = lambda a, b: b,
)
ASSIGN = Operator(
actd_supports = True, actd_str = r'\D=', actd_num = 0,
)
SHIFTU_LEFT = Operator(
actd_supports = True, actd_str = r'\Du<<', actd_num = 5,
token = '<<',
)
VACT2_CMP = Operator(
act2_supports = True, act2_str = r'\2cmp', act2_num = 18,
prefix_text = "CMP",
)
VACT2_UCMP = Operator(
act2_supports = True, act2_str = r'\2ucmp', act2_num = 19,
prefix_text = "UCMP",
)
MINU = Operator(
act2_supports = True, act2_str = r'\2u<', act2_num = 4,
)
ROT_RIGHT = Operator(
act2_supports = True, act2_str = r'\2ror', act2_num = 17,
prefix_text = "rotate",
compiletime_func = unsigned_rrotate,
validate_func = validate_func_int,
)
DIVU = Operator(
act2_supports = True, act2_str = r'\2u/', act2_num = 8,
actd_supports = True, actd_str = r'\Du/', actd_num = 9,
)
class GRMOperator(object):
def __init__(self, op_str, op_num):
self.op_str = op_str
self.op_num = op_num
self.value = op_num
def __str__(self):
return self.op_str
def write(self, file, size):
assert size == 1
file.print_bytex(self.op_num, self.op_str)
GRM_RESERVE = GRMOperator(r'\DR', 0)
nml-0.4.5/nml/grfstrings.py 0000644 0005672 0005672 00000142141 13315644406 017011 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import os, codecs, glob, re
from nml import generic
def utf8_get_size(char):
if char < 128: return 1
if char < 2048: return 2
if char < 65536: return 3
return 4
DEFAULT_LANGUAGE = 0x7F
DEFAULT_LANGNAME = "english.lng"
def validate_string(string):
"""
Check if a given string refers to a string that is translated in the language
files and raise an error otherwise.
@param string: The string to validate.
@type string: L{expression.String}
"""
if string.name.value not in default_lang.strings:
raise generic.ScriptError('Unknown string "{}"'.format(string.name.value), string.pos)
def is_ascii_string(string):
"""
Check whether a given string can be written using the ASCII codeset or
that we need unicode.
@param string: The string to check.
@type string: C{str}
@return: True iff the string is ascii-only.
@rtype: C{bool}
"""
assert isinstance(string, str)
i = 0
while i < len(string):
if string[i] != '\\':
if ord(string[i]) >= 0x7B:
return False
i += 1
else:
if string[i+1] in ('\\', '"'):
i += 2
elif string[i+1] == 'U':
return False
else:
i += 3
return True
def get_string_size(string, final_zero = True, force_ascii = False):
"""
Get the size (in bytes) of a given string.
@param string: The string to check.
@type string: C{str}
@param final_zero: Whether or not to account for a zero-byte directly after the string.
@type final_zero: C{bool}
@param force_ascii: When true, make sure the string is written as ascii as opposed to unicode.
@type force_ascii: C{bool}
@return: The length (in bytes) of the given string.
@rtype: C{int}
@raise generic.ScriptError: force_ascii and not is_ascii_string(string).
"""
size = 0
if final_zero: size += 1
if not is_ascii_string(string):
if force_ascii:
raise generic.ScriptError("Expected ascii string but got a unicode string")
size += 2
i = 0
while i < len(string):
if string[i] != '\\':
size += utf8_get_size(ord(string[i]))
i += 1
else:
if string[i+1] in ('\\', '"'):
size += 1
i += 2
elif string[i+1] == 'U':
size += utf8_get_size(int(string[i+2:i+6], 16))
i += 6
else:
size += 1
i += 3
return size
def get_translation(string, lang_id = DEFAULT_LANGUAGE):
"""
Get the translation of a given string in a certain language. If there is no
translation available in the given language return the default translation.
@param string: the string to get the translation for.
@type string: L{expression.String}
@param lang_id: The language id of the language to translate the string into.
@type lang_id: C{int}
@return: Translation of the given string in the given language.
@rtype: C{str}
"""
for lang_pair in langs:
langid, lang = lang_pair
if langid != lang_id: continue
if string.name.value not in lang.strings: break
return lang.get_string(string, lang_id)
return default_lang.get_string(string, lang_id)
def get_translations(string):
"""
Get a list of language ids that have a translation for the given string.
@param string: the string to get the translations for.
@type string: L{expression.String}
@return: List of languages that translate the given string.
@rtype: C{list} of C{int}
"""
translations = []
for lang_pair in langs:
langid, lang = lang_pair
assert langid is not None
if string.name.value in lang.strings and lang.get_string(string, langid) != default_lang.get_string(string, langid):
translations.append(langid)
# Also check for translated substrings
import nml.expression
for param in string.params:
if not isinstance(param, nml.expression.String): continue
param_translations = get_translations(param)
translations.extend([langid for langid in param_translations if not langid in translations])
return translations
def com_parse_comma(val, lang_id):
val = val.reduce_constant()
return str(val)
def com_parse_hex(val, lang_id):
val = val.reduce_constant()
return "0x{:X}".format(val.value)
def com_parse_string(val, lang_id):
import nml.expression
if not isinstance(val, (nml.expression.StringLiteral, nml.expression.String)):
raise generic.ScriptError("Expected a (literal) string", val.pos)
if isinstance(val, nml.expression.String):
# Check that the string exists
if val.name.value not in default_lang.strings:
raise generic.ScriptError("Substring \"{}\" does not exist".format(val.name.value), val.pos)
return get_translation(val, lang_id)
return val.value
commands = {
# Special characters / glyphs
'': {'unicode': r'\0D', 'ascii': r'\0D'},
'{': {'unicode': r'{', 'ascii': r'{' },
'NBSP': {'unicode': r'\U00A0'}, # character A0 is used as up arrow in TTD, so don't use ASCII here.
'COPYRIGHT': {'unicode': r'\U00A9', 'ascii': r'\A9'},
'TRAIN': {'unicode': r'\UE0B4', 'ascii': r'\B4'},
'LORRY': {'unicode': r'\UE0B5', 'ascii': r'\B5'},
'BUS': {'unicode': r'\UE0B6', 'ascii': r'\B6'},
'PLANE': {'unicode': r'\UE0B7', 'ascii': r'\B7'},
'SHIP': {'unicode': r'\UE0B8', 'ascii': r'\B8'},
# Change the font size.
'TINYFONT': {'unicode': r'\0E', 'ascii': r'\0E'},
'BIGFONT': {'unicode': r'\0F', 'ascii': r'\0F'},
'COMMA': {'unicode': r'\UE07B', 'ascii': r'\7B', 'size': 4, 'parse': com_parse_comma},
'SIGNED_WORD': {'unicode': r'\UE07C', 'ascii': r'\7C', 'size': 2, 'parse': com_parse_comma},
'UNSIGNED_WORD': {'unicode': r'\UE07E', 'ascii': r'\7E', 'size': 2, 'parse': com_parse_comma},
'CURRENCY': {'unicode': r'\UE07F', 'ascii': r'\7F', 'size': 4},
'STRING': {'unicode': r'\UE080', 'ascii': r'\80', 'allow_case': True, 'size': 2, 'parse': com_parse_string},
'DATE1920_LONG': {'unicode': r'\UE082', 'ascii': r'\82', 'size': 2},
'DATE1920_SHORT': {'unicode': r'\UE083', 'ascii': r'\83', 'size': 2},
'VELOCITY': {'unicode': r'\UE084', 'ascii': r'\84', 'size': 2},
'SKIP': {'unicode': r'\UE085', 'ascii': r'\85', 'size': 2},
'VOLUME': {'unicode': r'\UE087', 'ascii': r'\87', 'size': 2},
'HEX': {'unicode': r'\UE09A\08', 'ascii': r'\9A\08', 'size': 4, 'parse': com_parse_hex},
'STATION': {'unicode': r'\UE09A\0C', 'ascii': r'\9A\0C', 'size': 2},
'WEIGHT': {'unicode': r'\UE09A\0D', 'ascii': r'\9A\0D', 'size': 2},
'DATE_LONG': {'unicode': r'\UE09A\16', 'ascii': r'\9A\16', 'size': 4},
'DATE_SHORT': {'unicode': r'\UE09A\17', 'ascii': r'\9A\17', 'size': 4},
'POWER': {'unicode': r'\UE09A\18', 'ascii': r'\9A\18', 'size': 2},
'VOLUME_SHORT': {'unicode': r'\UE09A\19', 'ascii': r'\9A\19', 'size': 2},
'WEIGHT_SHORT': {'unicode': r'\UE09A\1A', 'ascii': r'\9A\1A', 'size': 2},
'CARGO_LONG': {'unicode': r'\UE09A\1B', 'ascii': r'\9A\1B', 'size': 2 * 2},
'CARGO_SHORT': {'unicode': r'\UE09A\1C', 'ascii': r'\9A\1C', 'size': 2 * 2},
'CARGO_TINY': {'unicode': r'\UE09A\1D', 'ascii': r'\9A\1D', 'size': 2 * 2},
'CARGO_NAME': {'unicode': r'\UE09A\1E', 'ascii': r'\9A\1E', 'size': 2},
# Colors
'BLUE': {'unicode': r'\UE088', 'ascii': r'\88'},
'SILVER': {'unicode': r'\UE089', 'ascii': r'\89'},
'GOLD': {'unicode': r'\UE08A', 'ascii': r'\8A'},
'RED': {'unicode': r'\UE08B', 'ascii': r'\8B'},
'PURPLE': {'unicode': r'\UE08C', 'ascii': r'\8C'},
'LTBROWN': {'unicode': r'\UE08D', 'ascii': r'\8D'},
'ORANGE': {'unicode': r'\UE08E', 'ascii': r'\8E'},
'GREEN': {'unicode': r'\UE08F', 'ascii': r'\8F'},
'YELLOW': {'unicode': r'\UE090', 'ascii': r'\90'},
'DKGREEN': {'unicode': r'\UE091', 'ascii': r'\91'},
'CREAM': {'unicode': r'\UE092', 'ascii': r'\92'},
'BROWN': {'unicode': r'\UE093', 'ascii': r'\93'},
'WHITE': {'unicode': r'\UE094', 'ascii': r'\94'},
'LTBLUE': {'unicode': r'\UE095', 'ascii': r'\95'},
'GRAY': {'unicode': r'\UE096', 'ascii': r'\96'},
'DKBLUE': {'unicode': r'\UE097', 'ascii': r'\97'},
'BLACK': {'unicode': r'\UE098', 'ascii': r'\98'},
# Deprecated string codes
'DWORD_S': {'unicode': r'\UE07B', 'ascii': r'\7B', 'deprecated': True, 'size': 4},
'PARAM': {'unicode': r'\UE07B', 'ascii': r'\7B', 'deprecated': True, 'size': 4},
'WORD_S': {'unicode': r'\UE07C', 'ascii': r'\7C', 'deprecated': True, 'size': 2},
'BYTE_S': {'unicode': r'\UE07D', 'ascii': r'\7D', 'deprecated': True},
'WORD_U': {'unicode': r'\UE07E', 'ascii': r'\7E', 'deprecated': True, 'size': 2},
'POP_WORD': {'unicode': r'\UE085', 'ascii': r'\85', 'deprecated': True, 'size': 2},
'CURRENCY_QWORD': {'unicode': r'\UE09A\01', 'ascii': r'\9A\01', 'deprecated': True},
'PUSH_WORD': {'unicode': r'\UE09A\03', 'ascii': r'\9A\03', 'deprecated': True},
'UNPRINT': {'unicode': r'\UE09A\04', 'ascii': r'\9A\04', 'deprecated': True},
'BYTE_HEX': {'unicode': r'\UE09A\06', 'ascii': r'\9A\06', 'deprecated': True},
'WORD_HEX': {'unicode': r'\UE09A\07', 'ascii': r'\9A\07', 'deprecated': True, 'size': 2},
'DWORD_HEX': {'unicode': r'\UE09A\08', 'ascii': r'\9A\08', 'deprecated': True, 'size': 4},
'QWORD_HEX': {'unicode': r'\UE09A\0B', 'ascii': r'\9A\0B', 'deprecated': True},
'WORD_S_TONNES': {'unicode': r'\UE09A\0D', 'ascii': r'\9A\0D', 'deprecated': True, 'size': 2},
}
special_commands = [
'P',
'G',
'G=',
]
def read_extra_commands(custom_tags_file):
"""
@param custom_tags_file: Filename of the custom tags file.
@type custom_tags_file: C{str}
"""
if not os.access(custom_tags_file, os.R_OK):
#Failed to open custom_tags.txt, ignore this
return
line_no = 0
for line in codecs.open(generic.find_file(custom_tags_file), "r", "utf-8"):
line_no += 1
line = line.strip()
if len(line) == 0 or line[0] == "#": continue
i = line.find(':')
if i == -1:
raise generic.ScriptError("Line has no ':' delimiter.", generic.LinePosition(custom_tags_file, line_no))
name = line[:i].strip()
value = line[i+1:]
if name in commands:
generic.print_warning('Overwriting existing tag "' + name + '".', generic.LinePosition(custom_tags_file, line_no))
commands[name] = {'unicode': value}
if is_ascii_string(value):
commands[name]['ascii'] = value
class StringCommand(object):
"""
Instantiated string command.
@ivar name: Name of the string command.
@type name: C{str}
@ivar case: ???
@type case: ???
@ivar arguments: Arguments of the instantiated command.
@type arguments: C{list} of ???
@ivar offset: Index to an argument of the P or G string command (ie "2" in {P 2 ...}), if available.
@type offset: C{int} or C{None}
@ivar str_pos: String argument index ("2" in {2:FOO..}, if available.
@type str_pos: C{int} or C{None}
@ivar pos: Position of the string.
@type pos: L{Position}
"""
def __init__(self, name, str_pos, pos):
assert name in commands or name in special_commands
self.name = name
self.case = None
self.arguments = []
self.offset = None
self.str_pos = str_pos
self.pos = pos
def set_arguments(self, arg_string):
start = -1
cur = 0
quoted = False
whitespace = " \t"
while cur < len(arg_string):
if start != -1:
if (quoted and arg_string[cur] == '"') or (not quoted and arg_string[cur] in whitespace):
if not quoted and self.offset is None and len(self.arguments) == 0 and isint(arg_string[start:cur]) and self.name in ('P', 'G'):
self.offset = int(arg_string[start:cur])
else:
self.arguments.append(arg_string[start:cur])
start = -1
elif arg_string[cur] not in whitespace:
quoted = arg_string[cur] == '"'
start = cur + 1 if quoted else cur
cur += 1
if start != -1 and not quoted:
self.arguments.append(arg_string[start:])
start = -1
return start == -1
def validate_arguments(self, lang):
if lang.langid == DEFAULT_LANGUAGE: return
if self.name == 'P':
if not lang.has_plural_pragma():
raise generic.ScriptError("Using {P} without a ##plural pragma", self.pos)
if len(self.arguments) != lang.get_num_plurals():
raise generic.ScriptError("Invalid number of arguments to plural command, expected {:d} but got {:d}".format(lang.get_num_plurals(), len(self.arguments)), self.pos)
elif self.name == 'G':
if not lang.has_gender_pragma():
raise generic.ScriptError("Using {G} without a ##gender pragma", self.pos)
if len(self.arguments) != len(lang.genders):
raise generic.ScriptError("Invalid number of arguments to gender command, expected {:d} but got {:d}".format(len(lang.genders), len(self.arguments)), self.pos)
elif self.name == 'G=':
if not lang.has_gender_pragma():
raise generic.ScriptError("Using {G=} without a ##gender pragma", self.pos)
if len(self.arguments) != 1:
raise generic.ScriptError("Invalid number of arguments to set-gender command, expected {:d} but got {:d}".format(1, len(self.arguments)), self.pos)
elif len(self.arguments) != 0:
raise generic.ScriptError("Unexpected arguments to command \"{}\"".format(self.name), self.pos)
def parse_string(self, str_type, lang, wanted_lang_id, prev_command, stack, static_args):
"""
Convert the string command to output text.
@param str_type: Exptected type of result text, C{"unicode"} or C{"ascii"}.
@type str_type: C{str}
@param lang: Language of the string.
@type lang: L{Language}
@param wanted_lang_id: Language-id to use for interpreting the command (this string may be from another language, eg with missing strings).
@param prev_command: Argument of previous string command (parameter number, size).
@type prev_command: C{tuple} or C{None}
@param stack: Stack of available arguments (list of (parameter number, size)).
@type stack: C{list} of C{tuple} (C{int}, C{int}) or C{None}
@param static_args: Static command arguments.
"""
if self.name in commands:
if not self.is_important_command():
return commands[self.name][str_type]
# Compute position of the argument in the stack.
stack_pos = 0
for (pos, size) in stack:
if pos == self.str_pos:
break
stack_pos += size
self_size = commands[self.name]['size']
stack.remove((self.str_pos, self_size))
if self.str_pos < len(static_args):
if 'parse' not in commands[self.name]:
raise generic.ScriptError("Provided a static argument for string command '{}' which is invalid".format(self.name), self.pos)
# Parse commands using the wanted (not current) lang id, so translations are used if present
return commands[self.name]['parse'](static_args[self.str_pos], wanted_lang_id)
prefix = ''
suffix = ''
if self.case:
prefix += STRING_SELECT_CASE[str_type] + '\\{:02X}'.format(self.case)
if stack_pos + self_size > 8:
raise generic.ScriptError("Trying to read an argument from the stack without reading the arguments before", self.pos)
if self_size == 4 and stack_pos == 4:
prefix += STRING_ROTATE[str_type] + STRING_ROTATE[str_type]
elif self_size == 4 and stack_pos == 2:
prefix += STRING_PUSH_WORD[str_type] + STRING_ROTATE[str_type] + STRING_ROTATE[str_type]
suffix += STRING_SKIP[str_type]
elif self_size == 2 and stack_pos == 6:
prefix += STRING_ROTATE[str_type]
elif self_size == 2 and stack_pos == 4:
prefix += STRING_PUSH_WORD[str_type] + STRING_ROTATE[str_type]
suffix += STRING_SKIP[str_type]
elif self_size == 2 and stack_pos == 2:
prefix += STRING_PUSH_WORD[str_type] + STRING_PUSH_WORD[str_type] + STRING_ROTATE[str_type]
suffix += STRING_SKIP[str_type] + STRING_SKIP[str_type]
else:
assert stack_pos == 0
return prefix + commands[self.name][str_type] + suffix
assert self.name in special_commands
# Create a local copy because we shouldn't modify the original
offset = self.offset
if offset is None:
if self.name == 'P':
if not prev_command:
raise generic.ScriptError("A plural choice list {P} has to be preceded by another string code or provide an offset", self.pos)
offset = prev_command[0]
else:
if not stack:
raise generic.ScriptError("A gender choice list {G} has to be followed by another string code or provide an offset", self.pos)
offset = stack[0][0]
offset -= len(static_args)
if self.name == 'P':
if offset < 0:
return self.arguments[lang.static_plural_form(static_args[offset]) - 1]
ret = BEGIN_PLURAL_CHOICE_LIST[str_type] + '\\{:02X}'.format(0x80 + offset)
for idx, arg in enumerate(self.arguments):
if idx == len(self.arguments) - 1:
ret += CHOICE_LIST_DEFAULT[str_type]
else:
ret += CHOICE_LIST_ITEM[str_type] + '\\{:02X}'.format(idx + 1)
ret += arg
ret += CHOICE_LIST_END[str_type]
return ret
if self.name == 'G':
if offset < 0:
return self.arguments[lang.static_gender(static_args[offset]) - 1]
ret = BEGIN_GENDER_CHOICE_LIST[str_type] + '\\{:02X}'.format(0x80 + offset)
for idx, arg in enumerate(self.arguments):
if idx == len(self.arguments) - 1:
ret += CHOICE_LIST_DEFAULT[str_type]
else:
ret += CHOICE_LIST_ITEM[str_type] + '\\{:02X}'.format(idx + 1)
ret += arg
ret += CHOICE_LIST_END[str_type]
return ret
def get_type(self):
if self.name in commands:
if 'ascii' in commands[self.name]: return 'ascii'
else: return 'unicode'
if self.name == 'P' or self.name == 'G':
for arg in self.arguments:
if not is_ascii_string(arg): return 'unicode'
return 'ascii'
def is_important_command(self):
if self.name in special_commands: return False
return 'size' in commands[self.name]
def get_arg_size(self):
return commands[self.name]['size']
# Characters that are valid in hex numbers
VALID_HEX = "0123456789abcdefABCDEF"
def is_valid_hex(string):
return all(c in VALID_HEX for c in string)
def validate_escapes(string, pos):
"""
Validate that all escapes (starting with a backslash) are correct.
When an invalid escape is encountered, an error is thrown
@param string: String to validate
@type string: C{str}
@param pos: Position information
@type pos: L{Position}
"""
i = 0
while i < len(string):
# find next '\'
i = string.find('\\', i)
if i == -1: break
if i+1 >= len(string):
raise generic.ScriptError("Unexpected end-of-line encountered after '\\'", pos)
if string[i+1] in ('\\', '"'):
i += 2
elif string[i+1] == 'U':
if i+5 >= len(string) or not is_valid_hex(string[i+2:i+6]):
raise generic.ScriptError("Expected 4 hexadecimal characters after '\\U'", pos)
i += 6
else:
if i+2 >= len(string) or not is_valid_hex(string[i+1:i+3]):
raise generic.ScriptError("Expected 2 hexadecimal characters after '\\'", pos)
i += 3
class NewGRFString(object):
"""
A string text in a language.
@ivar string: String text.
@type string: C{str}
@ivar cases: Mapping of cases to ...
@type cases: ???
@ivar components: Split string text.
@type components: C{list} of (C{str} or L{StringCommand})
@ivar pos: Position of the string text.
@type pos: L{Position}
"""
def __init__(self, string, lang, pos):
"""
Construct a L{NewGRFString}, and break down the string text into text literals and string commands.
@param string: String text.
@type string: C{str}
@param lang: Language containing this string.
@type lang: L{Language}
@param pos: Position of the string text.
@type pos: L{Position}
"""
validate_escapes(string, pos)
self.string = string
self.cases = {}
self.components = []
self.pos = pos
idx = 0
while idx < len(string):
if string[idx] != '{':
j = string.find('{', idx)
if j == -1:
self.components.append(string[idx:])
break
self.components.append(string[idx:j])
idx = j
start = idx + 1
end = start
cmd_pos = None
if start >= len(string):
raise generic.ScriptError("Expected '}' before end-of-line.", pos)
if string[start].isdigit():
while end < len(string) and string[end].isdigit(): end += 1
if end == len(string) or string[end] != ':':
raise generic.ScriptError("Error while parsing position part of string command", pos)
cmd_pos = int(string[start:end])
start = end + 1
end = start
#Read the command name
while end < len(string) and string[end] not in '} =.': end += 1
command_name = string[start:end]
if end < len(string) and string[end] == '=':
command_name += '='
if command_name not in commands and command_name not in special_commands:
raise generic.ScriptError("Undefined command \"{}\"".format(command_name), pos)
if command_name in commands and 'deprecated' in commands[command_name]:
generic.print_warning("String code '{}' has been deprecated and will be removed soon".format(command_name), pos)
del commands[command_name]['deprecated']
#
command = StringCommand(command_name, cmd_pos, pos)
if end >= len(string):
raise generic.ScriptError("Missing '}' from command \"{}\"".format(string[start:]), pos)
if string[end] == '.':
if command_name not in commands or 'allow_case' not in commands[command_name]:
raise generic.ScriptError("Command \"{}\" can't have a case".format(command_name), pos)
case_start = end + 1
end = case_start
while end < len(string) and string[end] not in '} ': end += 1
case = string[case_start:end]
if lang.cases is None or case not in lang.cases:
raise generic.ScriptError("Invalid case-name \"{}\"".format(case), pos)
command.case = lang.cases[case]
if string[end] != '}':
command.argument_is_assigment = string[end] == '='
arg_start = end + 1
end = string.find('}', end + 1)
if end == -1 or not command.set_arguments(string[arg_start:end]):
raise generic.ScriptError("Missing '}' from command \"{}\"".format(string[start:]), pos)
command.validate_arguments(lang)
if command_name == 'G=' and self.components:
raise generic.ScriptError("Set-gender command {G=} must be at the start of the string", pos)
self.components.append(command)
idx = end + 1
if len(self.components) > 0 and isinstance(self.components[0], StringCommand) and self.components[0].name == 'G=':
self.gender = self.components[0].arguments[0]
if self.gender not in lang.genders:
raise generic.ScriptError("Invalid gender name '{}'".format(self.gender), pos)
self.components.pop(0)
else:
self.gender = None
cmd_pos = 0
for cmd in self.components:
if not (isinstance(cmd, StringCommand) and cmd.is_important_command()):
continue
if cmd.str_pos is None:
cmd.str_pos = cmd_pos
cmd_pos = cmd.str_pos + 1
def get_type(self):
"""
Get the type of string text.
@return: C{"unicode"} if Unicode is required for expressing the string text, else C{"ascii"}.
@rtype: C{str}
"""
for comp in self.components:
if isinstance(comp, StringCommand):
if comp.get_type() == 'unicode':
return 'unicode'
else:
if not is_ascii_string(comp): return 'unicode'
for case in list(self.cases.values()):
if case.get_type() == 'unicode':
return 'unicode'
return 'ascii'
def remove_non_default_commands(self):
i = 0
while i < len(self.components):
comp = self.components[i]
if isinstance(comp, StringCommand):
if comp.name == 'P' or comp.name == 'G':
self.components[i] = comp.arguments[-1] if comp.arguments else ""
i += 1
def parse_string(self, str_type, lang, wanted_lang_id, static_args):
"""
Convert the string text to output text.
"""
ret = ""
stack = [(idx, size) for idx, size in enumerate(self.get_command_sizes())]
prev_command = None
for comp in self.components:
if isinstance(comp, StringCommand):
next_command = stack[0] if stack else None
ret += comp.parse_string(str_type, lang, wanted_lang_id, prev_command, stack, static_args)
if (comp.name in commands) and comp.is_important_command():
prev_command = next_command
else:
ret += comp
return ret
def get_command_sizes(self):
"""
Get the size of each string parameter.
"""
sizes = {}
for cmd in self.components:
if not (isinstance(cmd, StringCommand) and cmd.is_important_command()):
continue
if cmd.str_pos in sizes:
raise generic.ScriptError("Two or more string commands are using the same argument", self.pos)
sizes[cmd.str_pos] = cmd.get_arg_size()
sizes_list = []
for idx in range(len(sizes)):
if idx not in sizes:
raise generic.ScriptError("String argument {:d} is not used".format(idx), self.pos)
sizes_list.append(sizes[idx])
return sizes_list
def match_commands(self, other_string):
return self.get_command_sizes() == other_string.get_command_sizes()
def isint(x, base = 10):
try:
int(x, base)
return True
except ValueError:
return False
NUM_PLURAL_FORMS = 13
CHOICE_LIST_ITEM = {'unicode': r'\UE09A\10', 'ascii': r'\9A\10'}
CHOICE_LIST_DEFAULT = {'unicode': r'\UE09A\11', 'ascii': r'\9A\11'}
CHOICE_LIST_END = {'unicode': r'\UE09A\12', 'ascii': r'\9A\12'}
BEGIN_GENDER_CHOICE_LIST = {'unicode': r'\UE09A\13', 'ascii': r'\9A\13'}
BEGIN_CASE_CHOICE_LIST = {'unicode': r'\UE09A\14', 'ascii': r'\9A\14'}
BEGIN_PLURAL_CHOICE_LIST = {'unicode': r'\UE09A\15', 'ascii': r'\9A\15'}
SET_STRING_GENDER = {'unicode': r'\UE09A\0E', 'ascii': r'\9A\0E'}
STRING_SKIP = {'unicode': r'\UE085', 'ascii': r'\85'}
STRING_ROTATE = {'unicode': r'\UE086', 'ascii': r'\86'}
STRING_PUSH_WORD = {'unicode': r'\UE09A\03\20\20', 'ascii': r'\9A\03\20\20'}
STRING_SELECT_CASE = {'unicode': r'\UE09A\0F', 'ascii': r'\9A\0F'}
# Information about languages, borrowed from OpenTTD.
# (isocode name, grf langid, plural number)
LANG_INFOS = [('af_ZA', 0x1b, 0),
('ar_EG', 0x14, 1),
('be_BY', 0x10, 6),
('bg_BG', 0x18, 0),
('ca_ES', 0x22, 0),
('cs_CZ', 0x15, 10),
('cv_RU', 0x0b, 0),
('cy_GB', 0x0f, 0),
('da_DK', 0x2d, 0),
('de_DE', 0x02, 0),
('el_GR', 0x1e, 2),
('en_AU', 0x3d, 0),
('en_GB', 0x01, 0),
('en_US', 0x00, 0),
('eo_EO', 0x05, 0),
('es_ES', 0x04, 0),
('et_EE', 0x34, 0),
('eu_ES', 0x21, 0),
('fa_IR', 0x62, 0),
('fi_FI', 0x35, 0),
('fo_FO', 0x12, 0),
('fr_FR', 0x03, 2),
('fy_NL', 0x32, 0),
('ga_IE', 0x08, 4),
('gd_GB', 0x13, 13),
('gl_ES', 0x31, 0),
('he_IL', 0x61, 0),
('hr_HR', 0x38, 6),
('hu_HU', 0x24, 2),
('id_ID', 0x5a, 1),
('io_IO', 0x06, 0),
('is_IS', 0x29, 0),
('it_IT', 0x27, 0),
('ja_JP', 0x39, 1),
('ko_KR', 0x3a, 11),
('la_VA', 0x66, 0),
('lb_LU', 0x23, 0),
('lt_LT', 0x2b, 5),
('lv_LV', 0x2a, 3),
('mk_MK', 0x26, 0),
('mr_IN', 0x11, 0),
('ms_MY', 0x3c, 0),
('mt_MT', 0x09, 12),
('nb_NO', 0x2f, 0),
('nl_NL', 0x1f, 0),
('nn_NO', 0x0e, 0),
('pl_PL', 0x30, 7),
('pt_BR', 0x37, 2),
('pt_PT', 0x36, 0),
('ro_RO', 0x28, 0),
('ru_RU', 0x07, 6),
('sk_SK', 0x16, 10),
('sl_SI', 0x2c, 8),
('sr_RS', 0x0d, 6),
('sv_SE', 0x2e, 0),
('ta_IN', 0x0a, 0),
('th_TH', 0x42, 1),
('tr_TR', 0x3e, 1),
('uk_UA', 0x33, 6),
('ur_PK', 0x5c, 0),
('vi_VN', 0x54, 1),
('zh_CN', 0x56, 1),
('zh_TW', 0x0c, 1),
]
LANG_NAMES = dict((lng[0], lng[1]) for lng in LANG_INFOS)
LANG_PLURALS = dict((lng[1], lng[2]) for lng in LANG_INFOS)
class Language(object):
"""
@ivar default: Whether the language is the default language.
@type default: C{bool}
@ivar langid: Language id of the language, if known.
@type langid: C{None} or C{int}
@ivar plural: Plural type.
@type plural: C{None} or C{int}
@ivar genders:
@type genders:
@ivar gender_map:
@type gender_map:
@ivar cases:
@type cases:
@ivar case_map:
@type case_map:
@ivar strings: Language strings of the file.
@type strings: C{dict} of
"""
def __init__(self, default):
self.default = default
self.langid = None
self.plural = None
self.genders = None
self.gender_map = {}
self.cases = None
self.case_map = {}
self.strings = {}
def get_num_plurals(self):
if self.plural is None: return 0
num_plurals = {
0: 2,
1: 1,
2: 2,
3: 3,
4: 5,
5: 3,
6: 3,
7: 3,
8: 4,
9: 2,
10: 3,
11: 2,
12: 4,
13: 4,
}
return num_plurals[self.plural]
def has_plural_pragma(self):
return self.plural is not None
def has_gender_pragma(self):
return self.genders is not None
def static_gender(self, expr):
import nml.expression
if isinstance(expr, nml.expression.StringLiteral):
return len(self.genders)
if not isinstance(expr, nml.expression.String):
raise generic.ScriptError("{G} can only refer to a string argument")
parsed = self.get_string(expr, self.langid)
if parsed.find(SET_STRING_GENDER['ascii']) == 0:
return int(parsed[len(SET_STRING_GENDER['ascii']) + 1 : len(SET_STRING_GENDER['ascii']) + 3], 16)
if parsed.find(SET_STRING_GENDER['unicode']) == 0:
return int(parsed[len(SET_STRING_GENDER['unicode']) + 1 : len(SET_STRING_GENDER['unicode']) + 3], 16)
return len(self.genders)
def static_plural_form(self, expr):
#Return values are the same as "Plural index" here:
#http://newgrf-specs.tt-wiki.net/wiki/StringCodes#Using_plural_forms
val = expr.reduce_constant().value
if self.plural == 0:
return 1 if val == 1 else 2
if self.plural == 1:
return 1
if self.plural == 2:
return 1 if val in (0, 1) else 2
if self.plural == 3:
if val % 10 == 1 and val % 100 != 11:
return 1
return 2 if val == 0 else 3
if self.plural == 4:
if val == 1:
return 1
if val == 2:
return 2
if 3 <= val <= 6:
return 3
if 7 <= val <= 10:
return 4
return 5
if self.plural == 5:
if val % 10 == 1 and val % 100 != 11:
return 1
if 2 <= (val % 10) <= 9 and not 12 <= (val % 100) <= 19:
return 2
return 3
if self.plural == 6:
if val % 10 == 1 and val % 100 != 11:
return 1
if 2 <= (val % 10) <= 4 and not 12 <= (val % 100) <= 14:
return 2
return 3
if self.plural == 7:
if val == 0:
return 1
if 2 <= (val % 10) <= 4 and not 12 <= (val % 100) <= 14:
return 2
return 3
if self.plural == 8:
if val % 100 == 1:
return 1
if val % 100 == 2:
return 2
if val % 100 in (3, 4):
return 3
return 4
if self.plural == 9:
if val % 10 == 1 and val % 100 != 11:
return 1
return 2
if self.plural == 10:
if val == 1:
return 1
if 2 <= val <= 4:
return 2
return 3
if self.plural == 11:
if val % 10 in (0, 1, 3, 6, 7, 8):
return 1
return 2
if self.plural == 12:
if val == 1:
return 1
if val == 0 or 2 <= (val % 100) <= 10:
return 2
if 11 <= (val % 100) <= 19:
return 3
return 4
if self.plural == 13:
if val == 1 or val == 11:
return 1
if val == 2 or val == 12:
return 2
if (val >= 3 and val <= 10) or (val >= 13 and val <=19):
return 3
return 4
assert False, "Unknown plural type"
def get_string(self, string, lang_id):
"""
Lookup up a string by name/params and return the actual created string
@param string: String object
@type string: L{expression.String}
@param lang_id: Language ID we are actually looking for.
This may differ from the ID of this language,
if the string is missing from the target language.
@type lang_id: C{int}
@return: The created string
@rtype: C{str}
"""
string_id = string.name.value
assert isinstance(string_id, str)
assert string_id in self.strings
assert lang_id == self.langid or self.langid == DEFAULT_LANGUAGE
str_type = self.strings[string_id].get_type()
parsed_string = ""
if self.strings[string_id].gender is not None:
parsed_string += SET_STRING_GENDER[str_type] + '\\{:02X}'.format(self.genders[self.strings[string_id].gender])
if len(self.strings[string_id].cases) > 0:
parsed_string += BEGIN_CASE_CHOICE_LIST[str_type]
for case_name, case_string in sorted(self.strings[string_id].cases.items()):
case_id = self.cases[case_name]
parsed_string += CHOICE_LIST_ITEM[str_type] + '\\{:02X}'.format(case_id) + case_string.parse_string(str_type, self, lang_id, string.params)
parsed_string += CHOICE_LIST_DEFAULT[str_type]
parsed_string += self.strings[string_id].parse_string(str_type, self, lang_id, string.params)
if len(self.strings[string_id].cases) > 0:
parsed_string += CHOICE_LIST_END[str_type]
return parsed_string
def handle_grflangid(self, data, pos):
"""
Handle a 'grflangid' pragma.
@param data: Data of the pragma.
@type data: C{str}
"""
if self.langid is not None:
raise generic.ScriptError("grflangid already set", pos)
lang_text = data[1].strip()
value = LANG_NAMES.get(lang_text)
if value is None:
try:
value = int(lang_text, 16)
except ValueError:
raise generic.ScriptError("Invalid grflangid {!r}".format(lang_text), pos)
if value < 0 or value >= 0x7F:
raise generic.ScriptError("Invalid grflangid", pos)
self.langid = value
self.check_expected_plural(None)
def handle_plural(self, data, pos):
"""
Handle a 'plural' pragma.
@param data: Data of the pragma.
@type data: C{str}
"""
if self.plural is not None:
raise generic.ScriptError("plural form already set", pos)
try:
# Explicitly force base 10 like in OpenTTD's lang file
value = int(data[1], 10)
except ValueError:
raise generic.ScriptError("Invalid plural form", pos)
if value < 0 or value > NUM_PLURAL_FORMS:
raise generic.ScriptError("Invalid plural form", pos)
self.plural = value
self.check_expected_plural(pos)
def check_expected_plural(self, pos):
"""
Check whether the provided plural form of the language file
matches with the expected value for that language.
If not, raise an error.
@param pos: Position of the ##plural line, if position is available.
@type pos: L{Position}
"""
if self.plural is None or self.langid is None:
return
expected = LANG_PLURALS.get(self.langid)
if expected is not None and self.plural != expected:
msg = "Language with language id 0x{:02x} has plural form {:d} while the language uses plural form {:d}."
msg = msg.format(self.langid, self.plural, expected)
raise generic.ScriptError(msg, pos)
def handle_gender(self, data, pos):
"""
Handle a 'gender' pragma.
@param data: Data of the pragma.
@type data: C{str}
"""
if self.genders is not None:
raise generic.ScriptError("Genders already defined", pos)
self.genders = {}
for idx, gender in enumerate(data[1].split()):
self.genders[gender] = idx + 1
self.gender_map[gender] = []
def handle_map_gender(self, data, pos):
"""
Handle a 'map_gender' pragma.
@param data: Data of the pragma.
@type data: C{str}
"""
if self.genders is None:
raise generic.ScriptError("##map_gender is not allowed before ##gender", pos)
genders = data[1].split()
if len(genders) != 2:
raise generic.ScriptError("Invalid ##map_gender line", pos)
if genders[0] not in self.genders:
raise generic.ScriptError("Trying to map non-existing gender '{}'".format(genders[0]), pos)
self.gender_map[genders[0]].append(genders[1])
def handle_case(self, data, pos):
"""
Handle a 'case' pragma.
@param data: Data of the pragma.
@type data: C{str}
"""
if self.cases is not None:
raise generic.ScriptError("Cases already defined", pos)
self.cases = {}
for idx, case in enumerate(data[1].split()):
self.cases[case] = idx + 1
self.case_map[case] = []
def handle_map_case(self, data, pos):
"""
Handle a 'map_case' pragma.
@param data: Data of the pragma.
@type data: C{str}
"""
if self.cases is None:
raise generic.ScriptError("##map_case is not allowed before ##case", pos)
cases = data[1].split()
if len(cases) != 2:
raise generic.ScriptError("Invalid ##map_case line", pos)
if cases[0] not in self.cases:
raise generic.ScriptError("Trying to map non-existing case '{}'".format(cases[0]), pos)
self.case_map[cases[0]].append(cases[1])
def handle_text(self, data, pos):
"""
Handle a text string.
@param data: Data of the pragma.
@type data: C{str}
"""
_type, string, case, value = data
if not re.match("[A-Za-z_0-9]+$", string):
raise generic.ScriptError("Invalid string name \"{}\"".format(string), pos)
if string in self.strings and case is None:
raise generic.ScriptError("String name \"{}\" is used multiple times".format(string), pos)
if self.default:
self.strings[string] = NewGRFString(value, self, pos)
self.strings[string].remove_non_default_commands()
else:
if string not in default_lang.strings:
generic.print_warning("String name \"{}\" does not exist in master file".format(string), pos)
return
newgrf_string = NewGRFString(value, self, pos)
if not default_lang.strings[string].match_commands(newgrf_string):
generic.print_warning("String commands don't match with master file \"{}\"".format(DEFAULT_LANGNAME), pos)
return
if case is None:
self.strings[string] = newgrf_string
else:
if string not in self.strings:
generic.print_warning("String with case used before the base string", pos)
return
if self.cases is None or case not in self.cases:
generic.print_warning("Invalid case name \"{}\"".format(case), pos)
return
if case in self.strings[string].cases:
raise generic.ScriptError("String name \"{}.{}\" is used multiple times".format(string, case), pos)
if newgrf_string.gender:
generic.print_warning("Case-strings can't set the gender, only the base string can", pos)
return
self.strings[string].cases[case] = newgrf_string
def scan_line(self, line, pos):
"""
Scan a line of a language file.
@param line: Line to scan.
@type line: C{str}
@param pos: Position information of the line.
@type pos: L{Position}
@return: Contents of the scanned line:
- C{None} Nothing of interest found.
- (, ) A pragma line has been found.
- ('string', , ) A string with optional case has been found.
@rtype: C{None} or a C{tuple}
"""
if len(line) == 0: return None # Silently ignore empty lines.
if line[0] == '#':
if len(line) > 2 and line[1] == '#' and line[2] != '#':
# "##pragma" line.
if self.default: return None # Default language ignores all pragmas.
if line[:12] == "##grflangid ": return ('grflangid', line[12:])
if line[:9] == "##plural ": return ('plural', line[9:])
if line[:9] == "##gender ": return ('gender', line[9:])
if line[:13] == "##map_gender ": return ('map_gender', line[13:])
if line[:11] == "##map_case ": return ('map_case', line[11:])
if line[:7] == "##case ": return ('case', line[7:])
raise generic.ScriptError("Invalid pragma", pos)
return None # Normal comment
# Must be a line defining a string.
i = line.find(':')
if i == -1:
raise generic.ScriptError("Line has no ':' delimiter", pos)
name = line[:i].strip()
value = line[i + 1:]
i = name.find('.') # Find a case.
if i > 0:
case = name[i + 1:]
name = name[:i]
else:
case = None
if self.default and case is not None: return None # Ignore cases for the default language
return ('string', name, case, value)
def handle_string(self, line, pos):
funcs = { 'grflangid' : self.handle_grflangid,
'plural' : self.handle_plural,
'gender' : self.handle_gender,
'map_gender' : self.handle_map_gender,
'map_case' : self.handle_map_case,
'case' : self.handle_case,
'string' : self.handle_text }
res = self.scan_line(line, pos)
if res is not None: funcs[res[0]](res, pos)
default_lang = Language(True)
default_lang.langid = DEFAULT_LANGUAGE
langs = []
def parse_file(filename, default):
"""
Read and parse a single language file.
@param filename: The filename of the file to parse.
@type filename: C{str}
@param default: True iff this is the default language.
@type default: C{bool}
"""
lang = Language(False)
try:
with codecs.open(generic.find_file(filename), "r", "utf-8") as f:
for idx, line in enumerate(f):
pos = generic.LinePosition(filename, idx + 1)
line = line.rstrip('\n\r').lstrip('\uFEFF')
# The default language is processed twice here. Once as fallback langauge
# and once as normal language.
if default: default_lang.handle_string(line, pos)
lang.handle_string(line, pos)
except UnicodeDecodeError:
pos = generic.LanguageFilePosition(filename)
if default:
raise generic.ScriptError("The default language file contains non-utf8 characters.", pos)
generic.print_warning("Language file contains non-utf8 characters. Ignoring (part of) the contents.", pos)
except generic.ScriptError as err:
if default: raise
generic.print_warning(err.value, err.pos)
else:
if lang.langid is None:
generic.print_warning("Language file does not contain a ##grflangid pragma", generic.LanguageFilePosition(filename))
else:
for lng in langs:
if lng[0] == lang.langid:
msg = "Language file has the same ##grflangid (with number {:d}) as another language file".format(lang.langid)
raise generic.ScriptError(msg, generic.LanguageFilePosition(filename))
langs.append((lang.langid, lang))
def read_lang_files(lang_dir, default_lang_file):
"""
Read the language files containing the translations for string constants
used in the NML specification.
@param lang_dir: Name of the directory containing the language files.
@type lang_dir: C{str}
@param default_lang_file: Filename of the language file that has the
default translation which will be used as
fallback for other languages.
@type default_lang_file: C{str}
"""
global DEFAULT_LANGNAME
DEFAULT_LANGNAME = default_lang_file
if not os.path.exists(lang_dir + os.sep + default_lang_file):
generic.print_warning("Default language file \"{}\" doesn't exist".format(os.path.join(lang_dir, default_lang_file)))
return
parse_file(lang_dir + os.sep + default_lang_file, True)
for filename in glob.glob(lang_dir + os.sep + "*.lng"):
if filename.endswith(default_lang_file): continue
parse_file(filename, False)
langs.sort()
nml-0.4.5/nml/expression/ 0000755 0005672 0005672 00000000000 13315644467 016452 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/nml/expression/array.py 0000644 0005672 0005672 00000002424 13315644406 020135 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Expression
class Array(Expression):
def __init__(self, values, pos):
Expression.__init__(self, pos)
self.values = values
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Array of values:')
for v in self.values:
v.debug_print(indentation + 2)
def __str__(self):
return '[' + ', '.join([str(expr) for expr in self.values]) + ']'
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return Array([val.reduce(id_dicts, unknown_id_fatal) for val in self.values], self.pos)
nml-0.4.5/nml/expression/variable.py 0000644 0005672 0005672 00000007636 13315644406 020616 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Type, Expression, ConstantNumeric
class Variable(Expression):
def __init__(self, num, shift = None, mask = None, param = None, pos = None):
Expression.__init__(self, pos)
self.num = num
self.shift = shift if shift is not None else ConstantNumeric(0)
self.mask = mask if mask is not None else ConstantNumeric(0xFFFFFFFF)
self.param = param
self.add = None
self.div = None
self.mod = None
self.extra_params = []
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Action2 variable')
self.num.debug_print(indentation + 2)
if self.param is not None:
generic.print_dbg(indentation + 2, 'Parameter:')
if isinstance(self.param, str):
generic.print_dbg(indentation + 4, 'Procedure call:', self.param)
else:
self.param.debug_print(indentation + 4)
if len(self.extra_params) > 0:
generic.print_dbg(indentation + 2, 'Extra parameters:')
for extra_param in self.extra_params:
extra_param.debug_print(indentation + 4)
def __str__(self):
num = "0x{:02X}".format(self.num.value) if isinstance(self.num, ConstantNumeric) else str(self.num)
ret = 'var[{}, {}, {}'.format(num, self.shift, self.mask)
if self.param is not None:
ret += ', {}'.format(self.param)
ret += ']'
if self.add is not None:
ret = '({} + {})'.format(ret, self.add)
if self.div is not None:
ret = '({} / {})'.format(ret, self.div)
if self.mod is not None:
ret = '({} % {})'.format(ret, self.mod)
return ret
def reduce(self, id_dicts = [], unknown_id_fatal = True):
num = self.num.reduce(id_dicts)
shift = self.shift.reduce(id_dicts)
mask = self.mask.reduce(id_dicts)
param = self.param.reduce(id_dicts) if self.param is not None else None
if num.type() != Type.INTEGER or shift.type() != Type.INTEGER or mask.type() != Type.INTEGER or \
(param is not None and param.type() != Type.INTEGER):
raise generic.ScriptError("All parts of a variable access must be integers.", self.pos)
var = Variable(num, shift, mask, param, self.pos)
var.add = None if self.add is None else self.add.reduce(id_dicts)
var.div = None if self.div is None else self.div.reduce(id_dicts)
var.mod = None if self.mod is None else self.mod.reduce(id_dicts)
var.extra_params = [(extra_param[0], extra_param[1].reduce(id_dicts)) for extra_param in self.extra_params]
return var
def supported_by_action2(self, raise_error):
return True
def supported_by_actionD(self, raise_error):
if raise_error:
if isinstance(self.num, ConstantNumeric):
if self.num.value == 0x7C: raise generic.ScriptError("LOAD_PERM is only available in switch-blocks.", self.pos)
if self.num.value == 0x7D: raise generic.ScriptError("LOAD_TEMP is only available in switch-blocks.", self.pos)
raise generic.ScriptError("Variable accesses are not supported outside of switch-blocks.", self.pos)
return False
nml-0.4.5/nml/expression/patch_variable.py 0000644 0005672 0005672 00000003043 13315644406 021761 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Expression
class PatchVariable(Expression):
"""
Class for reading so-called 'patch variables' via a special ActionD
@ivar num: Variable number to read
@type num: C{int}
"""
def __init__(self, num, pos = None):
Expression.__init__(self, pos)
self.num = num
def debug_print(self, indentation):
generic.print_dbg(indentation, 'PatchVariable:', self.num)
def __str__(self):
return "PatchVariable({:d})".format(self.num)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def supported_by_action2(self, raise_error):
if raise_error:
raise generic.ScriptError("Reading patch variables is not supported in a switch-block.", self.pos)
return False
def supported_by_actionD(self, raise_error):
return True
nml-0.4.5/nml/expression/functioncall.py 0000644 0005672 0005672 00000065552 13315644406 021513 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import datetime, calendar, math
from nml import generic, nmlop
from .base_expression import Type, Expression, ConstantNumeric, ConstantFloat
from .binop import BinOp
from .bitmask import BitMask
from .parameter import parse_string_to_dword
from .storage_op import StorageOp
from .string_literal import StringLiteral
from .ternaryop import TernaryOp
from . import identifier
from functools import reduce
class FunctionCall(Expression):
def __init__(self, name, params, pos):
Expression.__init__(self, pos)
self.name = name
self.params = params
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Call function: ' + self.name.value)
for param in self.params:
generic.print_dbg(indentation + 2, 'Parameter:')
param.debug_print(indentation + 4)
def __str__(self):
ret = '{}({})'.format(self.name, ', '.join(str(param) for param in self.params))
return ret
def reduce(self, id_dicts = [], unknown_id_fatal = True):
# At this point we don't care about invalid arguments, they'll be handled later.
identifier.ignore_all_invalid_ids = True
params = [param.reduce(id_dicts, unknown_id_fatal = False) for param in self.params]
identifier.ignore_all_invalid_ids = False
if self.name.value in function_table:
func = function_table[self.name.value]
val = func(self.name.value, params, self.pos)
return val.reduce(id_dicts)
else:
#try user-defined functions
func_ptr = self.name.reduce(id_dicts, unknown_id_fatal = False, search_func_ptr = True)
if func_ptr != self.name: # we found something!
if func_ptr.type() == Type.SPRITEGROUP_REF:
func_ptr.param_list = params
return func_ptr
if func_ptr.type() != Type.FUNCTION_PTR:
raise generic.ScriptError("'{}' is defined, but it is not a function.".format(self.name.value), self.pos)
return func_ptr.call(params)
if unknown_id_fatal:
raise generic.ScriptError("'{}' is not defined as a function.".format(self.name.value), self.pos)
return FunctionCall(self.name, params, self.pos)
class SpecialCheck(Expression):
"""
Action7/9 special check (e.g. to see whether a cargo is defined)
@ivar op: Action7/9 operator to use
@type op: (C{int}, C{str})-tuple
@ivar varnum: Variable number to read
@type varnum: C{int}
@ivar results: Result of the check when skipping (0) or not skipping (1)
@type results: (C{int}, C{int})-tuple
@ivar value: Value to test
@type value: C{int}
@ivar varsize: Varsize for the action7/9 check
@type varsize: C{int}
@ivar mask: Mask to to test only certain bits of the value
@type mask: C{int}
@ivar pos: Position information
@type pos: L{Position}
"""
def __init__(self, op, varnum, results, value, to_string, varsize = 4, mask = None, pos = None):
Expression.__init__(self, pos)
self.op = op
self.varnum = varnum
self.results = results
self.value = value
self.to_string = to_string
self.varsize = varsize;
self.mask = mask
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def __str__(self):
return self.to_string
def supported_by_actionD(self, raise_error):
return True
class GRMOp(Expression):
def __init__(self, op, feature, count, to_string, pos = None):
Expression.__init__(self, pos)
self.op = op
self.feature = feature
self.count = count
self.to_string = to_string
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def __str__(self):
return self.to_string(self)
def supported_by_actionD(self, raise_error):
return True
#{ Builtin functions
def builtin_min(name, args, pos):
"""
min(...) builtin function.
@return Lowest value of the given arguments.
"""
if len(args) < 2:
raise generic.ScriptError("min() requires at least 2 arguments", pos)
return reduce(lambda x, y: BinOp(nmlop.MIN, x, y, pos), args)
def builtin_max(name, args, pos):
"""
max(...) builtin function.
@return Heighest value of the given arguments.
"""
if len(args) < 2:
raise generic.ScriptError("max() requires at least 2 arguments", pos)
return reduce(lambda x, y: BinOp(nmlop.MAX, x, y, pos), args)
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)
from nml import global_constants
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 = BinOp(nmlop.MUL, year, ConstantNumeric(365))
part2 = BinOp(nmlop.DIV, year, ConstantNumeric(4))
part3 = BinOp(nmlop.DIV, year, ConstantNumeric(100))
part4 = BinOp(nmlop.DIV, year, ConstantNumeric(400))
res = BinOp(nmlop.ADD, part1, part2)
res = BinOp(nmlop.SUB, res, part3)
res = BinOp(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_day_of_year(name, args, pos):
"""
day_of_year(month, day) builtin function.
@return Day of the year, assuming February has 28 days.
"""
if len(args) != 2:
raise generic.ScriptError(name + "() must have a month and a day parameter", pos)
month = args[0].reduce()
if not isinstance(month, ConstantNumeric):
raise generic.ScriptError('Month should be a compile-time constant.', month.pos)
if month.value < 1 or month.value > 12:
raise generic.ScriptError('Month should be a value between 1 and 12.', month.pos)
day = args[1].reduce()
if not isinstance(day, ConstantNumeric):
raise generic.ScriptError('Day should be a compile-time constant.', day.pos)
# Mapping of month to number of days in that month.
number_days = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31}
if day.value < 1 or day.value > number_days[month.value]:
raise generic.ScriptError('Day should be value between 1 and {:d}.'.format(number_days[month.value]), day.pos)
return ConstantNumeric(datetime.date(1, month.value, day.value).toordinal(), pos)
def builtin_storage(name, args, pos):
"""
Accesses to temporary / persistent storage
"""
return StorageOp(name, args, pos)
def builtin_ucmp(name, args, pos):
if len(args) != 2:
raise generic.ScriptError(name + "() must have exactly two parameters", pos)
return BinOp(nmlop.VACT2_UCMP, args[0], args[1], pos)
def builtin_cmp(name, args, pos):
if len(args) != 2:
raise generic.ScriptError(name + "() must have exactly two parameters", pos)
return BinOp(nmlop.VACT2_CMP, args[0], args[1], pos)
def builtin_rotate(name, args, pos):
if len(args) != 2:
raise generic.ScriptError(name + "() must have exactly two parameters", pos)
return BinOp(nmlop.ROT_RIGHT, args[0], args[1], pos)
def builtin_hasbit(name, args, pos):
"""
hasbit(value, bit_num) builtin function.
@return C{1} if and only if C{value} has bit C{bit_num} set, C{0} otherwise.
"""
if len(args) != 2:
raise generic.ScriptError(name + "() must have exactly two parameters", pos)
return BinOp(nmlop.HASBIT, args[0], args[1], pos)
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 = BinOp(nmlop.SHIFTU_RIGHT, args[0], args[1], pos)
part2 = BinOp(nmlop.SHIFT_LEFT, ConstantNumeric(0xFFFFFFFF), args[2], pos)
part3 = BinOp(nmlop.XOR, part2, ConstantNumeric(0xFFFFFFFF), pos)
return BinOp(nmlop.AND, part1, part3, pos)
def builtin_version_openttd(name, args, pos):
"""
version_openttd(major, minor, revision[, build]) builtin function.
@return The version information encoded in a double-word.
"""
if len(args) > 4 or len(args) < 3:
raise generic.ScriptError(name + "() must have 3 or 4 parameters", pos)
major = args[0].reduce_constant().value
minor = args[1].reduce_constant().value
revision = args[2].reduce_constant().value
build = args[3].reduce_constant().value if len(args) == 4 else 0x80000
return ConstantNumeric((major << 28) | (minor << 24) | (revision << 20) | build)
def builtin_cargotype_available(name, args, pos):
"""
cargotype_available(cargo_label) builtin function.
@return 1 if the cargo label is available, 0 otherwise.
"""
if len(args) != 1:
raise generic.ScriptError(name + "() must have exactly 1 parameter", pos)
label = args[0].reduce()
return SpecialCheck((0x0B, r'\7c'), 0, (0, 1), parse_string_to_dword(label), "{}({})".format(name, str(label)), pos = args[0].pos)
def builtin_railtype_available(name, args, pos):
"""
railtype_available(cargo_label) builtin function.
@return 1 if the railtype label is available, 0 otherwise.
"""
if len(args) != 1:
raise generic.ScriptError(name + "() must have exactly 1 parameter", pos)
label = args[0].reduce()
return SpecialCheck((0x0D, None), 0, (0, 1), parse_string_to_dword(label), "{}({})".format(name, str(label)), pos = args[0].pos)
def builtin_grf_status(name, args, pos):
"""
grf_(current_status|future_status|order_behind)(grfid[, mask]) builtin function.
@return 1 if the grf is, or will be, active, 0 otherwise.
"""
if len(args) not in (1, 2):
raise generic.ScriptError(name + "() must have 1 or 2 parameters", pos)
labels = [label.reduce() for label in args]
mask = parse_string_to_dword(labels[1]) if len(labels) > 1 else None
if name == 'grf_current_status':
op = (0x06, r'\7G')
results = (1, 0)
elif name == 'grf_future_status':
op = (0x0A, r'\7gg')
results = (0, 1)
elif name == 'grf_order_behind':
op = (0x08, r'\7gG')
results = (0, 1)
else:
assert False, "Unknown grf status function"
if mask is None:
string = "{}({})".format(name, str(labels))
varsize = 4
else:
string = "{}({}, {})".format(name, str(labels), str(mask))
varsize = 8
return SpecialCheck(op, 0x88, results, parse_string_to_dword(labels[0]), string, varsize, mask, args[0].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)
from nml import global_constants
effect = args[0].reduce_constant(global_constants.const_list).value
offset = BinOp(nmlop.ADD, args[1], ConstantNumeric(8), args[1].pos).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_create_effect(name, args, pos):
"""
Builtin function:
create_effect(effect_sprite, l_x_offset, t_y_offset, z_offset)
Use this to set the values for temporary storages 100+x
in the callback create_effect
"""
if len(args) != 4:
raise generic.ScriptError(name + "() must have 4 parameters", pos)
from nml import global_constants
sprite = args[0].reduce_constant(global_constants.const_list).value
offset1 = args[1].reduce_constant().value
offset2 = args[2].reduce_constant().value
offset3 = args[3].reduce_constant().value
generic.check_range(sprite, 0, 255, "effect_sprite in function " + name, args[0].pos)
generic.check_range(offset1, -128, 127, "l_x_offset in function " + name, args[1].pos)
generic.check_range(offset2, -128, 127, "t_y_offset in function " + name, args[2].pos)
generic.check_range(offset3, -128, 127, "z_offset in function " + name, args[3].pos)
return ConstantNumeric(sprite | (offset1 & 0xFF) << 8 | (offset2 & 0xFF) << 16 | (offset3 & 0xFF) << 24)
def builtin_str2number(name, args, pos):
if len(args) != 1:
raise generic.ScriptError(name + "() must have 1 parameter", pos)
return ConstantNumeric(parse_string_to_dword(args[0]))
def builtin_cargotype(name, args, pos):
if len(args) != 1:
raise generic.ScriptError(name + "() must have 1 parameter", pos)
from nml import global_constants
if not isinstance(args[0], StringLiteral) or args[0].value not in global_constants.cargo_numbers:
raise generic.ScriptError("Parameter for " + name + "() must be a string literal that is also in your cargo table", pos)
return ConstantNumeric(global_constants.cargo_numbers[args[0].value])
def builtin_railtype(name, args, pos):
if len(args) != 1:
raise generic.ScriptError(name + "() must have 1 parameter", pos)
from nml import global_constants
if not isinstance(args[0], StringLiteral) or args[0].value not in global_constants.railtype_table:
raise generic.ScriptError("Parameter for " + name + "() must be a string literal that is also in your railtype table", pos)
return ConstantNumeric(global_constants.railtype_table[args[0].value])
def builtin_reserve_sprites(name, args, pos):
if len(args) != 1:
raise generic.ScriptError(name + "() must have 1 parameter", pos)
count = args[0].reduce_constant()
func = lambda x: '{}({:d})'.format(name, count.value)
return GRMOp(nmlop.GRM_RESERVE, 0x08, count.value, func, pos)
def builtin_industry_type(name, args, pos):
"""
industry_type(IND_TYPE_OLD | IND_TYPE_NEW, id) builtin function
@return The industry type in the format used by grfs (industry prop 0x16 and var 0x64)
"""
if len(args) != 2:
raise generic.ScriptError(name + "() must have 2 parameters", pos)
from nml import global_constants
type = args[0].reduce_constant(global_constants.const_list).value
if type not in (0, 1):
raise generic.ScriptError("First argument of industry_type() must be IND_TYPE_OLD or IND_TYPE_NEW", pos)
# Industry ID uses 7 bits (0 .. 6), bit 7 is for old/new
id = args[1].reduce_constant(global_constants.const_list).value
if not 0 <= id <= 127:
raise generic.ScriptError("Second argument 'id' of industry_type() must be in range 0..127", pos)
return ConstantNumeric(type << 7 | id)
def builtin_trigonometric(name, args, pos):
if len(args) != 1:
raise generic.ScriptError(name + "() must have 1 parameter", pos)
val = args[0].reduce()
if not isinstance(val, (ConstantNumeric, ConstantFloat)):
raise generic.ScriptError("Parameter for " + name + "() must be a constant", pos)
trigonometric_func_table = {
'acos': math.acos,
'asin': math.asin,
'atan': math.atan,
'cos': math.cos,
'sin': math.sin,
'tan': math.tan,
}
return ConstantFloat(trigonometric_func_table[name](val.value), val.pos)
def builtin_int(name, args, pos):
if len(args) != 1:
raise generic.ScriptError(name + "() must have 1 parameter", pos)
val = args[0].reduce()
if not isinstance(val, (ConstantNumeric, ConstantFloat)):
raise generic.ScriptError("Parameter for " + name + "() must be a constant", pos)
return ConstantNumeric(int(val.value), val.pos)
def builtin_abs(name, args, pos):
if len(args) != 1:
raise generic.ScriptError(name + "() must have 1 parameter", pos)
guard = BinOp(nmlop.CMP_LT, args[0], ConstantNumeric(0), args[0].pos)
return TernaryOp(guard, BinOp(nmlop.SUB, ConstantNumeric(0), args[0], args[0].pos), args[0], args[0].pos).reduce()
def builtin_sound_file(name, args, pos):
from nml.actions import action11
if len(args) not in (1, 2):
raise generic.ScriptError(name + "() must have 1 or 2 parameters", pos)
if not isinstance(args[0], StringLiteral):
raise generic.ScriptError("Parameter for " + name + "() must be a string literal", pos)
volume = args[1].reduce_constant().value if len(args) >= 2 else 100
generic.check_range(volume, 0, 100, "sound volume", pos)
return ConstantNumeric(action11.add_sound( (args[0].value, volume), pos), pos)
def builtin_sound_import(name, args, pos):
from nml.actions import action11
if len(args) not in (2, 3):
raise generic.ScriptError(name + "() must have 2 or 3 parameters", pos)
grfid = parse_string_to_dword(args[0].reduce())
sound_num = args[1].reduce_constant().value
volume = args[2].reduce_constant().value if len(args) >= 3 else 100
generic.check_range(volume, 0, 100, "sound volume", pos)
return ConstantNumeric(action11.add_sound( (grfid, sound_num, volume), pos), pos)
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 = BinOp(nmlop.AND, args[0], ConstantNumeric(0xFF), args[0].pos)
y_coord = BinOp(nmlop.AND, args[1], ConstantNumeric(0xFF), args[1].pos)
# Shift Y to its position.
y_coord = BinOp(nmlop.SHIFT_LEFT, y_coord, ConstantNumeric(8), y_coord.pos)
return BinOp(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 = BinOp(nmlop.AND, slope, ConstantNumeric(0x1F), pos)
slope = BinOp(nmlop.MUL, slope, ConstantNumeric(0x8421), pos)
slope = BinOp(nmlop.AND, slope, ConstantNumeric(0x11111), pos)
return BinOp(nmlop.MOD, slope, ConstantNumeric(0xF), pos)
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 = BinOp(nmlop.AND, BinOp(nmlop.CMP_LE, args[0], ConstantNumeric(14), pos),
BinOp(nmlop.CMP_GE, args[0], ConstantNumeric(0), pos), pos)
expr = BinOp(nmlop.MUL, expr, args[0], pos)
# 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 = BinOp(nmlop.MUL, BinOp(nmlop.CMP_EQ, args[0], ConstantNumeric(slope), pos), ConstantNumeric(offset), pos)
expr = BinOp(nmlop.ADD, expr, to_add, pos)
return expr
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 BinOp(nmlop.ADD, args[0], ConstantNumeric(775), pos)
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 = BinOp(nmlop.MUL, args[1], ConstantNumeric(16), pos)
col12 = BinOp(nmlop.ADD, col2, args[0], pos)
# Base sprite is not a constant
from nml import global_constants
base = global_constants.patch_variable(global_constants.patch_variables['base_sprite_2cc'], pos)
return BinOp(nmlop.ADD, col12, base, pos)
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 = [BinOp(nmlop.AND, arg, ConstantNumeric(0xF), pos) for arg in args]
cur_next = BinOp(nmlop.SHIFT_LEFT, args[1], ConstantNumeric(8), pos)
return BinOp(nmlop.OR, args[0], cur_next, pos)
def builtin_format_string(name, args, pos):
"""
format_string(format, ... args ..) builtin function
@return Formatted string
"""
if len(args) < 1:
raise generic.ScriptError(name + "() must have at least one parameter", pos)
format = args[0].reduce()
if not isinstance(format, StringLiteral):
raise generic.ScriptError(name + "() parameter 1 'format' must be a literal string", format.pos)
# Validate other args
format_args = []
for i, arg in enumerate(args[1:]):
arg = arg.reduce()
if not isinstance(arg, (StringLiteral, ConstantFloat, ConstantNumeric)):
raise generic.ScriptError(name + "() parameter {:d} is not a constant number of literal string".format(i+1), arg.pos)
format_args.append(arg.value)
try:
result = format.value % tuple(format_args)
return StringLiteral(result, pos)
except Exception as ex:
raise generic.ScriptError("Invalid combination of format / arguments for {}: {}".format(name, str(ex)), pos)
#}
function_table = {
'min' : builtin_min,
'max' : builtin_max,
'date' : builtin_date,
'day_of_year' : builtin_day_of_year,
'bitmask' : lambda name, args, pos: BitMask(args, pos),
'STORE_TEMP' : builtin_storage,
'STORE_PERM' : builtin_storage,
'LOAD_TEMP' : builtin_storage,
'LOAD_PERM' : builtin_storage,
'hasbit' : builtin_hasbit,
'getbits' : builtin_getbits,
'version_openttd' : builtin_version_openttd,
'cargotype_available' : builtin_cargotype_available,
'railtype_available' : builtin_railtype_available,
'grf_current_status' : builtin_grf_status,
'grf_future_status' : builtin_grf_status,
'grf_order_behind' : builtin_grf_status,
'visual_effect' : builtin_visual_effect_and_powered,
'visual_effect_and_powered' : builtin_visual_effect_and_powered,
'create_effect' : builtin_create_effect,
'str2number' : builtin_str2number,
'cargotype' : builtin_cargotype,
'railtype' : builtin_railtype,
'reserve_sprites' : builtin_reserve_sprites,
'industry_type' : builtin_industry_type,
'int' : builtin_int,
'abs' : builtin_abs,
'acos' : builtin_trigonometric,
'asin' : builtin_trigonometric,
'atan' : builtin_trigonometric,
'cos' : builtin_trigonometric,
'sin' : builtin_trigonometric,
'tan' : builtin_trigonometric,
'UCMP' : builtin_ucmp,
'CMP' : builtin_cmp,
'rotate' : builtin_rotate,
'sound' : builtin_sound_file,
'import_sound': builtin_sound_import,
'relative_coord' : builtin_relative_coord,
'num_corners_raised' : builtin_num_corners_raised,
'slope_to_sprite_offset' : builtin_slope_to_sprite_offset,
'palette_1cc' : builtin_palette_1cc,
'palette_2cc' : builtin_palette_2cc,
'vehicle_curv_info' : builtin_vehicle_curv_info,
'format_string' : builtin_format_string,
}
nml-0.4.5/nml/expression/identifier.py 0000644 0005672 0005672 00000005166 13315644406 021147 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Expression, ConstantNumeric
from .string_literal import StringLiteral
ignore_all_invalid_ids = False
def default_id_func(x, pos):
"""
Default id conversion function.
@param x: Value to convert.
@type x: C{str}, C{int}, or C{float}
@param pos: Position of the id.
@type pos: L{Position}
@return: Expression of the id.
@rtype: L{Expression}
"""
if isinstance(x, str):
return StringLiteral(x, pos)
else:
return ConstantNumeric(x, pos)
class Identifier(Expression):
def __init__(self, value, pos = None):
Expression.__init__(self, pos)
self.value = value
def debug_print(self, indentation):
generic.print_dbg(indentation, 'ID:', self.value)
def __str__(self):
return self.value
def reduce(self, id_dicts = [], unknown_id_fatal = True, search_func_ptr = False):
for id_dict in id_dicts:
if isinstance(id_dict, tuple):
id_d, func = id_dict
else:
id_d, func = id_dict, default_id_func
if self.value in id_d:
if search_func_ptr:
# Do not reduce function pointers, since they have no (numerical) value
return func(id_d[self.value], self.pos)
else:
return func(id_d[self.value], self.pos).reduce(id_dicts)
if unknown_id_fatal and not ignore_all_invalid_ids:
raise generic.ScriptError("Unrecognized identifier '" + self.value + "' encountered", self.pos)
return self
def supported_by_actionD(self, raise_error):
if raise_error:
raise generic.ScriptError("Unknown identifier '{}'".format(self.value), self.pos)
return False
def __eq__(self, other):
return other is not None and isinstance(other, Identifier) and self.value == other.value
def __hash__(self):
return hash(self.value)
nml-0.4.5/nml/expression/boolean.py 0000644 0005672 0005672 00000003433 13315644406 020437 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Type, Expression
class Boolean(Expression):
"""
Convert to boolean truth value.
@ivar expr: (Integer) expression to convert.
@type expr: C{Expression}
"""
def __init__(self, expr, pos = None):
Expression.__init__(self, pos)
self.expr = expr
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Force expression to boolean:')
self.expr.debug_print(indentation + 2)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
expr = self.expr.reduce(id_dicts)
if expr.type() != Type.INTEGER:
raise generic.ScriptError("Only integers can be converted to a boolean value.", self.pos)
if expr.is_boolean(): return expr
return Boolean(expr)
def supported_by_action2(self, raise_error):
return self.expr.supported_by_action2(raise_error)
def supported_by_actionD(self, raise_error):
return self.expr.supported_by_actionD(raise_error)
def is_boolean(self):
return True
def __str__(self):
return "!!({})".format(self.expr)
nml-0.4.5/nml/expression/storage_op.py 0000644 0005672 0005672 00000011046 13315644406 021161 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import ConstantNumeric, Expression, Type
from .parameter import parse_string_to_dword
storage_op_info = {
'STORE_PERM' : {'store': True, 'perm': True, 'grfid': False, 'max': 0x0F},
'STORE_TEMP' : {'store': True, 'perm': False, 'grfid': False, 'max': 0x10F},
'LOAD_PERM' : {'store': False, 'perm': True, 'grfid': True, 'max': 0x0F},
'LOAD_TEMP' : {'store': False, 'perm': False, 'grfid': False, 'max': 0xFF},
}
class StorageOp(Expression):
"""
Class for reading/writing to (temporary or permanent) storage
@ivar name: Name of the called storage function
@type name: C{str}
@ivar info: Dictionary containting information about the operation to perform
@type info: C{dict}
@ivar value: Value to store, or C{None} for loading operations
@type value: L{Expression}
@ivar register: Register to access
@type register: L{Expression}
@ivar grfid: GRFID of the register to access
@type grfid: L{Expression}
"""
def __init__(self, name, args, pos = None):
Expression.__init__(self, pos)
self.name = name
assert name in storage_op_info
self.info = storage_op_info[name]
arg_len = (2,) if self.info['store'] else (1,)
if self.info['grfid']: arg_len += (arg_len[0] + 1,)
if len(args) not in arg_len:
argstr = "{:d}".format(arg_len[0]) if len(arg_len) == 1 else "{}..{}".format(arg_len[0], arg_len[1])
raise generic.ScriptError("{} requires {} argument(s), encountered {:d}".format(name, argstr, len(args)), pos)
i = 0
if self.info['store']:
self.value = args[i]
i += 1
else:
self.value = None
self.register = args[i]
i += 1
if i < len(args):
self.grfid = args[i]
assert self.info['grfid']
else:
self.grfid = None
def debug_print(self, indentation):
generic.print_dbg(indentation, self.name)
generic.print_dbg(indentation + 2, 'Register:')
self.register.debug_print(indentation + 4)
if self.value is not None:
generic.print_dbg(indentation + 2, 'Value:')
self.value.debug_print(indentation + 4)
if self.grfid is not None:
generic.print_dbg(indentation + 2, 'GRFID:')
self.grfid.debug_print(indentation + 4)
def __str__(self):
args = []
if self.value is not None: args.append(str(self.value))
args.append(str(self.register))
if self.grfid is not None: args.append(str(self.grfid))
return "{}({})".format(self.name, ", ".join(args))
def reduce(self, id_dicts = [], unknown_id_fatal = True):
args = []
if self.value is not None:
value = self.value.reduce(id_dicts)
if value.type() != Type.INTEGER:
raise generic.ScriptError("Value to store must be an integer.", value.pos)
args.append(value)
register = self.register.reduce(id_dicts)
if register.type() != Type.INTEGER:
raise generic.ScriptError("Register to access must be an integer.", register.pos)
if isinstance(register, ConstantNumeric) and register.value > self.info['max']:
raise generic.ScriptError("Maximum register for {} is {:d}".format(self.name, self.info['max']), self.pos)
args.append(register)
if self.grfid is not None:
grfid = self.grfid.reduce(id_dicts)
# Test validity
parse_string_to_dword(grfid)
args.append(grfid)
return StorageOp(self.name, args, self.pos)
def supported_by_action2(self, raise_error):
return True
def supported_by_actionD(self, raise_error):
if raise_error:
raise generic.ScriptError("{}() may only be used inside switch-blocks".format(self.name), self.pos)
return False
nml-0.4.5/nml/expression/ternaryop.py 0000644 0005672 0005672 00000005604 13315644406 021045 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Type, Expression, ConstantNumeric
class TernaryOp(Expression):
def __init__(self, guard, expr1, expr2, pos):
Expression.__init__(self, pos)
self.guard = guard
self.expr1 = expr1
self.expr2 = expr2
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Ternary operator')
generic.print_dbg(indentation, 'Guard:')
self.guard.debug_print(indentation + 2)
generic.print_dbg(indentation, 'Expression 1:')
self.expr1.debug_print(indentation + 2)
generic.print_dbg(indentation, 'Expression 2:')
self.expr2.debug_print(indentation + 2)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
guard = self.guard.reduce(id_dicts)
expr1 = self.expr1.reduce(id_dicts)
expr2 = self.expr2.reduce(id_dicts)
if isinstance(guard, ConstantNumeric):
if guard.value != 0:
return expr1
else:
return expr2
if guard.type() != Type.INTEGER or expr1.type() != Type.INTEGER or expr2.type() != Type.INTEGER:
raise generic.ScriptError("All parts of the ternary operator (?:) must be integers.", self.pos)
return TernaryOp(guard, expr1, expr2, self.pos)
def supported_by_action2(self, raise_error):
return self.guard.supported_by_action2(raise_error) and self.expr1.supported_by_action2(raise_error) and self.expr2.supported_by_action2(raise_error)
def supported_by_actionD(self, raise_error):
return self.guard.supported_by_actionD(raise_error) and self.expr1.supported_by_actionD(raise_error) and self.expr2.supported_by_actionD(raise_error)
def is_boolean(self):
return self.expr1.is_boolean() and self.expr2.is_boolean()
def __eq__(self, other):
return other is not None and isinstance(other, TernaryOp) and self.guard == other.guard and self.expr1 == other.expr1 and self.expr2 == other.expr2
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash((self.guard, self.expr1, self.expr2))
def __str__(self):
return "({} ? {} : {})".format(str(self.guard), str(self.expr1), str(self.expr2))
nml-0.4.5/nml/expression/special_parameter.py 0000644 0005672 0005672 00000006265 13315644406 022506 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Expression
class SpecialParameter(Expression):
"""
Class for handling special grf parameters.
These can be assigned special, custom methods for reading / writing to them.
@ivar name: Name of the parameter, for debugging purposes.
@type name: C{str}
@ivar info: Information about the parameter.
@type info: C{dict}
@ivar write_func: Function that will be called when the parameter is the target of an assignment
Arguments:
Dictionary with parameter information (self.info)
Target expression to assign
Position information
Return value is a 2-tuple:
Left side of the assignment (must be a parameter)
Right side of the assignment (may be any expression)
@type write_func: C{function}
@ivar read_func: Function that will be called to read out the parameter value
Arguments:
Dictionary with parameter information (self.info)
Position information
Return value:
Expression that should be evaluated to get the parameter value
@type read_func: C{function}
@ivar is_bool: Does read_func return a boolean value?
@type is_bool: C{bool}
"""
def __init__(self, name, info, write_func, read_func, is_bool, pos = None):
Expression.__init__(self, pos)
self.name = name
self.info = info
self.write_func = write_func
self.read_func = read_func
self.is_bool = is_bool
def debug_print(self, indentation):
generic.print_dbg(indentation, "Special parameter '{}'".format(self.name))
def __str__(self):
return self.name
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def is_boolean(self):
return self.is_bool
def can_assign(self):
return self.write_func is not None
def to_assignment(self, expr):
param, expr = self.write_func(self.info, expr, self.pos)
param = param.reduce()
expr = expr.reduce()
return (param, expr)
def to_reading(self):
param = self.read_func(self.info, self.pos)
param = param.reduce()
return param
def supported_by_actionD(self, raise_error):
return True
def supported_by_action2(self, raise_error):
return True
nml-0.4.5/nml/expression/__init__.py 0000644 0005672 0005672 00000003443 13315644406 020560 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import re
from .array import Array
from .base_expression import Type, Expression, ConstantNumeric, ConstantFloat
from .bin_not import BinNot, Not
from .binop import BinOp
from .bitmask import BitMask
from .boolean import Boolean
from .functioncall import FunctionCall, SpecialCheck, GRMOp
from .functionptr import FunctionPtr
from .identifier import Identifier
from .patch_variable import PatchVariable
from .parameter import Parameter, OtherGRFParameter, parse_string_to_dword
from .special_parameter import SpecialParameter
from .spritegroup_ref import SpriteGroupRef
from .storage_op import StorageOp
from .string import String
from .string_literal import StringLiteral
from .ternaryop import TernaryOp
from .variable import Variable
is_valid_id = re.compile('[a-zA-Z_][a-zA-Z0-9_]{3}$')
def identifier_to_print(name):
"""
Check whether the given name is a valid 4 letter identifier to print (for cargoes and railtypes).
@param name: Name to check.
@return The identifier itself, if it is a valid name, else a string literal text with the name.
"""
if is_valid_id.match(name): return name
return '"{}"'.format(name)
nml-0.4.5/nml/expression/functionptr.py 0000644 0005672 0005672 00000004270 13315644406 021373 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Type, Expression
class FunctionPtr(Expression):
"""
Pointer to a function.
If this appears inside an expression, the user has made an error.
@ivar name: Identifier that has been resolved to this function pointer.
@type name: L{Identifier}
@ivar func: Function that will be called to resolve this function call. Arguments:
Name of the function (C{str})
List of passed arguments (C{list} of L{Expression})
Position information (L{Position})
Any extra arguments passed to the constructor of this class
@type func: C{function}
@ivar extra_args List of arguments that should be passed to the function that is to be called.
@type extra_args C{list}
"""
def __init__(self, name, func, *extra_args):
self.name = name
self.func = func
self.extra_args = extra_args
def debug_print(self, indentation):
assert False, "Function pointers should not appear inside expressions."
def __str__(self):
assert False, "Function pointers should not appear inside expressions."
def reduce(self, id_dicts = [], unknown_id_fatal = True):
raise generic.ScriptError("'{}' is a function and should be called using the function call syntax.".format(str(self.name)), self.name.pos)
def type(self):
return Type.FUNCTION_PTR
def call(self, args):
return self.func(self.name.value, args, self.name.pos, *self.extra_args)
nml-0.4.5/nml/expression/spritegroup_ref.py 0000644 0005672 0005672 00000005715 13315644406 022244 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from nml.actions import action2
from .base_expression import Type, Expression
class SpriteGroupRef(Expression):
"""
Container for a reference to a sprite group / layout
@ivar name: Name of the referenced item
@type name: L{Identifier}
@ivar param_list: List of parameters to be passed
@type param_list: C{list} of L{Expression}
@ivar pos: Position of this reference
@type pos: L{Position}
@ivar act2: Action2 that is the target of this reference
To be used for action2s that have no direct equivalent in the AST
@type act2: L{Action2}
"""
def __init__(self, name, param_list, pos, act2 = None):
self.name = name
self.param_list = param_list
self.pos = pos
self.act2 = act2
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Reference to:', self.name)
if len(self.param_list) != 0:
generic.print_dbg(indentation, 'Parameters:')
for p in self.param_list:
p.debug_print(indentation + 2)
def __str__(self):
if self.param_list:
return '{}({})'.format(self.name, ', '.join(str(x) for x in self.param_list))
return str(self.name)
def get_action2_id(self, feature):
"""
Get the action2 set-ID that this reference maps to
@param feature: Feature of the action2
@type feature: C{int}
@return: The set ID
@rtype: C{int}
"""
if self.act2 is not None: return self.act2.id
if self.name.value == 'CB_FAILED': return 0 # 0 serves as a failed CB result because it is never used
try:
spritegroup = action2.resolve_spritegroup(self.name)
except generic.ScriptError:
raise AssertionError("Illegal action2 reference '{}' encountered.".format(self.name.value))
return spritegroup.get_action2(feature).id
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def type(self):
return Type.SPRITEGROUP_REF
def __eq__(self, other):
return other is not None and isinstance(other, SpriteGroupRef) and other.name == self.name and other.param_list == self.param_list
def __hash__(self):
return hash(self.name) ^ hash(tuple(self.param_list))
nml-0.4.5/nml/expression/string_literal.py 0000644 0005672 0005672 00000002705 13315644406 022043 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
from .base_expression import Type, Expression
class StringLiteral(Expression):
"""
String literal expression.
@ivar value: Value of the string literal.
@type value: C{str}
"""
def __init__(self, value, pos):
Expression.__init__(self, pos)
self.value = value
def debug_print(self, indentation):
generic.print_dbg(indentation, 'String literal: "{}"'.format(self.value))
def __str__(self):
return '"{}"'.format(self.value)
def write(self, file, size):
assert len(self.value) == size
file.print_string(self.value, final_zero = False, force_ascii = True)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def type(self):
return Type.STRING_LITERAL
nml-0.4.5/nml/expression/binop.py 0000644 0005672 0005672 00000023725 13315644406 020135 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, nmlop
from .base_expression import Expression, ConstantNumeric, ConstantFloat
from .string_literal import StringLiteral
from .variable import Variable
from .boolean import Boolean
class BinOp(Expression):
def __init__(self, op, expr1, expr2, pos = None):
Expression.__init__(self, pos)
self.op = op
self.expr1 = expr1
self.expr2 = expr2
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Binary operator, op =', self.op.token)
self.expr1.debug_print(indentation + 2)
self.expr2.debug_print(indentation + 2)
def __str__(self):
if self.op == nmlop.SUB and isinstance(self.expr1, ConstantNumeric) and self.expr1.value == 0:
return '-' + str(self.expr2)
return self.op.to_string(self.expr1, self.expr2)
def get_priority(self, expr):
"""
Get the priority of an expression. For optimalization reason we prefer complexer
expressions (= low priority) on the left hand side of this expression. The following
priorities are used:
-1: everything that doesn't fit in one of the other categories.
0: Variables to be parsed in a varaction2
1: Expressions that can be parsed via actionD, with the exception of constant numbers
2: constant numbers
@param expr: The expression to get the priority of.
@type expr: L{Expression}
@return: The priority for the given expression.
@rtype: C{int}
"""
if isinstance(expr, Variable): return 0
if isinstance(expr, ConstantNumeric): return 2
if expr.supported_by_actionD(False): return 1
return -1
def reduce(self, id_dicts = [], 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 in commutative_operators or self.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 = BinOp(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 = BinOp(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 = BinOp(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 = BinOp(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 BinOp(nmlop.ADD, expr1.expr1, ConstantNumeric(expr1.expr2.value + val), self.pos).reduce()
if expr1.op == nmlop.SUB:
return BinOp(nmlop.SUB, expr1.expr1, ConstantNumeric(expr1.expr2.value - val), self.pos).reduce()
if op == nmlop.OR and isinstance(expr1, Boolean) and isinstance(expr2, Boolean):
return Boolean(BinOp(op, expr1.expr, expr2.expr, self.pos)).reduce(id_dicts)
return BinOp(op, expr1, expr2, self.pos)
def supported_by_action2(self, raise_error):
if not self.op.act2_supports:
token = " '{}'".format(self.op.token) if self.op.token else ""
if raise_error: raise generic.ScriptError("Operator{} not supported in a switch-block".format(token), self.pos)
return False
return self.expr1.supported_by_action2(raise_error) and self.expr2.supported_by_action2(raise_error)
def supported_by_actionD(self, raise_error):
if not self.op.actd_supports:
if raise_error:
if self.op == nmlop.STO_PERM: raise generic.ScriptError("STORE_PERM is only available in switch-blocks.", self.pos)
elif self.op == nmlop.STO_TMP: raise generic.ScriptError("STORE_TEMP is only available in switch-blocks.", self.pos)
#default case
token = " '{}'".format(self.op.token) if self.op.token else ""
raise generic.ScriptError("Operator{} not supported in parameter assignment".format(token), self.pos)
return False
return self.expr1.supported_by_actionD(raise_error) and self.expr2.supported_by_actionD(raise_error)
def is_boolean(self):
if self.op in (nmlop.AND, nmlop.OR, nmlop.XOR):
return self.expr1.is_boolean() and self.expr2.is_boolean()
return self.op.returns_boolean
def __eq__(self, other):
return other is not None and isinstance(other, BinOp) and self.op == other.op and self.expr1 == other.expr1 and self.expr2 == other.expr2
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash((self.op, self.expr1, self.expr2))
commutative_operators = set([
nmlop.ADD,
nmlop.MUL,
nmlop.AND,
nmlop.OR,
nmlop.XOR,
nmlop.CMP_EQ,
nmlop.CMP_NEQ,
nmlop.MIN,
nmlop.MAX,
])
nml-0.4.5/nml/expression/base_expression.py 0000644 0005672 0005672 00000012631 13315644406 022211 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
class Type(object):
"""
Enum-type class of the various value types possible in NML
"""
INTEGER = 0
FLOAT = 1
STRING_LITERAL = 2
FUNCTION_PTR = 3
SPRITEGROUP_REF = 4
class Expression(object):
"""
Superclass for all expression classes.
@ivar pos: Position of the data in the original file.
@type pos: :L{Position}
"""
def __init__(self, pos):
self.pos = pos
def debug_print(self, indentation):
"""
Print all data with explanation of what it is to standard output.
@param indentation: Indent all printed lines with at least
C{indentation} spaces.
"""
raise NotImplementedError('debug_print must be implemented in expression-subclass {!r}'.format(type(self)))
def __str__(self):
"""
Convert this expression to a string representing this expression in valid NML-code.
@return: A string representation of this expression.
"""
raise NotImplementedError('__str__ must be implemented in expression-subclass {!r}'.format(type(self)))
def reduce(self, id_dicts = [], unknown_id_fatal = True):
"""
Reduce this expression to the simplest representation possible.
@param id_dicts: A list with dicts that are used to map identifiers
to another (often numeric) representation.
@param unknown_id_fatal: Is encountering an unknown identifier somewhere
in this expression a fatal error?
@return: A deep copy of this expression simplified as much as possible.
"""
raise NotImplementedError('reduce must be implemented in expression-subclass {!r}'.format(type(self)))
def reduce_constant(self, id_dicts = []):
"""
Reduce this expression and make sure the result is a constant number.
@param id_dicts: A list with dicts that are used to map identifiers
to another (often numeric) representation.
@return: A constant number that is the result of this expression.
"""
expr = self.reduce(id_dicts)
if not isinstance(expr, ConstantNumeric):
raise generic.ConstError(self.pos)
return expr
def supported_by_action2(self, raise_error):
"""
Check if this expression can be used inside a switch-block.
@param raise_error: If true raise a scripterror instead of returning false.
@return: True if this expression can be calculated by advanced varaction2.
"""
if raise_error: raise generic.ScriptError("This expression is not supported in a switch-block", self.pos)
return False
def supported_by_actionD(self, raise_error):
"""
Check if this expression can be used inside a parameter-assignment.
@param raise_error: If true raise a scripterror instead of returning false.
@return: True if this expression can be calculated by actionD.
"""
if raise_error: raise generic.ScriptError("This expression can not be assigned to a parameter", self.pos)
return False
def is_boolean(self):
"""
Check if this expression is limited to 0 or 1 as value.
@return: True if the value of this expression is either 0 or 1.
"""
return False
def type(self):
"""
Determine the datatype of this expression.
@return: A constant from the L{Type} class, representing the data type.
"""
return Type.INTEGER
class ConstantNumeric(Expression):
def __init__(self, value, pos = None):
Expression.__init__(self, pos)
self.value = generic.truncate_int32(value)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Int:', self.value)
def write(self, file, size):
file.print_varx(self.value, size)
def __str__(self):
return str(self.value)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def supported_by_action2(self, raise_error):
return True
def supported_by_actionD(self, raise_error):
return True
def is_boolean(self):
return self.value == 0 or self.value == 1
def __eq__(self, other):
return other is not None and isinstance(other, ConstantNumeric) and other.value == self.value
def __hash__(self):
return self.value
class ConstantFloat(Expression):
def __init__(self, value, pos):
Expression.__init__(self, pos)
self.value = float(value)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Float:', self.value)
def __str__(self):
return str(self.value)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
return self
def type(self):
return Type.FLOAT
nml-0.4.5/nml/expression/bitmask.py 0000644 0005672 0005672 00000003520 13315644406 020447 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, nmlop
from .base_expression import Type, Expression, ConstantNumeric
from .binop import BinOp
class BitMask(Expression):
def __init__(self, values, pos):
Expression.__init__(self, pos)
self.values = values
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Get bitmask:')
for value in self.values:
value.debug_print(indentation + 2)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
ret = ConstantNumeric(0, self.pos)
for orig_expr in self.values:
val = orig_expr.reduce(id_dicts)
if val.type() != Type.INTEGER:
raise generic.ScriptError("Parameters of 'bitmask' must be integers.", orig_expr.pos)
if isinstance(val, ConstantNumeric) and val.value >= 32:
raise generic.ScriptError("Parameters of 'bitmask' cannot be greater than 31", orig_expr.pos)
val = BinOp(nmlop.SHIFT_LEFT, ConstantNumeric(1), val, val.pos)
ret = BinOp(nmlop.OR, ret, val, self.pos)
return ret.reduce()
def __str__(self):
return "bitmask(" + ", ".join(str(e) for e in self.values) + ")"
nml-0.4.5/nml/expression/string.py 0000644 0005672 0005672 00000004104 13315644406 020322 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from .base_expression import Expression
from .identifier import Identifier
from nml import generic
from functools import reduce
class String(Expression):
def __init__(self, params, pos):
Expression.__init__(self, pos)
if len(params) == 0:
raise generic.ScriptError("string() requires at least one parameter.", pos)
self.name = params[0]
if not isinstance(self.name, Identifier):
raise generic.ScriptError("First parameter of string() must be an identifier.", pos)
self.params = params[1:]
def debug_print(self, indentation):
generic.print_dbg(indentation, 'String:')
self.name.debug_print(indentation + 2)
for param in self.params:
generic.print_dbg(indentation + 2, 'Parameter:')
param.debug_print(indentation + 4)
def __str__(self):
ret = 'string(' + self.name.value
for p in self.params:
ret += ', ' + str(p)
ret += ')'
return ret
def reduce(self, id_dicts = [], unknown_id_fatal = True):
params = [p.reduce(id_dicts) for p in self.params]
return String([self.name] + params, self.pos)
def __eq__(self, other):
return other is not None and isinstance(other, String) and self.name == other.name and self.params == other.params
def __hash__(self):
return hash(self.name) ^ reduce(lambda x, y: x ^ hash(y), self.params, 0)
nml-0.4.5/nml/expression/parameter.py 0000644 0005672 0005672 00000010522 13315644406 020775 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, grfstrings
from .base_expression import Type, Expression, ConstantNumeric
from .string_literal import StringLiteral
class Parameter(Expression):
def __init__(self, num, pos = None, by_user = False):
Expression.__init__(self, pos)
self.num = num
if by_user and isinstance(num, ConstantNumeric) and not (0 <= num.value <= 63):
generic.print_warning("Accessing parameters out of the range 0..63 is not supported and may lead to unexpected behaviour.", pos)
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Parameter:')
self.num.debug_print(indentation + 2)
def __str__(self):
return 'param[{}]'.format(self.num)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
num = self.num.reduce(id_dicts)
if num.type() != Type.INTEGER:
raise generic.ScriptError("Parameter number must be an integer.", num.pos)
return Parameter(num, self.pos)
def supported_by_action2(self, raise_error):
supported = isinstance(self.num, ConstantNumeric)
if not supported and raise_error:
raise generic.ScriptError("Parameter acessess with non-constant numbers are not supported in a switch-block.", self.pos)
return supported
def supported_by_actionD(self, raise_error):
return True
def __eq__(self, other):
return other is not None and isinstance(other, Parameter) and self.num == other.num
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash((self.num,))
class OtherGRFParameter(Expression):
def __init__(self, grfid, num, pos = None):
Expression.__init__(self, pos)
self.grfid = grfid
self.num = num
def debug_print(self, indentation):
generic.print_dbg(indentation, 'OtherGRFParameter:')
self.grfid.debug_print(indentation + 2)
self.num.debug_print(indentation + 2)
def __str__(self):
return 'param[{}, {}]'.format(self.grfid, self.num)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
grfid = self.grfid.reduce(id_dicts)
#Test validity
parse_string_to_dword(grfid)
num = self.num.reduce(id_dicts)
if num.type() != Type.INTEGER:
raise generic.ScriptError("Parameter number must be an integer.", num.pos)
return OtherGRFParameter(grfid, num, self.pos)
def supported_by_action2(self, raise_error):
if raise_error:
raise generic.ScriptError("Reading parameters from another GRF is not supported in a switch-block.", self.pos)
return False
def supported_by_actionD(self, raise_error):
return True
def parse_string_to_dword(string):
"""
Convert string literal expression of length 4 to it's equivalent 32 bit number.
@param string: Expression to convert.
@type string: L{Expression}
@return: Value of the converted expression (a 32 bit integer number, little endian).
@rtype: C{int}
"""
if not isinstance(string, StringLiteral) or grfstrings.get_string_size(string.value, False, True) != 4:
raise generic.ScriptError("Expected a string literal of length 4", string.pos)
pos = string.pos
string = string.value
bytes = []
i = 0
try:
while len(bytes) < 4:
if string[i] == '\\':
bytes.append(int(string[i+1:i+3], 16))
i += 3
else:
bytes.append(ord(string[i]))
i += 1
except ValueError:
raise generic.ScriptError("Cannot convert string to integer id", pos)
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)
nml-0.4.5/nml/expression/bin_not.py 0000644 0005672 0005672 00000007004 13315644406 020446 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, nmlop
from .base_expression import Type, Expression, ConstantNumeric
from .binop import BinOp
from .boolean import Boolean
class BinNot(Expression):
def __init__(self, expr, pos = None):
Expression.__init__(self, pos)
self.expr = expr
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Binary not:')
self.expr.debug_print(indentation + 2)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
expr = self.expr.reduce(id_dicts)
if expr.type() != Type.INTEGER:
raise generic.ScriptError("Not-operator (~) requires an integer argument.", expr.pos)
if isinstance(expr, ConstantNumeric): return ConstantNumeric(0xFFFFFFFF ^ expr.value)
if isinstance(expr, BinNot): return expr.expr
return BinNot(expr)
def supported_by_action2(self, raise_error):
return self.expr.supported_by_action2(raise_error)
def supported_by_actionD(self, raise_error):
return self.expr.supported_by_actionD(raise_error)
def __str__(self):
return "~" + str(self.expr)
class Not(Expression):
def __init__(self, expr, pos = None):
Expression.__init__(self, pos)
self.expr = expr
def debug_print(self, indentation):
generic.print_dbg(indentation, 'Logical not:')
self.expr.debug_print(indentation + 2)
def reduce(self, id_dicts = [], unknown_id_fatal = True):
expr = self.expr.reduce(id_dicts)
if expr.type() != Type.INTEGER:
raise generic.ScriptError("Not-operator (!) requires an integer argument.", expr.pos)
if isinstance(expr, ConstantNumeric): return ConstantNumeric(expr.value == 0)
if isinstance(expr, Not): return Boolean(expr.expr).reduce()
if isinstance(expr, BinOp):
if expr.op == nmlop.CMP_EQ: return BinOp(nmlop.CMP_NEQ, expr.expr1, expr.expr2)
if expr.op == nmlop.CMP_NEQ: return BinOp(nmlop.CMP_EQ, expr.expr1, expr.expr2)
if expr.op == nmlop.CMP_LE: return BinOp(nmlop.CMP_GT, expr.expr1, expr.expr2)
if expr.op == nmlop.CMP_GE: return BinOp(nmlop.CMP_LT, expr.expr1, expr.expr2)
if expr.op == nmlop.CMP_LT: return BinOp(nmlop.CMP_GE, expr.expr1, expr.expr2)
if expr.op == nmlop.CMP_GT: return BinOp(nmlop.CMP_LE, expr.expr1, expr.expr2)
if expr.op == nmlop.HASBIT: return BinOp(nmlop.NOTHASBIT, expr.expr1, expr.expr2)
if expr.op == nmlop.NOTHASBIT: return BinOp(nmlop.HASBIT, expr.expr1, expr.expr2)
return Not(expr)
def supported_by_action2(self, raise_error):
return self.expr.supported_by_action2(raise_error)
def supported_by_actionD(self, raise_error):
return self.expr.supported_by_actionD(raise_error)
def is_boolean(self):
return True
def __str__(self):
return "!" + str(self.expr)
nml-0.4.5/nml/parser.py 0000644 0005672 0005672 00000070232 13315644406 016116 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic, expression, tokens, nmlop, unit
from nml.ast import assignment, basecost, cargotable, conditional, deactivate, disable_item, error, font, general, grf, item, loop, produce, railtypetable, replace, spriteblock, switch, townnames, snowline, skipall, tilelayout, alt_sprites, base_graphics, override, sort_vehicles
from nml.actions import actionD, real_sprite
import ply.yacc as yacc
class NMLParser(object):
"""
@ivar lexer: Scanner providing tokens.
@type lexer: L{NMLLexer}
@ivar tokens: Tokens of the scanner (used by PLY).
@type tokens: C{List} of C{str}
@ivar parser: PLY parser.
@type parser: L{ply.yacc}
"""
def __init__(self):
self.lexer = tokens.NMLLexer()
self.lexer.build()
self.tokens = self.lexer.tokens
self.parser = yacc.yacc(debug = False, module = self, write_tables = 0)
def parse(self, text, input_filename):
self.lexer.setup(text, input_filename)
return self.parser.parse(None, lexer = self.lexer.lexer)
#operator precedence (lower in the list = higher priority)
precedence = (
('left', 'COMMA'),
('right', 'TERNARY_OPEN','COLON'),
('left', 'LOGICAL_OR'),
('left', 'LOGICAL_AND'),
('left', 'OR'),
('left', 'XOR'),
('left', 'AND'),
('left', 'COMP_EQ', 'COMP_NEQ', 'COMP_LE', 'COMP_GE', 'COMP_LT', 'COMP_GT'),
('left', 'SHIFT_LEFT', 'SHIFT_RIGHT', 'SHIFTU_RIGHT'),
('left', 'PLUS', 'MINUS'),
('left', 'TIMES', 'DIVIDE', 'MODULO'),
('left', 'LOGICAL_NOT', 'BINARY_NOT'),
)
def p_error(self, t):
if t is None:
raise generic.ScriptError('Syntax error, unexpected end-of-file')
else:
raise generic.ScriptError('Syntax error, unexpected token "{}"'.format(t.value), t.lineno)
#
# Main script blocks
#
def p_main_script(self, t):
'main_script : script'
t[0] = general.MainScript(t[1])
def p_script(self, t):
'''script :
| script main_block'''
if len(t) == 1: t[0] = []
else: t[0] = t[1] + [t[2]]
def p_main_block(self, t):
'''main_block : switch
| random_switch
| produce
| spriteset
| spritegroup
| spritelayout
| template_declaration
| tilelayout
| town_names
| cargotable
| railtype
| grf_block
| param_assignment
| skip_all
| conditional
| loop
| item
| property_block
| graphics_block
| liveryoverride_block
| error_block
| disable_item
| deactivate
| replace
| replace_new
| base_graphics
| font_glyph
| alt_sprites
| snowline
| engine_override
| sort_vehicles
| basecost'''
t[0] = t[1]
#
# Expressions
#
def p_expression(self, t):
'''expression : NUMBER
| FLOAT
| param
| variable
| ID
| STRING_LITERAL
| string'''
t[0] = t[1]
def p_parenthesed_expression(self, t):
'expression : LPAREN expression RPAREN'
t[0] = t[2]
def p_parameter(self, t):
'param : PARAMETER LBRACKET expression RBRACKET'
t[0] = expression.Parameter(t[3], t.lineno(1), True)
def p_parameter_other_grf(self, t):
'param : PARAMETER LBRACKET expression COMMA expression RBRACKET'
t[0] = expression.OtherGRFParameter(t[3], t[5], t.lineno(1))
code_to_op = {
'+' : nmlop.ADD,
'-' : nmlop.SUB,
'*' : nmlop.MUL,
'/' : nmlop.DIV,
'%' : nmlop.MOD,
'&' : nmlop.AND,
'|' : nmlop.OR,
'^' : nmlop.XOR,
'&&' : nmlop.AND,
'||' : nmlop.OR,
'==' : nmlop.CMP_EQ,
'!=' : nmlop.CMP_NEQ,
'<=' : nmlop.CMP_LE,
'>=' : nmlop.CMP_GE,
'<' : nmlop.CMP_LT,
'>' : nmlop.CMP_GT,
'<<' : nmlop.SHIFT_LEFT,
'>>' : nmlop.SHIFT_RIGHT,
'>>>': nmlop.SHIFTU_RIGHT,
}
def p_binop(self, t):
'''expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression
| expression MODULO expression
| expression AND expression
| expression OR expression
| expression XOR expression
| expression SHIFT_LEFT expression
| expression SHIFT_RIGHT expression
| expression SHIFTU_RIGHT expression
| expression COMP_EQ expression
| expression COMP_NEQ expression
| expression COMP_LE expression
| expression COMP_GE expression
| expression COMP_LT expression
| expression COMP_GT expression'''
t[0] = expression.BinOp(self.code_to_op[t[2]], t[1], t[3], t[1].pos)
def p_binop_logical(self, t):
'''expression : expression LOGICAL_AND expression
| expression LOGICAL_OR expression'''
t[0] = expression.BinOp(self.code_to_op[t[2]], expression.Boolean(t[1]), expression.Boolean(t[3]), t[1].pos)
def p_logical_not(self, t):
'expression : LOGICAL_NOT expression'
t[0] = expression.Not(expression.Boolean(t[2]), t.lineno(1))
def p_binary_not(self, t):
'expression : BINARY_NOT expression'
t[0] = expression.BinNot(t[2], t.lineno(1))
def p_ternary_op(self, t):
'expression : expression TERNARY_OPEN expression COLON expression'
t[0] = expression.TernaryOp(t[1], t[3], t[5], t[1].pos)
def p_unary_minus(self, t):
'expression : MINUS expression'
t[0] = expression.BinOp(self.code_to_op[t[1]], expression.ConstantNumeric(0), t[2], t.lineno(1))
def p_variable(self, t):
'variable : VARIABLE LBRACKET expression_list RBRACKET'
t[0] = expression.Variable(*t[3])
t[0].pos = t.lineno(1)
def p_function(self, t):
'expression : ID LPAREN expression_list RPAREN'
t[0] = expression.FunctionCall(t[1], t[3], t[1].pos)
def p_array(self, t):
'expression : LBRACKET expression_list RBRACKET'
t[0] = expression.Array(t[2], t.lineno(1))
#
# Commonly used non-terminals that are not expressions
#
def p_assignment_list(self, t):
'''assignment_list : assignment
| param_desc
| assignment_list assignment
| assignment_list param_desc'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
def p_assignment(self, t):
'assignment : ID COLON expression SEMICOLON'
t[0] = assignment.Assignment(t[1], t[3], t[1].pos)
def p_param_desc(self, t):
'''param_desc : PARAMETER expression LBRACE setting_list RBRACE
| PARAMETER LBRACE setting_list RBRACE'''
if len(t) == 5: t[0] = grf.ParameterDescription(t[3])
else: t[0] = grf.ParameterDescription(t[4], t[2])
def p_setting_list(self, t):
'''setting_list : setting
| setting_list setting'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
def p_setting(self, t):
'setting : ID LBRACE setting_value_list RBRACE'
t[0] = grf.ParameterSetting(t[1], t[3])
def p_setting_value_list(self, t):
'''setting_value_list : setting_value
| setting_value_list setting_value'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
def p_setting_value(self, t):
'setting_value : assignment'
t[0] = t[1]
def p_names_setting_value(self, t):
'setting_value : ID COLON LBRACE name_string_list RBRACE SEMICOLON'
t[0] = assignment.Assignment(t[1], t[4], t[1].pos)
def p_name_string_list(self, t):
'''name_string_list : name_string_item
| name_string_list name_string_item'''
if len(t) == 2: t[0] = expression.Array([t[1]], t[1].pos)
else: t[0] = expression.Array(t[1].values + [t[2]], t[1].pos)
def p_name_string_item(self, t):
'name_string_item : expression COLON string SEMICOLON'
t[0] = assignment.Assignment(t[1], t[3], t[1].pos)
def p_string(self, t):
'string : STRING LPAREN expression_list RPAREN'
t[0] = expression.String(t[3], t.lineno(1))
def p_non_empty_expression_list(self, t):
'''non_empty_expression_list : expression
| non_empty_expression_list COMMA expression'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[3]]
def p_expression_list(self, t):
'''expression_list :
| non_empty_expression_list
| non_empty_expression_list COMMA'''
t[0] = [] if len(t) == 1 else t[1]
def p_non_empty_id_list(self, t):
'''non_empty_id_list : ID
| non_empty_id_list COMMA ID'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[3]]
def p_id_list(self, t):
'''id_list :
| non_empty_id_list
| non_empty_id_list COMMA'''
t[0] = [] if len(t) == 1 else t[1]
def p_generic_assignment(self, t):
'generic_assignment : expression COLON expression SEMICOLON'
t[0] = assignment.Assignment(t[1], t[3], t.lineno(1))
def p_generic_assignment_list(self, t):
'''generic_assignment_list :
| generic_assignment_list generic_assignment'''
t[0] = [] if len(t) == 1 else t[1] + [t[2]]
def p_snowline_assignment(self, t):
'''snowline_assignment : expression COLON expression SEMICOLON
| expression COLON expression UNIT SEMICOLON'''
unit_value = None if len(t) == 5 else unit.get_unit(t[4])
t[0] = assignment.UnitAssignment(t[1], t[3], unit_value, t.lineno(1))
def p_snowline_assignment_list(self, t):
'''snowline_assignment_list :
| snowline_assignment_list snowline_assignment'''
t[0] = [] if len(t) == 1 else t[1] + [t[2]]
#
# Item blocks
#
def p_item(self, t):
'item : ITEM LPAREN expression_list RPAREN LBRACE script RBRACE'
t[0] = item.Item(t[3], t[6], t.lineno(1))
def p_property_block(self, t):
'property_block : PROPERTY LBRACE property_list RBRACE'
t[0] = item.PropertyBlock(t[3], t.lineno(1))
def p_property_list(self, t):
'''property_list : property_assignment
| property_list property_assignment'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
def p_property_assignment(self, t):
'''property_assignment : ID COLON expression SEMICOLON
| ID COLON expression UNIT SEMICOLON
| NUMBER COLON expression SEMICOLON
| NUMBER COLON expression UNIT SEMICOLON'''
name = t[1]
unit_value = None if len(t) == 5 else unit.get_unit(t[4])
t[0] = item.Property(name, t[3], unit_value, t.lineno(1))
def p_graphics_block(self, t):
'graphics_block : GRAPHICS LBRACE graphics_list RBRACE'
t[0] = item.GraphicsBlock(t[3][0], t[3][1], t.lineno(1))
def p_liveryoverride_block(self, t):
'liveryoverride_block : LIVERYOVERRIDE LPAREN expression RPAREN LBRACE graphics_list RBRACE'
t[0] = item.LiveryOverride(t[3], item.GraphicsBlock(t[6][0], t[6][1], t.lineno(1)), t.lineno(1))
def p_graphics_list(self, t):
'''graphics_list : graphics_assignment_list
| graphics_assignment_list switch_value
| switch_value'''
# Save graphics block as a tuple, we need to add position info later
if len(t) == 2:
if isinstance(t[1], list):
t[0] = (t[1], None)
else:
t[0] = ([], t[1])
else:
t[0] = (t[1], t[2])
def p_graphics_assignment(self, t):
'graphics_assignment : expression COLON switch_value'
t[0] = item.GraphicsDefinition(t[1], t[3])
def p_graphics_assignment_list(self, t):
'''graphics_assignment_list : graphics_assignment
| graphics_assignment_list graphics_assignment'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
#
# Program flow control (if/else/while)
#
def p_conditional(self, t):
'''conditional : if_else_parts
| if_else_parts else_block'''
parts = t[1]
if len(t) > 2:
parts.append(t[2])
t[0] = conditional.ConditionalList(parts)
def p_else_block(self, t):
'else_block : ELSE LBRACE script RBRACE'
t[0] = conditional.Conditional(None, t[3], t.lineno(1))
def p_if_else_parts(self, t):
'''if_else_parts : IF LPAREN expression RPAREN LBRACE script RBRACE
| if_else_parts ELSE IF LPAREN expression RPAREN LBRACE script RBRACE'''
if len(t) == 8: t[0] = [conditional.Conditional(t[3], t[6], t.lineno(1))]
else: t[0] = t[1] + [conditional.Conditional(t[5], t[8], t.lineno(2))]
def p_loop(self, t):
'loop : WHILE LPAREN expression RPAREN LBRACE script RBRACE'
t[0] = loop.Loop(t[3], t[6], t.lineno(1))
#
# (Random) Switch block
#
def p_switch(self, t):
'switch : SWITCH LPAREN expression_list RPAREN LBRACE switch_body RBRACE'
t[0] = switch.Switch(t[3], t[6], t.lineno(1))
def p_switch_body(self, t):
'''switch_body : switch_ranges switch_value
| switch_ranges'''
t[0] = switch.SwitchBody(t[1], t[2] if len(t) == 3 else None)
def p_switch_ranges(self, t):
'''switch_ranges :
| switch_ranges expression COLON switch_value
| switch_ranges expression UNIT COLON switch_value
| switch_ranges expression RANGE expression COLON switch_value
| switch_ranges expression RANGE expression UNIT COLON switch_value'''
if len(t) == 1: t[0] = []
elif len(t) == 5: t[0] = t[1] + [switch.SwitchRange(t[2], t[2], t[4])]
elif len(t) == 6: t[0] = t[1] + [switch.SwitchRange(t[2], t[2], t[5], t[3])]
elif len(t) == 7: t[0] = t[1] + [switch.SwitchRange(t[2], t[4], t[6])]
else: t[0] = t[1] + [switch.SwitchRange(t[2], t[4], t[7], t[5])]
def p_switch_value(self, t):
'''switch_value : RETURN expression SEMICOLON
| RETURN SEMICOLON
| expression SEMICOLON'''
if len(t) == 4: t[0] = switch.SwitchValue(t[2], True, t[2].pos)
elif t[1] == 'return': t[0] = switch.SwitchValue(None, True, t.lineno(1))
else: t[0] = switch.SwitchValue(t[1], False, t[1].pos)
def p_random_switch(self, t):
'random_switch : RANDOMSWITCH LPAREN expression_list RPAREN LBRACE random_body RBRACE'
t[0] = switch.RandomSwitch(t[3], t[6], t.lineno(1))
def p_random_body(self, t):
'''random_body :
| random_body expression COLON switch_value'''
if len(t) == 1: t[0] = []
else: t[0] = t[1] + [switch.RandomChoice(t[2], t[4])]
def p_produce(self, t):
'produce : PRODUCE LPAREN expression_list RPAREN SEMICOLON'
t[0] = produce.Produce(t[3], t.lineno(1))
#
# Real sprites and related stuff
#
def p_real_sprite(self, t):
'''real_sprite : LBRACKET expression_list RBRACKET
| ID COLON LBRACKET expression_list RBRACKET'''
if len(t) == 4:
t[0] = real_sprite.RealSprite(param_list = t[2], poslist = [t.lineno(1)])
else:
t[0] = real_sprite.RealSprite(param_list = t[4], label = t[1], poslist = [t.lineno(1)])
def p_recolour_assignment_list(self, t):
'''recolour_assignment_list :
| recolour_assignment_list recolour_assignment'''
t[0] = [] if len(t) == 1 else t[1] + [t[2]]
def p_recolour_assignment_1(self, t):
'recolour_assignment : expression COLON expression SEMICOLON'
t[0] = assignment.Assignment(assignment.Range(t[1], None), assignment.Range(t[3], None), t[1].pos)
def p_recolour_assignment_2(self, t):
'recolour_assignment : expression RANGE expression COLON expression RANGE expression SEMICOLON'
t[0] = assignment.Assignment(assignment.Range(t[1], t[3]), assignment.Range(t[5], t[7]), t[1].pos)
def p_recolour_assignment_3(self, t):
'recolour_assignment : expression RANGE expression COLON expression SEMICOLON'
t[0] = assignment.Assignment(assignment.Range(t[1], t[3]), assignment.Range(t[5], None), t[1].pos)
def p_recolour_sprite(self, t):
'''real_sprite : RECOLOUR_SPRITE LBRACE recolour_assignment_list RBRACE
| ID COLON RECOLOUR_SPRITE LBRACE recolour_assignment_list RBRACE'''
if len(t) == 5:
t[0] = real_sprite.RecolourSprite(mapping = t[3], poslist = [t.lineno(1)])
else:
t[0] = real_sprite.RecolourSprite(mapping = t[5], label = t[1], poslist = [t.lineno(1)])
def p_template_declaration(self, t):
'template_declaration : TEMPLATE ID LPAREN id_list RPAREN LBRACE spriteset_contents RBRACE'
t[0] = spriteblock.TemplateDeclaration(t[2], t[4], t[7], t.lineno(1))
def p_template_usage(self, t):
'''template_usage : ID LPAREN expression_list RPAREN
| ID COLON ID LPAREN expression_list RPAREN'''
if len(t) == 5:
t[0] = real_sprite.TemplateUsage(t[1], t[3], None, t.lineno(1))
else:
t[0] = real_sprite.TemplateUsage(t[3], t[5], t[1], t.lineno(1))
def p_spriteset_contents(self, t):
'''spriteset_contents : real_sprite
| template_usage
| spriteset_contents real_sprite
| spriteset_contents template_usage'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
def p_replace(self, t):
'''replace : REPLACESPRITE LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE
| REPLACESPRITE ID LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE'''
if len(t) == 9: t[0] = replace.ReplaceSprite(t[4], t[7], t[2], t.lineno(1))
else: t[0] = replace.ReplaceSprite(t[3], t[6], None, t.lineno(1))
def p_replace_new(self, t):
'''replace_new : REPLACENEWSPRITE LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE
| REPLACENEWSPRITE ID LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE'''
if len(t) == 9: t[0] = replace.ReplaceNewSprite(t[4], t[7], t[2], t.lineno(1))
else: t[0] = replace.ReplaceNewSprite(t[3], t[6], None, t.lineno(1))
def p_base_graphics(self, t):
'''base_graphics : BASE_GRAPHICS LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE
| BASE_GRAPHICS ID LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE'''
if len(t) == 9: t[0] = base_graphics.BaseGraphics(t[4], t[7], t[2], t.lineno(1))
else: t[0] = base_graphics.BaseGraphics(t[3], t[6], None, t.lineno(1))
def p_font_glyph(self, t):
'''font_glyph : FONTGLYPH LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE
| FONTGLYPH ID LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE'''
if len(t) == 9: t[0] = font.FontGlyphBlock(t[4], t[7], t[2], t.lineno(1))
else: t[0] = font.FontGlyphBlock(t[3], t[6], None, t.lineno(1))
def p_alt_sprites(self, t):
'alt_sprites : ALT_SPRITES LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE'
t[0] = alt_sprites.AltSpritesBlock(t[3], t[6], t.lineno(1))
#
# Sprite sets/groups and such
#
def p_spriteset(self, t):
'spriteset : SPRITESET LPAREN expression_list RPAREN LBRACE spriteset_contents RBRACE'
t[0] = spriteblock.SpriteSet(t[3], t[6], t.lineno(1))
def p_spritegroup_normal(self, t):
'spritegroup : SPRITEGROUP ID LBRACE spriteview_list RBRACE'
t[0] = spriteblock.SpriteGroup(t[2], t[4], t.lineno(1))
def p_spritelayout(self, t):
'''spritelayout : SPRITELAYOUT ID LBRACE layout_sprite_list RBRACE
| SPRITELAYOUT ID LPAREN id_list RPAREN LBRACE layout_sprite_list RBRACE'''
if len(t) == 6:
t[0] = spriteblock.SpriteLayout(t[2], [], t[4], t.lineno(1))
else:
t[0] = spriteblock.SpriteLayout(t[2], t[4], t[7], t.lineno(1))
def p_spriteview_list(self, t):
'''spriteview_list :
| spriteview_list spriteview'''
if len(t) == 1: t[0] = []
else: t[0] = t[1] + [t[2]]
def p_spriteview(self, t):
'''spriteview : ID COLON LBRACKET expression_list RBRACKET SEMICOLON
| ID COLON expression SEMICOLON'''
if len(t) == 7: t[0] = spriteblock.SpriteView(t[1], t[4], t.lineno(1))
else: t[0] = spriteblock.SpriteView(t[1], [t[3]], t.lineno(1))
def p_layout_sprite_list(self, t):
'''layout_sprite_list :
| layout_sprite_list layout_sprite'''
if len(t) == 1: t[0] = []
else: t[0] = t[1] + [t[2]]
def p_layout_sprite(self, t):
'layout_sprite : ID LBRACE layout_param_list RBRACE'
t[0] = spriteblock.LayoutSprite(t[1], t[3], t.lineno(1))
def p_layout_param_list(self, t):
'''layout_param_list : assignment
| layout_param_list assignment'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
#
# Town names
#
def p_town_names(self, t):
'''town_names : TOWN_NAMES LPAREN expression RPAREN LBRACE town_names_param_list RBRACE
| TOWN_NAMES LBRACE town_names_param_list RBRACE'''
if len(t) == 8: t[0] = townnames.TownNames(t[3], t[6], t.lineno(1))
else: t[0] = townnames.TownNames(None, t[3], t.lineno(1))
def p_town_names_param_list(self, t):
'''town_names_param_list : town_names_param
| town_names_param_list town_names_param'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
def p_town_names_param(self, t):
'''town_names_param : ID COLON string SEMICOLON
| LBRACE town_names_part_list RBRACE
| LBRACE town_names_part_list COMMA RBRACE'''
if t[1] != '{': t[0] = townnames.TownNamesParam(t[1], t[3], t.lineno(1))
else: t[0] = townnames.TownNamesPart(t[2], t.lineno(1))
def p_town_names_part_list(self, t):
'''town_names_part_list : town_names_part
| town_names_part_list COMMA town_names_part'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[3]]
def p_town_names_part(self, t):
'''town_names_part : TOWN_NAMES LPAREN expression COMMA expression RPAREN
| ID LPAREN STRING_LITERAL COMMA expression RPAREN'''
if t[1] == 'town_names': t[0] = townnames.TownNamesEntryDefinition(t[3], t[5], t.lineno(1))
else: t[0] = townnames.TownNamesEntryText(t[1], t[3], t[5], t.lineno(1))
#
# Snow line
#
def p_snowline(self, t):
'snowline : SNOWLINE LPAREN ID RPAREN LBRACE snowline_assignment_list RBRACE'
t[0] = snowline.Snowline(t[3], t[6], t.lineno(1))
#
# Various misc. main script blocks that don't belong anywhere else
#
def p_param_assignment(self, t):
'param_assignment : expression EQ expression SEMICOLON'
t[0] = actionD.ParameterAssignment(t[1], t[3])
def p_error_block(self, t):
'error_block : ERROR LPAREN expression_list RPAREN SEMICOLON'
t[0] = error.Error(t[3], t.lineno(1))
def p_disable_item(self, t):
'disable_item : DISABLE_ITEM LPAREN expression_list RPAREN SEMICOLON'
t[0] = disable_item.DisableItem(t[3], t.lineno(1))
def p_cargotable(self, t):
'''cargotable : CARGOTABLE LBRACE cargotable_list RBRACE
| CARGOTABLE LBRACE cargotable_list COMMA RBRACE'''
t[0] = cargotable.CargoTable(t[3], t.lineno(1))
def p_cargotable_list(self, t):
'''cargotable_list : ID
| STRING_LITERAL
| cargotable_list COMMA ID
| cargotable_list COMMA STRING_LITERAL'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[3]]
def p_railtypetable(self, t):
'''railtype : RAILTYPETABLE LBRACE railtypetable_list RBRACE
| RAILTYPETABLE LBRACE railtypetable_list COMMA RBRACE'''
t[0] = railtypetable.RailtypeTable(t[3], t.lineno(1))
def p_railtypetable_list(self, t):
'''railtypetable_list : railtypetable_item
| railtypetable_list COMMA railtypetable_item'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[3]]
def p_railtypetable_item(self, t):
'''railtypetable_item : ID
| STRING_LITERAL
| ID COLON LBRACKET expression_list RBRACKET'''
if len(t) == 2: t[0] = t[1]
else: t[0] = assignment.Assignment(t[1], t[4], t[1].pos)
def p_basecost(self, t):
'basecost : BASECOST LBRACE generic_assignment_list RBRACE'
t[0] = basecost.BaseCost(t[3], t.lineno(1))
def p_deactivate(self, t):
'deactivate : DEACTIVATE LPAREN expression_list RPAREN SEMICOLON'
t[0] = deactivate.DeactivateBlock(t[3], t.lineno(1))
def p_grf_block(self, t):
'grf_block : GRF LBRACE assignment_list RBRACE'
t[0] = grf.GRF(t[3], t.lineno(1))
def p_skip_all(self, t):
'skip_all : SKIP_ALL SEMICOLON'
t[0] = skipall.SkipAll(t.lineno(1))
def p_engine_override(self, t):
'engine_override : ENGINE_OVERRIDE LPAREN expression_list RPAREN SEMICOLON'
t[0] = override.EngineOverride(t[3], t.lineno(1))
def p_sort_vehicles(self, t):
'sort_vehicles : SORT_VEHICLES LPAREN expression_list RPAREN SEMICOLON'
t[0] = sort_vehicles.SortVehicles(t[3], t.lineno(1))
def p_tilelayout(self, t):
'tilelayout : TILELAYOUT ID LBRACE tilelayout_list RBRACE'
t[0] = tilelayout.TileLayout(t[2], t[4], t.lineno(1))
def p_tilelayout_list(self, t):
'''tilelayout_list : tilelayout_item
| tilelayout_list tilelayout_item'''
if len(t) == 2: t[0] = [t[1]]
else: t[0] = t[1] + [t[2]]
def p_tilelayout_item_tile(self, t):
'tilelayout_item : expression COMMA expression COLON expression SEMICOLON'
t[0] = tilelayout.LayoutTile(t[1], t[3], t[5])
def p_tilelayout_item_prop(self, t):
'tilelayout_item : assignment'
t[0] = t[1]
nml-0.4.5/nml/free_number_list.py 0000644 0005672 0005672 00000011162 13315644406 020143 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
from nml import generic
class FreeNumberList(object):
"""
Contains a list with numbers and functions to pop one number from the list,
to save the current state and to restore to the previous state.
@ivar total_amount: Amount of numbers in the beginning.
@type total_amount: C{int}
@ivar stats: Statistics about id usage.
@type stats: Tuple of C{int} and L{Position} refering to the amount on location of most concurrently used ids.
@ivar free_numbers: The list with currently unused numbers.
@type free_numbers: C{list}
@ivar states: A list of lists. Each sublist contains all numbers
that were L{popped} between the last call to L{save} and
the next call to L{save}. Every time L{save} is called one
sublist is added, every time L{restore} is called the topmost
sublist is removed and it's values are added to the free number
list again.
@type states: C{list}
@ivar used_numbers: A set with all numbers that have been used at
some time. This is used by L{pop_unique}.
@type used_numbers: C{set}
@ivar exception: Exception to be thrown when there is no number available
when requested via pop() or pop_global().
@type exception: C{str}
@ivar exception_unique: Exception to be thrown when there is no unique number
available when it is requested via pop_unique().
@type exception_unique: C{str}
"""
def __init__(self, free_numbers, exception, exception_unique):
self.total_amount = len(free_numbers)
self.stats = (0, None)
self.free_numbers = free_numbers
self.states = []
self.used_numbers = set()
self.exception = exception
self.exception_unique = exception_unique
def pop(self, pos):
"""
Pop a free number from the list. You have to call L{save} at least
once before calling L{pop}.
@param pos: Positional context.
@type pos: L{Position}
@return: Some free number.
"""
assert len(self.states) > 0
if len(self.free_numbers) == 0:
raise generic.ScriptError(self.exception, pos)
num = self.free_numbers.pop()
self.states[-1].append(num)
self.used_numbers.add(num)
num_used = len(self.used_numbers)
if num_used > self.stats[0]:
self.stats = (num_used, pos)
return num
def pop_global(self, pos):
"""
Pop a free number from the list. The number may have been used before
and already been restored but it'll never be given out again.
@param pos: Positional context.
@type pos: L{Position}
@return: Some free number.
"""
if len(self.free_numbers) == 0:
raise generic.ScriptError(self.exception, pos)
return self.free_numbers.pop()
def pop_unique(self, pos):
"""
Pop a free number from the list. The number has not been used before
and will not be used again.
@param pos: Positional context.
@type pos: L{Position}
@return: A unique free number.
"""
for num in reversed(self.free_numbers):
if num in self.used_numbers: continue
self.free_numbers.remove(num)
self.used_numbers.add(num)
num_used = len(self.used_numbers)
if num_used > self.stats[0]:
self.stats = (num_used, pos)
return num
raise generic.ScriptError(self.exception_unique, pos)
def save(self):
"""
Save the current state. All calls to L{pop} will be saved and can be
reverted by calling L{restore}
"""
self.states.append([])
def restore(self):
"""
Add all numbers given out via L{pop} since the last L{save} to the free
number list.
"""
assert len(self.states) > 0
self.states[-1].reverse()
self.free_numbers.extend(self.states[-1])
self.states.pop()
nml-0.4.5/nml/output_nml.py 0000644 0005672 0005672 00000002123 13315644406 017022 0 ustar jenkins jenkins 0000000 0000000 __license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
NML is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
import codecs
from nml import output_base
class OutputNML(output_base.TextOutputBase):
"""
Class for outputting NML.
"""
def __init__(self, filename):
output_base.TextOutputBase.__init__(self, filename)
def open_file(self):
return codecs.open(self.filename, 'w', encoding='utf-8')
def write(self, text):
self.file.write(text)
def newline(self):
self.file.write('\n')
nml-0.4.5/examples/ 0000755 0005672 0005672 00000000000 13315644467 015303 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/examples/road_vehicle/ 0000755 0005672 0005672 00000000000 13315644467 017727 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/examples/road_vehicle/example_road_vehicle.nml 0000644 0005672 0005672 00000017123 13315644406 024573 0 ustar jenkins jenkins 0000000 0000000 /*
* This file is aimed to provide an example on how to code a vehicle in NML
* In this case a road vehicle is coded, other vehicle types work in a similar fashion.
* To keep the code readable, not every property or variable is documented in
* detail, refer to the vehicle-specific reference in the documentation.
* The coded vehicle is relatively simple. Apart from the minimum required,
* it has cargo-specific capacaity and graphics. For a more advanced example,
* refer to the train example.
*
* The vehicle coded here is a first-generation flatbed truck.
* It is taken from OpenGFX+ Road Vehicles, with some modifications
* to provide a better example. Original graphics are by DanMack and Zephyris,
* coding by Terkhen and planetmaker.
*
* Apart from this file, you will also need the following
* - Graphics, found in 'gfx' folder.
* - Language files, to be placed in the 'lang' folder.
* Currently only english.lng is supplied.
*/
/*
* First, define a grf block. This defines some basic properties of the grf,
* which are required for the grf to be valid and loadable. Additionally,
* user-configurable parameters are defined here also.
*/
grf {
/* This grf is part of NML, therefore "NML" is chosen as the first three
* characters of the GRFID. It is the fourth real grf defined as part of
* NML, therefore the last character is set to 3 as numbering is zero-based.
* Successive grfs will have 4, 5, etc., to make sure each example grf has
* a unique GRFID.
*/
grfid: "NML\03";
/* GRF name and description strings are defined in the lang files */
name: string(STR_GRF_NAME);
desc: string(STR_GRF_DESC);
/* This is the first version, start numbering at 0. */
version: 0;
min_compatible_version: 0;
}
/* Check for engine pool */
if (!dynamic_engines) {
error(ERROR, USED_WITH, string(STR_ERROR_ENGINE_POOL));
}
/* Define a cargo translation table
* All cargo types that need any special treatment must be included here
* Add cargos used in the refit mask first, as those are limited to 32 bits
* so these cargos must be within the first 32 entries */
cargotable {
/* Used in refit mask */
LVST, // Livestock (piece goods)
WOOL, // Wool (piece goods, covered)
SCRP, // Scrap metal (bulk)
FICR, // Fibre crops (bulk, piece goods)
PETR, // Petrol / fuel oil (liquid)
RFPR, // Refined products (liquid)
/* Not used in refit mask */
GOOD, // Goods (express)
ENSP, // Engineering supplies (express, piece goods)
FMSP, // Farm supplies (express, piece goods)
MNSP, // Manufacturing supplies (express, piece goods)
PAPR, // Paper (piece goods)
STEL, // Steel (piece goods)
VEHI, // Vehicles (piece goods, oversized)
COPR, // Copper (piece goods)
WOOD, // Wood (piece goods)
}
/* Sprite template for a truck */
template tmpl_truck(x) {
[ 0 + x, 0, 8, 18, -3, -10]
[ 16 + x, 0, 20, 16, -14, -7]
[ 48 + x, 0, 28, 12, -14, -6]
[ 96 + x, 0, 20, 16, -6, -7]
[ 128 + x, 0, 8, 18, -3, -10]
[ 144 + x, 0, 20, 16, -14, -7]
[ 176 + x, 0, 28, 12, -14, -6]
[ 224 + x, 0, 20, 16, -6, -7]
}
/* Define various cargo-specific graphics */
/* Paper */
spriteset(flatbed_truck_1_paper_empty, "gfx/flatbed_truck_1_paper.png") { tmpl_truck(0) }
spriteset(flatbed_truck_1_paper_full, "gfx/flatbed_truck_1_paper.png") { tmpl_truck(260) }
spritegroup flatbed_truck_1_paper {
loaded: [flatbed_truck_1_paper_empty, flatbed_truck_1_paper_full];
loading: [flatbed_truck_1_paper_empty, flatbed_truck_1_paper_full];
}
/* Steel */
spriteset(flatbed_truck_1_steel_empty, "gfx/flatbed_truck_1_steel.png") { tmpl_truck(0) }
spriteset(flatbed_truck_1_steel_full, "gfx/flatbed_truck_1_steel.png") { tmpl_truck(260) }
spritegroup flatbed_truck_1_steel {
loaded: [flatbed_truck_1_steel_empty, flatbed_truck_1_steel_full];
loading: [flatbed_truck_1_steel_empty, flatbed_truck_1_steel_full];
}
/* Wood */
spriteset(flatbed_truck_1_wood_empty, "gfx/flatbed_truck_1_wood.png") { tmpl_truck(0) }
spriteset(flatbed_truck_1_wood_full, "gfx/flatbed_truck_1_wood.png") { tmpl_truck(260) }
spritegroup flatbed_truck_1_wood {
loaded: [flatbed_truck_1_wood_empty, flatbed_truck_1_wood_full];
loading: [flatbed_truck_1_wood_empty, flatbed_truck_1_wood_full];
}
/* Copper */
spriteset(flatbed_truck_1_copper_empty, "gfx/flatbed_truck_1_copper.png") { tmpl_truck(0) }
spriteset(flatbed_truck_1_copper_full, "gfx/flatbed_truck_1_copper.png") { tmpl_truck(260) }
spritegroup flatbed_truck_1_copper {
loaded: [flatbed_truck_1_copper_empty, flatbed_truck_1_copper_full];
loading: [flatbed_truck_1_copper_empty, flatbed_truck_1_copper_full];
}
/* Goods */
spriteset(flatbed_truck_1_goods_empty, "gfx/flatbed_truck_1_goods.png") { tmpl_truck(0) }
spriteset(flatbed_truck_1_goods_full, "gfx/flatbed_truck_1_goods.png") { tmpl_truck(260) }
spritegroup flatbed_truck_1_goods {
loaded: [flatbed_truck_1_goods_empty, flatbed_truck_1_goods_full];
loading: [flatbed_truck_1_goods_empty, flatbed_truck_1_goods_full];
}
/* Decide capacity based on cargo type (cargo_capacity callback)
* cargo_type_in_veh is available in the purchase list also,
* so a separate CB for in the purchase list is not needed */
switch (FEAT_ROADVEHS, SELF, flatbed_truck_1_capacity_switch, cargo_type_in_veh) {
GOOD: return 14;
ENSP: return 14;
FMSP: return 14;
MNSP: return 14;
PAPR: return 15;
PETR: return 10;
RFPR: return 10;
STEL: return 15;
VEHI: return 12;
return 20;
}
/* Define the road vehicle */
item(FEAT_ROADVEHS, flatbed_truck_1) {
property {
/* Properties common to all vehicle types */
name: string(STR_NAME_FLATBED_TRUCK_1);
climates_available: bitmask(CLIMATE_TEMPERATE, CLIMATE_ARCTIC, CLIMATE_TROPICAL);
introduction_date: date(1926,01,01);
model_life: 65;
/* retire_early not set, use default retirement behaviour */
vehicle_life: 15;
reliability_decay: 20;
refittable_cargo_classes: bitmask(CC_PIECE_GOODS, CC_EXPRESS);
non_refittable_cargo_classes: bitmask(CC_PASSENGERS, CC_REFRIGERATED);
/* Livestock, wool and fibre crops can be transported acc. cargo classes
* But we don't want that, so disable refitting for them.
* Scrap metal, petrol and refined products cannot be transported acc. cargo classes
* We do want to transport those, so enable refitting for them. */
cargo_allow_refit: [LVST, WOOL, SCRP, FICR, PETR, RFPR];
cargo_disallow_refit: []; // we allow other cargoes, if class matches
loading_speed: 5;
cost_factor: 108;
running_cost_factor: 90;
/* cargo_age_period is left at default */
/* RV-specific properties */
sprite_id: SPRITE_ID_NEW_ROADVEH;
speed: 48 km/h;
misc_flags: bitmask(ROADVEH_FLAG_2CC);
refit_cost: 0; // Refitting is free
/* callback_flags are not set, no need to manually enable callbacks */
running_cost_base: RUNNING_COST_ROADVEH;
power: 120 hp;
weight: 9.5 ton;
/* TE and air drag coefficient is left at default */
cargo_capacity: 20; // Changed by callback
sound_effect: SOUND_BUS_START_PULL_AWAY;
/* Visual effect is left at default (no effect) */
}
/* Define graphics for various cargo types, as well as the capacity callback */
graphics {
cargo_capacity: flatbed_truck_1_capacity_switch;
PAPR: flatbed_truck_1_paper;
STEL: flatbed_truck_1_steel;
COPR: flatbed_truck_1_copper;
WOOD: flatbed_truck_1_wood;
default: flatbed_truck_1_goods; // Default to Goods.
}
}
nml-0.4.5/examples/road_vehicle/gfx/ 0000755 0005672 0005672 00000000000 13315644467 020513 5 ustar jenkins jenkins 0000000 0000000 nml-0.4.5/examples/road_vehicle/gfx/flatbed_truck_1_copper.png 0000644 0005672 0005672 00000004062 13315644406 025615 0 ustar jenkins jenkins 0000000 0000000 ‰PNG
IHDR ø €^< sRGB ®Îé PLTE ÿî îï ïð ðñ ñò òó óô ôõ õö ö¨¨¨¸¸¸ÈÈÈØØØèèèüüü4