Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package nml for openSUSE:Factory checked in 
at 2026-04-16 17:26:13
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/nml (Old)
 and      /work/SRC/openSUSE:Factory/.nml.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "nml"

Thu Apr 16 17:26:13 2026 rev:31 rq:1347253 version:0.9.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/nml/nml.changes  2025-12-04 11:27:28.840042282 
+0100
+++ /work/SRC/openSUSE:Factory/.nml.new.11940/nml.changes       2026-04-16 
17:26:56.511362250 +0200
@@ -0,0 +1,16 @@
+-------------------------------------------------------------------
+Thu Apr 16 09:24:22 UTC 2026 - Jan Baier <[email protected]>
+
+- update to 0.9.0:
+  Support for NewGRF additions of OpenTTD 16:
+   - Add: Flag for allow unpowered wagons to lead a train when backing up 
(#420)
+   - Add: Variable for when a train is driving backwards (#421)
+   - Change: distinguish perimeter from area in station distributed cargo 
flag. (#422)
+  Support for NewGRF additions of OpenTTD 15:
+   - Add: Support for NewGRF badges. (#359)
+   - Change: add support for vehicle var 0x65 (#378)
+  Other changes and fixes:
+   - Fix: Feature 0x14 missing from extract tables. (#403)
+   - Fix #407: missing position for some action0 errors (#408)
+
+-------------------------------------------------------------------

Old:
----
  nml-0.8.1.tar.gz

New:
----
  nml-0.9.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ nml.spec ++++++
--- /var/tmp/diff_new_pack.uslz1h/_old  2026-04-16 17:26:58.355438124 +0200
+++ /var/tmp/diff_new_pack.uslz1h/_new  2026-04-16 17:26:58.367438618 +0200
@@ -1,8 +1,7 @@
 #
 # spec file for package nml
 #
-# Copyright (c) 2025 SUSE LLC
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +17,7 @@
 
 
 Name:           nml
-Version:        0.8.1
+Version:        0.9.0
 Release:        0
 Summary:        NewGRF Meta Language
 License:        GPL-2.0-or-later
@@ -27,15 +26,13 @@
 Source:         
https://github.com/OpenTTD/%{name}/releases/download/%{version}/%{name}-%{version}.tar.gz
 BuildRequires:  fdupes
 BuildRequires:  gcc
-BuildRequires:  python3-devel
+BuildRequires:  python3-devel >= 3.10
 BuildRequires:  python3-pip
 BuildRequires:  python3-setuptools
 BuildRequires:  python3-wheel
 # We need the required packages also on building for regression tests:
 BuildRequires:  python3-Pillow >= 3.4
-BuildRequires:  python3-ply
 Requires:       python3-Pillow >= 3.4
-Requires:       python3-ply
 Provides:       nmlc = %{version}
 
 %description

++++++ nml-0.8.1.tar.gz -> nml-0.9.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/PKG-INFO new/nml-0.9.0/PKG-INFO
--- old/nml-0.8.1/PKG-INFO      2025-12-02 20:17:28.717712000 +0100
+++ new/nml-0.9.0/PKG-INFO      2026-04-16 00:54:57.798354400 +0200
@@ -1,10 +1,10 @@
 Metadata-Version: 2.4
 Name: nml
-Version: 0.8.1
+Version: 0.9.0
 Summary: An OpenTTD NewGRF compiler for the nml language
 Home-page: https://github.com/OpenTTD/nml
 Author: NML Development Team
-Author-email: [email protected]
+Author-email: [email protected]
 License: GPL-2.0+
 Classifier: Development Status :: 2 - Pre-Alpha
 Classifier: Environment :: Console
@@ -12,12 +12,13 @@
 Classifier: License :: OSI Approved :: GNU General Public License (GPL)
 Classifier: Operating System :: OS Independent
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Topic :: Software Development :: Compilers
-Requires-Python: >=3.5
+Requires-Python: >=3.10
 License-File: LICENSE
 Requires-Dist: Pillow>=3.4
 Dynamic: author
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/README.md new/nml-0.9.0/README.md
--- old/nml-0.8.1/README.md     2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/README.md     2026-04-16 00:54:52.000000000 +0200
@@ -26,7 +26,8 @@
 ## 1) Contact
 
 - [issue tracker / source repository](https://github.com/OpenTTD/nml)
-- IRC chat using #openttd on irc.oftc.net [more info about our irc 
channel](https://wiki.openttd.org/Irc)
+- IRC chat using #openttd on irc.oftc.net [more info about our IRC 
channel](https://wiki.openttd.org/en/Development/IRC%20channel)
+- The Official OpenTTD [Discord](https://discord.gg/openttd)
 
 ## 2) Dependencies
 
@@ -35,12 +36,13 @@
 NML requires the following 3rd party packages to run:
 
 - `python`
-  Minimal version is 3.5. Python 2 is not supported.
+  Minimum version is 3.10*. Python 2 is not supported.
+  <sub><sup>Python versions before 3.10 might still work, but for developing 
NML, Python version 3.10 or newer is recommended.</sub></sup>
 - `python image library`
-  For install options see 
https://pillow.readthedocs.io/en/stable/installation.html
-  Minimal version is 3.4. Older versions are not supported.
+  For install options see [Pillow: Basic 
Installation](https://pillow.readthedocs.io/en/stable/installation/basic-installation.html)
+  <sub><sup>Minimum version is 3.4. Older versions are not 
supported.</sub></sup>
 - `ply`
-  Downloadable from http://www.dabeaz.com/ply/
+  NML comes bundled with ply version 2022.10.27, located in the `ply` folder 
inside the `nml` folder.
 
 ### 2.2) Optional dependencies
 
@@ -57,16 +59,26 @@
 
 ## 3) Installation
 
-The easiest way to install NML is by using pip:
+The easiest way to install NML is by using pip or 
[uv](https://docs.astral.sh/uv/):
 
 ```bash
-pip3 install nml
+python3 -m pip install nml
+```
+or
+```bash
+uv tool install nml
 ```
 
 In order to install NML from a source checkout run:
 
 ```bash
-python setup.py install
+python3 -m pip install .
+```
+
+If you want to install it in editable mode, so that changes to the code are 
instantly applied without reinstalling NML, run:
+
+```bash
+python3 -m pip install -e .
 ```
 
 If you want to install the package manually copy 'nmlc' to any directory
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/docs/changelog.txt 
new/nml-0.9.0/docs/changelog.txt
--- old/nml-0.8.1/docs/changelog.txt    2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/docs/changelog.txt    2026-04-16 00:54:52.000000000 +0200
@@ -1,3 +1,21 @@
+0.9.0 (2026-04-15)
+------------------------------------------------------------------------
+This release sets python 3.10 as minimum version, and adds support for badges 
and push-pull trains.
+
+Support for NewGRF additions of OpenTTD 16:
+ - Add: Flag for allow unpowered wagons to lead a train when backing up (#420)
+ - Add: Variable for when a train is driving backwards (#421)
+ - Change: distinguish perimeter from area in station distributed cargo flag. 
(#422)
+
+Support for NewGRF additions of OpenTTD 15:
+ - Add: Support for NewGRF badges. (#359)
+ - Change: add support for vehicle var 0x65 (#378)
+
+Other changes and fixes:
+ - Fix: Feature 0x14 missing from extract tables. (#403)
+ - Fix #407: missing position for some action0 errors (#408)
+
+
 0.8.1 (2025-12-02)
 ------------------------------------------------------------------------
 Support for NewGRF additions of OpenTTD 15:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/__version__.py 
new/nml-0.9.0/nml/__version__.py
--- old/nml-0.8.1/nml/__version__.py    2025-12-02 20:17:28.000000000 +0100
+++ new/nml-0.9.0/nml/__version__.py    2026-04-16 00:54:57.000000000 +0200
@@ -1,2 +1,2 @@
 # this file is autogenerated by setup.py
-version = "0.8.1"
+version = "0.9.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/actions/action0.py 
new/nml-0.9.0/nml/actions/action0.py
--- old/nml-0.8.1/nml/actions/action0.py        2025-12-02 20:17:25.000000000 
+0100
+++ new/nml-0.9.0/nml/actions/action0.py        2026-04-16 00:54:52.000000000 
+0200
@@ -211,6 +211,7 @@
     BlockAllocation(0, 62, "Roadtype"),
     BlockAllocation(0, 62, "Tramtype"),
     BlockAllocation(0, 0xFFFE, "RoadStop"),  # UINT16_MAX - 1
+    BlockAllocation(0, 64000, "Badge"),
 ]
 
 
@@ -780,6 +781,26 @@
         return len(self.id_list) * 4 + 1
 
 
+class StringListProp(BaseAction0Property):
+    def __init__(self, prop_num, string_list):
+        self.prop_num = prop_num
+        self.string_list = string_list
+
+    def write(self, file):
+        file.print_bytex(self.prop_num)
+        for i, string_val in enumerate(self.string_list):
+            if i > 0 and i % 5 == 0:
+                file.newline()
+            file.print_string(string_val.value, True, True)
+        file.newline()
+
+    def get_size(self):
+        size = 1
+        for i, string_val in enumerate(self.string_list):
+            size += grfstrings.get_string_size(string_val.value, True, True)
+        return size
+
+
 def get_cargolist_action(cargo_list):
     action0 = Action0(0x08, 0)
     action0.prop_list.append(IDListProp(0x09, cargo_list))
@@ -787,6 +808,22 @@
     return [action0]
 
 
+def get_badgelist_action(badge_list):
+    index = 0
+    actions = []
+    while index < len(badge_list):
+        last = min(index + 250, len(badge_list))
+
+        action0 = Action0(0x08, index)
+        action0.prop_list.append(StringListProp(0x18, badge_list[index:last]))
+        action0.num_ids = last - index
+        actions.append(action0)
+
+        index = last
+
+    return actions
+
+
 def get_tracktypelist_action(table_prop_id, cond_tracktype_not_defined, 
tracktype_list):
     action6.free_parameters.save()
     act6 = action6.Action6()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/actions/action0properties.py 
new/nml-0.9.0/nml/actions/action0properties.py
--- old/nml-0.8.1/nml/actions/action0properties.py      2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/actions/action0properties.py      2026-04-16 
00:54:52.000000000 +0200
@@ -15,7 +15,7 @@
 
 import itertools
 
-from nml import generic, nmlop, global_constants
+from nml import generic, grfstrings, nmlop, global_constants
 from nml.expression import (
     AcceptCargo,
     Array,
@@ -170,7 +170,7 @@
 #
 # 'required' (value doesn't matter) if the property is required for the item 
to be valid.
 
-properties = 0x15 * [None]
+properties = 0x16 * [None]
 
 #
 # Some helper functions that are used for multiple features
@@ -305,38 +305,52 @@
     Property value that is a variable-length list of variable sized values, 
the list length is written before the data.
     """
 
-    def __init__(self, prop_num, data, size, extended):
+    def __init__(self, prop_num, data, data_size, len_size):
         # 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
-        self.size = size
-        self.extended = extended
+        self.data_size = data_size
+        self.len_size = len_size
 
     def write(self, file):
         file.print_bytex(self.prop_num)
         for elem in self.data:
-            if self.extended:
-                file.print_bytex(0xFF)
-                file.print_word(len(elem))
-            else:
-                file.print_byte(len(elem))
+            file.print_varx(len(elem), self.len_size)
             for i, val in enumerate(elem):
                 if i % 8 == 0:
                     file.newline()
-                file.print_varx(val, self.size)
+                file.print_varx(val, self.data_size)
             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 + 3 or 1 for 
the length
-            total_len += len(elem) * self.size + (3 if self.extended else 1)
+            # For each item ID to set, make space for all values + len_size 
for the length
+            total_len += len(elem) * self.data_size + self.len_size
         return total_len
 
 
-def VariableByteListProp(prop_num, data, extended=False):
-    return VariableListProp(prop_num, data, 1, extended)
+def VariableByteListProp(prop_num, data, len_size=1):
+    return VariableListProp(prop_num, data, 1, len_size)
+
+
+class StringProp(BaseAction0Property):
+    """
+    Property value that is zero-terminated string.
+    """
+
+    def __init__(self, prop_num, string):
+        self.prop_num = prop_num
+        self.string = string
+
+    def write(self, file):
+        file.print_bytex(self.prop_num)
+        file.print_string(self.string.value, True, True)
+        file.newline()
+
+    def get_size(self):
+        return grfstrings.get_string_size(self.string.value) + 1
 
 
 def ctt_list(prop_num, *values):
@@ -353,8 +367,40 @@
     ]
 
 
-def VariableWordListProp(num_prop, data, extended=False):
-    return VariableListProp(num_prop, data, 2, extended)
+def VariableWordListProp(num_prop, data, len_size=1):
+    return VariableListProp(num_prop, data, 2, len_size)
+
+
+def badge_list(prop_num, *values):
+    # values may have multiple entries, if more than one item ID is set
+    # Each value is an expression.Array of badge labels
+
+    table = global_constants.badge_numbers
+
+    for value in values:
+        if not isinstance(value, Array):
+            raise generic.ScriptError("Value of badgelist property must be an 
array", value.pos)
+
+        for badge in value.values:
+            if not isinstance(badge, StringLiteral) or badge.value not in 
table:
+                raise generic.ScriptError(
+                    "Parameter for badges must be a string literal that is 
also in your badge table", value.pos
+                )
+
+    return [
+        VariableListProp(
+            prop_num,
+            [[table[badge.value] for badge in single_item_array.values] for 
single_item_array in values],
+            2,
+            2,
+        )
+    ]
+
+
+def string_property(prop_num, value):
+    if not isinstance(value, StringLiteral):
+        raise generic.ScriptError("Value of label property must be a 
StringLiteral", value.pos)
+    return [StringProp(prop_num, value)]
 
 
 def accepted_cargos(prop_num, *values):
@@ -395,7 +441,7 @@
             return value if index == 0 else None
         if isinstance(value, Array) and len(value.values) == 2 and 
isinstance(value.values[index], ConstantNumeric):
             return value.values[index]
-        raise generic.ScriptError("refittable_cargo_classes must be a constant 
or an array of 2 constants")
+        raise generic.ScriptError("refittable_cargo_classes must be a constant 
or an array of 2 constants", value.pos)
 
     def prop_test(value):
         return value is not None
@@ -419,7 +465,7 @@
         if len(value.values) == 1:
             return [Action0Property(single_num, 
value.values[0].reduce_constant(), 1)]
         return [VariableByteListProp(multiple_num, 
[[type.reduce_constant().value for type in value.values]])]
-    raise generic.ScriptError("'{}' must be a constant or an array of 
constants".format(prop_name))
+    raise generic.ScriptError("'{}' must be a constant or an array of 
constants".format(prop_name), value.pos)
 
 
 #
@@ -490,6 +536,7 @@
     "curve_speed_mod":                {"size": 2, "num": 0x2E, 
"unit_conversion": 256},
     "variant_group":                  {"size": 2, "num": 0x2F},
     "extra_flags":                    {"size": 4, "num": 0x30},
+    "badges":                         {"custom_function": lambda value: 
badge_list(0x33, value)},
 }
 # fmt: on
 
@@ -568,6 +615,7 @@
     ],
     "variant_group":                {"size": 2, "num": 0x26},
     "extra_flags":                  {"size": 4, "num": 0x27},
+    "badges":                       {"custom_function": lambda value: 
badge_list(0x2A, value)},
 }
 # fmt: on
 
@@ -658,6 +706,7 @@
     "variant_group":                {"size": 2, "num": 0x20},
     "extra_flags":                  {"size": 4, "num": 0x21},
     "acceleration":                 {"size": 1, "num": 0x24},
+    "badges":                       {"custom_function": lambda value: 
badge_list(0x26, value)},
 }
 # fmt: on
 
@@ -719,6 +768,7 @@
     "range":                        {"size": 2, "num": 0x1F},
     "variant_group":                {"size": 2, "num": 0x20},
     "extra_flags":                  {"size": 4, "num": 0x21},
+    "badges":                       {"custom_function": lambda value: 
badge_list(0x24, value)},
 }
 # fmt: on
 
@@ -782,6 +832,8 @@
     for layout in value.values:
         if not isinstance(layout, Array) or len(layout.values) == 0:
             raise generic.ScriptError("A station layout must be an array of 
platforms", layout.pos)
+        if not isinstance(layout.values[0], Array):
+            raise generic.ScriptError("A platform must be an array of tile 
types", layout.values[0].pos)
         length = len(layout.values[0].values)
         number = len(layout.values)
         if (length, number) in layouts:
@@ -791,7 +843,7 @@
         layouts[(length, number)] = []
         for platform in layout.values:
             if not isinstance(platform, Array) or len(platform.values) == 0:
-                raise generic.ScriptError("A platform must be an array of tile 
types")
+                raise generic.ScriptError("A platform must be an array of tile 
types", platform.pos)
             if len(platform.values) != length:
                 raise generic.ScriptError("All platforms in a station layout 
must have the same length", platform.pos)
             for type in platform.values:
@@ -811,7 +863,7 @@
     if not isinstance(value, Array) or len(value.values) % 2 != 0:
         raise generic.ScriptError("Flag list must be an array of even length", 
value.pos)
     if len(value.values) > 8:
-        return [VariableByteListProp(0x1E, [[flags.reduce_constant().value for 
flags in value.values]], True)]
+        return [VariableByteListProp(0x1E, [[flags.reduce_constant().value for 
flags in value.values]], 3)]
     pylons = 0
     wires = 0
     blocked = 0
@@ -833,7 +885,7 @@
 def station_tile_list(value, prop_num, description):
     if not isinstance(value, Array) or len(value.values) % 2 != 0:
         raise generic.ScriptError(f"{description} list must be an array of 
even length", value.pos)
-    return [VariableByteListProp(prop_num, [[x.reduce_constant().value for x 
in value.values]], True)]
+    return [VariableByteListProp(prop_num, [[x.reduce_constant().value for x 
in value.values]], 3)]
 
 
 # fmt: off
@@ -861,6 +913,7 @@
     "name":                  {"size": 2, "num": (256, -1, 0x1C), "string": 
(256, 0xC5, 0xDC), "required": True},
     "classname":             {"size": 2, "num": (256, -1, 0x1D), "string": 
(256, 0xC4, 0xDC)},
     "tile_flags":            {"custom_function": station_tile_flags},  # = 
prop 1E
+    "badges":                {"custom_function": lambda value: 
badge_list(0x1F, value)},
     "heights":               {"custom_function": lambda x: 
station_tile_list(x, 0x20, "Station height")},
     "blocked_pillars":       {"custom_function": lambda x: 
station_tile_list(x, 0x21, "Station blocked pillar")},
 }
@@ -1071,6 +1124,7 @@
         "multitile_function": mt_house_same,
         "custom_function": lambda *values: accepted_cargos(0x23, *values),
     },
+    "badges":                  {"custom_function": lambda value: 
badge_list(0x24, value)},
 }
 # fmt: on
 
@@ -1092,6 +1146,7 @@
     "animation_triggers":  {"size": 1, "num": 0x11},
     "special_flags":       {"size": 1, "num": 0x12},
     "accepted_cargos":     {"custom_function": lambda value: 
accepted_cargos(0x13, value)},
+    "badges":              {"custom_function": lambda value: badge_list(0x14, 
value)},
 }
 # fmt: on
 
@@ -1364,6 +1419,7 @@
     "nearby_station_name":    {"size": 2, "num": 0x24, "string": 0xDC},
     # prop 25+26+27+28 combined in one structure
     "cargo_types":            {"custom_function": industry_cargo_types},
+    "badges":                 {"custom_function": lambda value: 
badge_list(0x29, value)},
 }
 # fmt: on
 
@@ -1467,6 +1523,7 @@
     "noise_level":      {"size": 1, "num": 0x0F},
     "name":             {"size": 2, "num": 0x10, "string": 0xDC},
     "maintenance_cost": {"size": 2, "num": 0x11},
+    "badges":           {"custom_function": lambda value: badge_list(0x12, 
value)},
 }
 # fmt: on
 
@@ -1507,6 +1564,7 @@
     "height":                 {"size": 1, "num": 0x16},
     "num_views":              {"size": 1, "num": 0x17},
     "count_per_map256":       {"size": 1, "num": 0x18},
+    "badges":                 {"custom_function": lambda value: 
badge_list(0x19, value)},
 }
 # fmt: on
 
@@ -1553,6 +1611,7 @@
     "sort_order":           {"size": 1, "num": 0x1A},
     "name":                 {"size": 2, "num": 0x1B, "string": 0xDC},
     "maintenance_cost":     {"size": 2, "num": 0x1C},
+    "badges":               {"custom_function": lambda value: badge_list(0x1E, 
value)},
 }
 
 #
@@ -1591,6 +1650,7 @@
     "animation_info":     {"size": 2, "num": 0x0F, "value_function": 
animation_info},
     "animation_speed":    {"size": 1, "num": 0x10},
     "animation_triggers": {"size": 1, "num": 0x11},
+    "badges":             {"custom_function": lambda value: badge_list(0x12, 
value)},
 }
 
 #
@@ -1679,4 +1739,15 @@
     "heights":                   {"custom_function": lambda x: 
station_tile_list(x, 0x13, "Station height")},
     "blocked_pillars":           {"custom_function": lambda x: 
station_tile_list(x, 0x14, "Station blocked pillar")},
     "cost_multipliers":          {"custom_function": lambda x: 
byte_sequence_list(x, 0x15, "Cost multipliers", 2)},
+    "badges":                    {"custom_function": lambda value: 
badge_list(0x16, value)},
+}
+
+#
+# Feature 0x15 (Badges)
+#
+
+properties[0x15] = {
+    'label':                     {'custom_function': lambda x: 
string_property(0x08, x), "required": True},
+    'flags':                     {'size': 4, 'num': 0x09},
+    'name':                      {'num': -1, 'string': None},
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/actions/action2.py 
new/nml-0.9.0/nml/actions/action2.py
--- old/nml-0.8.1/nml/actions/action2.py        2025-12-02 20:17:25.000000000 
+0100
+++ new/nml-0.9.0/nml/actions/action2.py        2026-04-16 00:54:52.000000000 
+0200
@@ -217,8 +217,9 @@
             free_action2_ids.append(act2.id)
 
 
-# Features using sprite groups directly: vehicles, stations, canals, cargos, 
railtypes, airports, roadtypes, tramtypes
-features_sprite_group = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x0B, 0x0D, 0x10, 
0x12, 0x13]
+# Features using sprite groups directly: vehicles, stations, canals, cargos, 
railtypes, airports, roadtypes, tramtypes,
+# badges
+features_sprite_group = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x0B, 0x0D, 0x10, 
0x12, 0x13, 0x15]
 # Features using sprite layouts: houses, industry tiles, objects, airport 
tiles, and road stops
 features_sprite_layout = [0x07, 0x09, 0x0F, 0x11, 0x14]
 # All features that need sprite sets
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/actions/action2var_variables.py 
new/nml-0.9.0/nml/actions/action2var_variables.py
--- old/nml-0.8.1/nml/actions/action2var_variables.py   2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/actions/action2var_variables.py   2026-04-16 
00:54:52.000000000 +0200
@@ -167,6 +167,7 @@
     '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},
+    'train_is_driving_backwards'       : {'var': 0xFE, 'start': 11, 'size':  
1},
 }
 
 # Vehicle-type-specific variables
@@ -235,6 +236,9 @@
 def vehicle_tramtype(name, args, pos, info):
     return (expression.functioncall.builtin_resolve_typelabel(name, args, pos, 
table_name="tramtype"), [])
 
+def badge_parameter(name, args, pos, info):
+    return [expression.functioncall.builtin_resolve_typelabel(name, args, pos, 
table_name="badgetype"), []]
+
 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},
@@ -242,6 +246,8 @@
     '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},
+    'count_has_badge'     : {'var': 0x64, 'start':  0, 'size': 8, 
'param_function':badge_parameter},
+    'has_badge'           : {'var': 0x7A, 'start':  0, 'size': 1, 
'param_function':badge_parameter},
 }
 
 varact2vars60x_trains = {
@@ -250,6 +256,7 @@
     'tile_supports_railtype' : {'var': 0x63, 'start':  1, 'size': 1, 
'param_function':vehicle_railtype},
     'tile_powers_railtype'   : {'var': 0x63, 'start':  2, 'size': 1, 
'param_function':vehicle_railtype},
     'tile_is_railtype'       : {'var': 0x63, 'start':  3, 'size': 1, 
'param_function':vehicle_railtype},
+    'tile_has_railtype_badge': {'var': 0x65, 'start':  0, 'size': 1, 
'param_function':badge_parameter},
 }
 
 varact2vars60x_roadvehs = {
@@ -261,6 +268,8 @@
     'tile_powers_tramtype'   : {'var': 0x63, 'start':  2, 'size': 1, 
'param_function':vehicle_tramtype},
     'tile_is_roadtype'       : {'var': 0x63, 'start':  3, 'size': 1, 
'param_function':vehicle_roadtype},
     'tile_is_tramtype'       : {'var': 0x63, 'start':  3, 'size': 1, 
'param_function':vehicle_tramtype},
+    'tile_has_roadtype_badge': {'var': 0x65, 'start':  0, 'size': 1, 
'param_function':badge_parameter},
+    'tile_has_tramtype_badge': {'var': 0x65, 'start':  0, 'size': 1, 
'param_function':badge_parameter},
 }
 
 #
@@ -293,6 +302,7 @@
     '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},
+    'has_badge'                 : {'var': 0x7A, 'start': 0, 'size':  1, 
'param_function': badge_parameter},
 }
 
 varact2vars_stations = {
@@ -484,6 +494,7 @@
     '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},
+    'has_badge'                          : {'var': 0x7A, 'start':  0, 'size':  
1, 'param_function': badge_parameter},
 }
 
 #
@@ -515,6 +526,7 @@
     '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},
+    'has_badge'                    : {'var': 0x7A, 'start':  0, 'size':  1, 
'param_function': badge_parameter},
 }
 
 #
@@ -620,6 +632,7 @@
     'incoming_cargo_waiting'       : {'var': 0x6F, 'start':  0, 'size': 32, 
'param_function': industry_cargotype},
     'production_rate'              : {'var': 0x70, 'start':  0, 'size': 32, 
'param_function': industry_cargotype},
     'transported_last_month_pct'   : {'var': 0x71, 'start':  0, 'size': 32, 
'param_function': industry_cargotype, 'value_function': value_mul_div(101, 
256)},
+    'has_badge'                    : {'var': 0x7A, 'start':  0, 'size':  1, 
'param_function': badge_parameter},
 }
 
 #
@@ -686,6 +699,8 @@
 
     'object_count'                 : {'var': 0x64, 'start': 16, 'size':  8, 
'param_function': industry_count},
     'object_distance'              : {'var': 0x64, 'start':  0, 'size': 16, 
'param_function': industry_count},
+
+    'has_badge'                    : {'var': 0x7A, 'start':  0, 'size':  1, 
'param_function': badge_parameter},
 }
 
 #
@@ -703,7 +718,10 @@
     'railtype'              : {'var': 0x45, 'start': 16, 'size': 8},
     'random_bits'           : {'var': 0x5F, 'start': 8, 'size':  2},
 }
-# Railtypes have no 60+x variables
+
+varact2vars60x_railtype = {
+    'has_badge'             : {'var': 0x7A, 'start': 0, 'size':  1, 
'param_function': badge_parameter},
+}
 
 #
 # Airport tiles (feature 0x11)
@@ -730,6 +748,7 @@
     '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},
+    'has_badge'                    : {'var': 0x7A, 'start':  0, 'size':  1, 
'param_function': badge_parameter},
 }
 
 #
@@ -747,7 +766,10 @@
     'railtype'              : {'var': 0x45, 'start': 16, 'size': 8},
     'random_bits'           : {'var': 0x5F, 'start': 8, 'size':  2},
 }
-# Roadtypes have no 60+x variables
+
+varact2vars60x_roadtype = {
+    'has_badge'             : {'var': 0x7A, 'start': 0, 'size':  1, 
'param_function': badge_parameter},
+}
 
 #
 # Tramtypes (feature 0x13)
@@ -764,7 +786,10 @@
     'railtype'              : {'var': 0x45, 'start': 16, 'size': 8},
     'random_bits'           : {'var': 0x5F, 'start': 8, 'size':  2},
 }
-# Tramtypes have no 60+x variables
+
+varact2vars60x_tramtype = {
+    'has_badge'             : {'var': 0x7A, 'start': 0, 'size':  1, 
'param_function': badge_parameter},
+}
 
 
 #
@@ -848,6 +873,14 @@
     'nearby_tile_road_stop_id'          : {'var': 0x6B, 'start':  0, 'size': 
16, 'param_function': signed_tile_offset},
 }
 
+#
+# Badges (feature 0x15)
+#
+
+varact2vars_badges = {
+    'intro_date'             : {'var': 0x40, 'start':  0, 'size': 32},
+}
+
 class VarAct2Scope:
     def __init__(self, name, vars_normal, vars_60x, 
has_persistent_storage=False):
         self.name = name
@@ -882,11 +915,12 @@
 scope_soundeffects = VarAct2Scope("SoundEffects", {}, {})
 scope_airports = VarAct2Scope("Airports", varact2vars_airports, 
varact2vars60x_airports, has_persistent_storage=True)
 scope_objects = VarAct2Scope("Objects", varact2vars_objects, 
varact2vars60x_objects)
-scope_railtypes = VarAct2Scope("RailTypes", varact2vars_railtype, {})
+scope_railtypes = VarAct2Scope("RailTypes", varact2vars_railtype, 
varact2vars60x_railtype)
 scope_airporttiles = VarAct2Scope("AirportTiles", varact2vars_airporttiles, 
varact2vars60x_airporttiles)
-scope_roadtypes = VarAct2Scope("RoadTypes", varact2vars_roadtype, {})
-scope_tramtypes = VarAct2Scope("TramTypes", varact2vars_tramtype, {})
+scope_roadtypes = VarAct2Scope("RoadTypes", varact2vars_roadtype, 
varact2vars60x_roadtype)
+scope_tramtypes = VarAct2Scope("TramTypes", varact2vars_tramtype, 
varact2vars60x_tramtype)
 scope_roadstops = VarAct2Scope("RoadStops", varact2vars_roadstop, 
varact2vars60x_roadstop)
+scope_badges = VarAct2Scope("Badges", varact2vars_badges, {})
 
 varact2features = [
     VarAct2Feature(scope_trains, scope_trains),
@@ -910,4 +944,5 @@
     VarAct2Feature(scope_roadtypes, None),
     VarAct2Feature(scope_tramtypes, None),
     VarAct2Feature(scope_roadstops, scope_towns),
+    VarAct2Feature(scope_badges, None),
 ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/actions/action3_callbacks.py 
new/nml-0.9.0/nml/actions/action3_callbacks.py
--- old/nml-0.8.1/nml/actions/action3_callbacks.py      2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/actions/action3_callbacks.py      2026-04-16 
00:54:52.000000000 +0200
@@ -15,7 +15,7 @@
 
 from nml import nmlop
 
-callbacks = 0x15 * [{}]
+callbacks = 0x16 * [{}]
 
 # Possible values for 'purchase':
 # 0 (or not set): not called from purchase list
@@ -320,3 +320,23 @@
     'default'         : {'type': 'cargo', 'num': None},
     'purchase'        : {'type': 'cargo', 'num': 0xFF},
 }
+
+# Badges
+callbacks[0x15] = {
+    'trains'          : {'type': 'cargo', 'num': 0x00},
+    'roadvehs'        : {'type': 'cargo', 'num': 0x01},
+    'ships'           : {'type': 'cargo', 'num': 0x02},
+    'aircraft'        : {'type': 'cargo', 'num': 0x03},
+    'stations'        : {'type': 'cargo', 'num': 0x04},
+    'houses'          : {'type': 'cargo', 'num': 0x07},
+    'industrytiles'   : {'type': 'cargo', 'num': 0x09},
+    'industries'      : {'type': 'cargo', 'num': 0x0A},
+    'airports'        : {'type': 'cargo', 'num': 0x0D},
+    'objects'         : {'type': 'cargo', 'num': 0x0F},
+    'railtypes'       : {'type': 'cargo', 'num': 0x10},
+    'airporttiles'    : {'type': 'cargo', 'num': 0x11},
+    'roadtypes'       : {'type': 'cargo', 'num': 0x12},
+    'tramtypes'       : {'type': 'cargo', 'num': 0x13},
+    'roadstops'       : {'type': 'cargo', 'num': 0x14},
+    'default'         : {'type': 'cargo', 'num': None},
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/actions/action4.py 
new/nml-0.9.0/nml/actions/action4.py
--- old/nml-0.8.1/nml/actions/action4.py        2025-12-02 20:17:25.000000000 
+0100
+++ new/nml-0.9.0/nml/actions/action4.py        2026-04-16 00:54:52.000000000 
+0200
@@ -199,7 +199,7 @@
     else:
         # Not a string range, so we must have an id
         assert id is not None
-        size = 3 if feature <= 3 else 1
+        size = 3 if (feature <= 3 or feature == 21) else 1
         if isinstance(id, expression.ConstantNumeric):
             id_val = id.value
         else:
@@ -207,7 +207,7 @@
             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)
+            mod = (tmp_param, 2 if (feature <= 3 or feature == 21) else 1, 5 
if (feature <= 3 or feature == 21) else 4)
 
     if write_action4s:
         strings = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/ast/badgetable.py 
new/nml-0.9.0/nml/ast/badgetable.py
--- old/nml-0.8.1/nml/ast/badgetable.py 1970-01-01 01:00:00.000000000 +0100
+++ new/nml-0.9.0/nml/ast/badgetable.py 2026-04-16 00:54:52.000000000 +0200
@@ -0,0 +1,52 @@
+__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 action0
+from nml.ast import base_statement
+
+
+class BadgeTable(base_statement.BaseStatement):
+    def __init__(self, badge_list, pos):
+        base_statement.BaseStatement.__init__(self, "badge table", pos, False, 
False)
+        self.badge_list = badge_list
+
+    def register_names(self):
+        generic.OnlyOnce.enforce(self, "badge table")
+        for i, badge in enumerate(self.badge_list):
+            if isinstance(badge, expression.Identifier):
+                self.badge_list[i] = expression.StringLiteral(badge.value, 
badge.pos)
+            if self.badge_list[i].value in global_constants.badge_numbers:
+                generic.print_warning(
+                    generic.Warning.GENERIC,
+                    "Duplicate entry in badge table: 
{}".format(self.badge_list[i].value),
+                    badge.pos,
+                )
+            else:
+                global_constants.badge_numbers[self.badge_list[i].value] = i
+
+    def debug_print(self, indentation):
+        generic.print_dbg(indentation, "Badge table")
+        for badge in self.badge_list:
+            generic.print_dbg(indentation, "Badge:", badge.value)
+
+    def get_action_list(self):
+        return action0.get_badgelist_action(self.badge_list)
+
+    def __str__(self):
+        ret = "badgetable {\n"
+        ret += ", ".join([expression.identifier_to_print(badge.value) for 
badge in self.badge_list])
+        ret += "\n}\n"
+        return ret
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/ast/general.py 
new/nml-0.9.0/nml/ast/general.py
--- old/nml-0.8.1/nml/ast/general.py    2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/ast/general.py    2026-04-16 00:54:52.000000000 +0200
@@ -38,6 +38,7 @@
     "FEAT_ROADTYPES": 0x12,
     "FEAT_TRAMTYPES": 0x13,
     "FEAT_ROADSTOPS": 0x14,
+    "FEAT_BADGES": 0x15,
 }
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/editors/extract_tables.py 
new/nml-0.9.0/nml/editors/extract_tables.py
--- old/nml-0.8.1/nml/editors/extract_tables.py 2025-12-02 20:17:25.000000000 
+0100
+++ new/nml-0.9.0/nml/editors/extract_tables.py 2026-04-16 00:54:52.000000000 
+0200
@@ -92,6 +92,7 @@
     action0properties.properties[0x11],
     action0properties.properties[0x12],
     action0properties.properties[0x13],
+    action0properties.properties[0x14],
 ]
 
 properties = set()
@@ -120,6 +121,7 @@
     action3_callbacks.callbacks[0x11],
     action3_callbacks.callbacks[0x12],
     action3_callbacks.callbacks[0x13],
+    action3_callbacks.callbacks[0x14],
 ]
 
 callbacks = set()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/expression/functioncall.py 
new/nml-0.9.0/nml/expression/functioncall.py
--- old/nml-0.8.1/nml/expression/functioncall.py        2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/expression/functioncall.py        2026-04-16 
00:54:52.000000000 +0200
@@ -470,7 +470,7 @@
     return ConstantNumeric(parse_string_to_dword(args[0]))
 
 
-@builtins("cargotype", "railtype", "roadtype", "tramtype")
+@builtins("badgetype", "cargotype", "railtype", "roadtype", "tramtype")
 def builtin_resolve_typelabel(name, args, pos, table_name=None):
     """
     {cargo,rail,road,tram}type(label) builtin functions.
@@ -478,6 +478,7 @@
     Also used from some Action2Var variables to resolve cargo labels.
     """
     tracktype_funcs = {
+        "badgetype": global_constants.badge_numbers,
         "cargotype": global_constants.cargo_numbers,
         "railtype": global_constants.railtype_table,
         "roadtype": global_constants.roadtype_table,
@@ -489,6 +490,8 @@
     table = tracktype_funcs[table_name]
     if table_name == "cargotype":
         table_name = "cargo"  # NML syntax uses "cargotable" and 
"railtypetable"
+    if table_name == "badgetype":
+        table_name = "badge"
 
     if len(args) != 1:
         raise generic.ScriptError(name + "() must have 1 parameter", pos)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/global_constants.py 
new/nml-0.9.0/nml/global_constants.py
--- old/nml-0.8.1/nml/global_constants.py       2025-12-02 20:17:25.000000000 
+0100
+++ new/nml-0.9.0/nml/global_constants.py       2026-04-16 00:54:52.000000000 
+0200
@@ -144,10 +144,18 @@
     "VEHICLE_FLAG_NO_BREAKDOWN_SMOKE": 6,
 
     # vehicle extra flags
-    "VEHICLE_FLAG_DISABLE_NEW_VEHICLE_MESSAGE"    : 0,
-    "VEHICLE_FLAG_DISABLE_EXCLUSIVE_PREVIEW"      : 1,
+    "VEHICLE_FLAG_DISABLE_NEW_VEHICLE_MESSAGE" : 0,
+    "VEHICLE_FLAG_DISABLE_EXCLUSIVE_PREVIEW" : 1,
     "VEHICLE_FLAG_SYNC_VARIANT_EXCLUSIVE_PREVIEW" : 2,
-    "VEHICLE_FLAG_SYNC_VARIANT_RELIABILITY"       : 3,
+    "VEHICLE_FLAG_SYNC_VARIANT_RELIABILITY" : 3,
+    "VEHICLE_FLAG_TRAIN_HAS_CAB" : 4,
+
+    # badge flags
+    "BADGE_FLAG_COPY_TO_RELATED_ENTITY" : 0,
+    "BADGE_FLAG_NAME_LIST_STOP" : 1,
+    "BADGE_FLAG_NAME_LIST_FIRST_ONLY" : 2,
+    "BADGE_FLAG_USE_COMPANY_COLOUR" : 3,
+    "BADGE_FLAG_NAME_SKIP" : 4,
 
     # Graphic flags for waterfeatures
     "WATERFEATURE_ALTERNATIVE_SPRITES" : 0,
@@ -372,10 +380,12 @@
     "CB_RANDOM_TRIGGER"        : 0x01,
 
     # station general flags
-    "STAT_FLAG_DISTRIBUTED_CARGO"     : 1,
+    "STAT_FLAG_DISTRIBUTED_CARGO_BY_PERIMETER" : 1,
+    "STAT_FLAG_DISTRIBUTED_CARGO" : "STAT_FLAG_DISTRIBUTED_CARGO_BY_PERIMETER",
     "STAT_FLAG_RANDOM_ANIMATION"      : 2,
     "STAT_FLAG_CUSTOM_FOUNDATIONS"    : 3,
     "STAT_FLAG_EXTENDED_FOUNDATIONS"  : 4,
+    "STAT_FLAG_DISTRIBUTED_CARGO_BY_AREA" : 5,
 
     # station tile flags
     "STAT_TILE_PYLON"   : 0,
@@ -1437,6 +1447,7 @@
 
 
 cargo_numbers = {}
+badge_numbers = {}
 
 is_default_railtype_table = True
 # if no railtype_table is provided, OpenTTD assumes these 3 railtypes
@@ -1484,6 +1495,7 @@
     (patch_variables, patch_variable),
     (named_parameters, param_from_name),
     cargo_numbers,
+    badge_numbers,
     railtype_table,
     roadtype_table,
     tramtype_table,
@@ -1504,6 +1516,8 @@
     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 len(badge_numbers) > 0:
+        generic.print_info("Badge translation table: 
{}/{}".format(len(badge_numbers), 0xFFFF))
     if not is_default_railtype_table:
         generic.print_info("Railtype translation table: 
{}/{}".format(len(railtype_table), 0x100))
     if not is_default_roadtype_table:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/parser.py new/nml-0.9.0/nml/parser.py
--- old/nml-0.8.1/nml/parser.py 2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/parser.py 2026-04-16 00:54:52.000000000 +0200
@@ -13,13 +13,14 @@
 with NML; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""
 
-from .ply import yacc
+from nml.ply import yacc
 
 from nml import expression, generic, nmlop, tokens, unit
 from nml.actions import actionD, real_sprite
 from nml.ast import (
     alt_sprites,
     assignment,
+    badgetable,
     base_graphics,
     basecost,
     cargotable,
@@ -120,6 +121,7 @@
         | template_declaration
         | tilelayout
         | town_names
+        | badgetable
         | cargotable
         | railtype
         | roadtype
@@ -741,6 +743,21 @@
         "disable_item : DISABLE_ITEM LPAREN expression_list RPAREN SEMICOLON"
         t[0] = disable_item.DisableItem(t[3], t.lineno(1))
 
+    def p_badgetable(self, t):
+        """badgetable : BADGETABLE LBRACE badgetable_list RBRACE
+        | BADGETABLE LBRACE badgetable_list COMMA RBRACE"""
+        t[0] = badgetable.BadgeTable(t[3], t.lineno(1))
+
+    def p_badgetable_list(self, t):
+        """badgetable_list : ID
+        | STRING_LITERAL
+        | badgetable_list COMMA ID
+        | badgetable_list COMMA STRING_LITERAL"""
+        if len(t) == 2:
+            t[0] = [t[1]]
+        else:
+            t[0] = t[1] + [t[3]]
+
     def p_cargotable(self, t):
         """cargotable : CARGOTABLE LBRACE cargotable_list RBRACE
         | CARGOTABLE LBRACE cargotable_list COMMA RBRACE"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/spriteencoder.py 
new/nml-0.9.0/nml/spriteencoder.py
--- old/nml-0.8.1/nml/spriteencoder.py  2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/spriteencoder.py  2026-04-16 00:54:52.000000000 +0200
@@ -297,7 +297,7 @@
             if im.mode == "RGBA":
                 info_byte |= INFO_ALPHA
 
-            (im_width, im_height) = im.size
+            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)
@@ -320,7 +320,7 @@
             im_mask_pal = palette.validate_palette(mask_im, 
filename_8bpp.value)
             info_byte |= INFO_PAL
 
-            (im_width, im_height) = mask_im.size
+            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)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/tokens.py new/nml-0.9.0/nml/tokens.py
--- old/nml-0.8.1/nml/tokens.py 2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/tokens.py 2026-04-16 00:54:52.000000000 +0200
@@ -16,7 +16,7 @@
 import re
 import sys
 
-from .ply import lex
+from nml.ply import lex
 
 from nml import expression, generic
 
@@ -26,6 +26,7 @@
     "var":                 "VARIABLE",
     "param":               "PARAMETER",
     "cargotable":          "CARGOTABLE",
+    "badgetable":          "BADGETABLE",
     "railtypetable":       "RAILTYPETABLE",
     "roadtypetable":       "ROADTYPETABLE",
     "tramtypetable":       "TRAMTYPETABLE",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml/version_info.py 
new/nml-0.9.0/nml/version_info.py
--- old/nml-0.8.1/nml/version_info.py   2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/nml/version_info.py   2026-04-16 00:54:52.000000000 +0200
@@ -21,19 +21,16 @@
     versions = {}
     # PIL
     try:
-        import PIL
+        from PIL import __version__ as pil_version
 
-        versions["PIL"] = PIL.__version__
+        versions["PIL"] = pil_version
     except ImportError:
         versions["PIL"] = "Not found!"
 
     # PLY
-    try:
-        from ply import lex
+    from nml.ply import __version__ as ply_version
 
-        versions["PLY"] = lex.__version__
-    except ImportError:
-        versions["PLY"] = "Not found!"
+    versions["PLY"] = ply_version
 
     return versions
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml.egg-info/PKG-INFO 
new/nml-0.9.0/nml.egg-info/PKG-INFO
--- old/nml-0.8.1/nml.egg-info/PKG-INFO 2025-12-02 20:17:28.000000000 +0100
+++ new/nml-0.9.0/nml.egg-info/PKG-INFO 2026-04-16 00:54:57.000000000 +0200
@@ -1,10 +1,10 @@
 Metadata-Version: 2.4
 Name: nml
-Version: 0.8.1
+Version: 0.9.0
 Summary: An OpenTTD NewGRF compiler for the nml language
 Home-page: https://github.com/OpenTTD/nml
 Author: NML Development Team
-Author-email: [email protected]
+Author-email: [email protected]
 License: GPL-2.0+
 Classifier: Development Status :: 2 - Pre-Alpha
 Classifier: Environment :: Console
@@ -12,12 +12,13 @@
 Classifier: License :: OSI Approved :: GNU General Public License (GPL)
 Classifier: Operating System :: OS Independent
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Topic :: Software Development :: Compilers
-Requires-Python: >=3.5
+Requires-Python: >=3.10
 License-File: LICENSE
 Requires-Dist: Pillow>=3.4
 Dynamic: author
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/nml.egg-info/SOURCES.txt 
new/nml-0.9.0/nml.egg-info/SOURCES.txt
--- old/nml-0.8.1/nml.egg-info/SOURCES.txt      2025-12-02 20:17:28.000000000 
+0100
+++ new/nml-0.9.0/nml.egg-info/SOURCES.txt      2026-04-16 00:54:57.000000000 
+0200
@@ -118,6 +118,7 @@
 nml/ast/__init__.py
 nml/ast/alt_sprites.py
 nml/ast/assignment.py
+nml/ast/badgetable.py
 nml/ast/base_graphics.py
 nml/ast/base_statement.py
 nml/ast/basecost.py
@@ -214,6 +215,7 @@
 regression/039_storage.nml
 regression/040_station.nml
 regression/041_articulated_tram_32bpp.nml
+regression/042_badges.nml
 regression/Makefile
 regression/arctic_railwagons.pcx
 regression/beef.wav
@@ -313,6 +315,8 @@
 regression/expected/040_station.nfo
 regression/expected/041_articulated_tram_32bpp.grf
 regression/expected/041_articulated_tram_32bpp.nfo
+regression/expected/042_badges.grf
+regression/expected/042_badges.nfo
 regression/expected/example_industry.grf
 regression/expected/example_industry.nfo
 regression/expected/example_object.grf
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/regression/042_badges.nml 
new/nml-0.9.0/regression/042_badges.nml
--- old/nml-0.8.1/regression/042_badges.nml     1970-01-01 01:00:00.000000000 
+0100
+++ new/nml-0.9.0/regression/042_badges.nml     2026-04-16 00:54:52.000000000 
+0200
@@ -0,0 +1,72 @@
+grf {
+    grfid: "NML\42";
+    name: string(STR_REGRESSION_NAME);
+    desc: string(STR_REGRESSION_DESC);
+    version: 0;
+    min_compatible_version: 0;
+}
+
+badgetable {
+    "flag/GB",
+    "flag/US",
+    "power/steam",
+    "power/diesel",
+    "power/electric",
+}
+
+switch (FEAT_TRAINS, SELF, sw_can_attach_wagon, has_badge("power/electric")) {
+    1: return CB_RESULT_ATTACH_ALLOW;
+    return string(STR_NO_BADGE);
+}
+
+item (FEAT_TRAINS, default_train, 8) {
+    property {
+        badges: ["flag/GB", "power/steam"];
+    }
+    graphics {
+        can_attach_wagon: sw_can_attach_wagon;
+    }
+}
+
+item (FEAT_ROADVEHS, default_roadveh, 0) {
+    property {
+        badges: ["flag/US", "power/diesel"];
+    }
+}
+
+item (FEAT_BADGES, power) {
+    property {
+        label: "power";
+        name: string(STR_POWER);
+    }
+}
+
+template tmpl_truck(x) {
+    [ 96,56, 28,15,  -14, -8]
+}
+
+spriteset(sprite_steam, ZOOM_LEVEL_NORMAL, BIT_DEPTH_32BPP, 
"opengfx_generic_trams1.png") { tmpl_truck(0) }
+
+item (FEAT_BADGES, steam) {
+    property {
+        label: "power/steam";
+        name: string(STR_POWER_STEAM);
+    }
+    graphics {
+        default: sprite_steam;
+    }
+}
+
+item (FEAT_BADGES, diesel) {
+    property {
+        label: "power/diesel";
+        name: string(STR_POWER_DIESEL);
+    }
+}
+
+item (FEAT_BADGES, electric) {
+    property {
+        label: "power/electric";
+        name: string(STR_POWER_ELECTRIC);
+    }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/regression/expected/013_train_callback.nfo 
new/nml-0.9.0/regression/expected/013_train_callback.nfo
--- old/nml-0.8.1/regression/expected/013_train_callback.nfo    2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/regression/expected/013_train_callback.nfo    2026-04-16 
00:54:52.000000000 +0200
@@ -233,7 +233,7 @@
 1D \dx00000000
 29 \wx01CB
 1D \dx00000000
-2C \b4
+2C 04
 00 01 0C 0D
 1D \dx00000000
 15 13
@@ -250,7 +250,7 @@
 1C 28
 05 00
 05 00
-34 \b2
+34 02
 00 01
 0B \wx0000
 0E \dx00004C30
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/nml-0.8.1/regression/expected/017_articulated_tram.nfo 
new/nml-0.9.0/regression/expected/017_articulated_tram.nfo
--- old/nml-0.8.1/regression/expected/017_articulated_tram.nfo  2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/regression/expected/017_articulated_tram.nfo  2026-04-16 
00:54:52.000000000 +0200
@@ -45,7 +45,7 @@
 16 \dx00000000
 1E \wx0000
 16 \dx00000000
-24 \b0
+24 00
 16 \dx00000000
 10 FF
 1C 01
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/regression/expected/030_house.nfo 
new/nml-0.9.0/regression/expected/030_house.nfo
--- old/nml-0.8.1/regression/expected/030_house.nfo     2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/regression/expected/030_house.nfo     2026-04-16 
00:54:52.000000000 +0200
@@ -265,13 +265,13 @@
 19 00 00 00 00
 0B 64 00 00 00
 0C 19 00 00 00
-23 \b4
+23 04
 \wx0802 \wx0803 \wx0200 \wx0101
-\b4
+04
 \wx0802 \wx0803 \wx0200 \wx0101
-\b4
+04
 \wx0802 \wx0803 \wx0200 \wx0101
-\b4
+04
 \wx0802 \wx0803 \wx0200 \wx0101
 10 \wx00C8 \wx00C8 \wx00C8 \wx00C8
 11 FA FA FA FA
@@ -285,13 +285,13 @@
 16 00 00 00 00
 1A 94 94 94 94
 1B 02 02 02 02
-20 \b2
+20 02
 02 03
-\b2
+02
 02 03
-\b2
+02
 02 03
-\b2
+02
 02 03
 
 42 * 9 00 07 \b1 01 FF \wx0000
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/regression/expected/040_station.nfo 
new/nml-0.9.0/regression/expected/040_station.nfo
--- old/nml-0.8.1/regression/expected/040_station.nfo   2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/regression/expected/040_station.nfo   2026-04-16 
00:54:52.000000000 +0200
@@ -26,7 +26,7 @@
 13 18
 12 \dx00000002
 0C F0
-1E FF \w10
+1E FF \wx000A
 00 01 02 03 04 05 06 07
 02 05
 0E \b1 \b1
@@ -35,9 +35,9 @@
 04 04
 06 06
 \b0 \b0
-20 FF \w8
+20 FF \wx0008
 01 01 02 02 03 03 04 04
-21 FF \w8
+21 FF \wx0008
 0C 03 0C 03 9C 63 9C 63
 
 6 * 11 04 04 FF 01 \wxC4FF "Test" 00
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/nml-0.8.1/regression/expected/041_articulated_tram_32bpp.nfo 
new/nml-0.9.0/regression/expected/041_articulated_tram_32bpp.nfo
--- old/nml-0.8.1/regression/expected/041_articulated_tram_32bpp.nfo    
2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/regression/expected/041_articulated_tram_32bpp.nfo    
2026-04-16 00:54:52.000000000 +0200
@@ -45,7 +45,7 @@
 16 \dx00000000
 1E \wx0000
 16 \dx00000000
-24 \b0
+24 00
 16 \dx00000000
 10 FF
 1C 01
Binary files old/nml-0.8.1/regression/expected/042_badges.grf and 
new/nml-0.9.0/regression/expected/042_badges.grf differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/regression/expected/042_badges.nfo 
new/nml-0.9.0/regression/expected/042_badges.nfo
--- old/nml-0.8.1/regression/expected/042_badges.nfo    1970-01-01 
01:00:00.000000000 +0100
+++ new/nml-0.9.0/regression/expected/042_badges.nfo    2026-04-16 
00:54:52.000000000 +0200
@@ -0,0 +1,94 @@
+// Automatically generated by GRFCODEC. Do not modify!
+// (Info version 32)
+// 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>>
+// Escapes: 71 70 7= 7! 7< 7> 7G 7g 7gG 7GG 7gg 7c 7C
+// Escapes: D= = DR D+ = DF D- = DC Du* = DM D* = DnF Du<< = DnC D<< = DO D& 
D| Du/ D/ Du% D%
+// Format: spritenum imagefile depth xpos ypos xsize ysize xrel yrel zoom flags
+
+0 * 4 \d24
+
+1 * 54 14 "C" "INFO"
+"B" "VRSN" \w4 \dx00000000
+"B" "MINV" \w4 \dx00000000
+"B" "NPAR" \w1 00
+"B" "PALS" \w1 "A"
+"B" "BLTR" \w1 "3"
+00
+00
+2 * 52 08 08 "NML\42" "NML regression test" 00 "A test newgrf testing NML" 00
+3 * 43 04 00 FF 01 \wxD000 "Can only attach power/steam vehicles" 00
+
+4 * 64 00 08 \b1 05 FF \wx0000
+18 "flag/GB" 00 "flag/US" 00 "power/steam" 00 "power/diesel" 00 
"power/electric" 00
+
+// Name: sw_can_attach_wagon
+5 * 24 02 00 FF 89
+7A 04 00 \dx00000001
+\b1
+\wx8401 \dx00000001 \dx00000001        // 1 .. 1: return 1025;
+\wx8000 // default: return string(STR_NO_BADGE);
+
+6 * 14 00 00 \b1 01 FF \wx0008
+33 \wx0002
+\wx0000 \wx0002
+
+7 * 6 01 00 \b1 FF \wx0000
+
+// Name: @CB_FAILED_REAL00
+8 * 9 02 00 FE \b1 \b1
+\w0
+\w0
+
+// Name: @CB_FAILED00
+9 * 23 02 00 FE 89
+0C 00 \dx0000FFFF
+\b1
+\wx8000 \dx00000000 \dx00000000        // graphics callback -> return 0
+\wx00FE // Non-graphics callback, return graphics result
+
+// Name: @action3_0
+10 * 23 02 00 FE 89
+0C 00 \dx0000FFFF
+\b1
+\wx00FF \dx0000001D \dx0000001D        // sw_can_attach_wagon;
+\wx00FE // @CB_FAILED00;
+
+11 * 9 03 00 01 FF \wx0008 \b0
+\wx00FE        // @action3_0;
+
+12 * 14 00 01 \b1 01 FF \wx0000
+2A \wx0002
+\wx0001 \wx0003
+
+13 * 14 00 15 \b1 01 FF \wx0000
+08 "power" 00
+
+14 * 13 04 15 7F 01 FF \wx0000 "Power" 00
+
+15 * 20 00 15 \b1 01 FF \wx0001
+08 "power/steam" 00
+
+16 * 13 04 15 7F 01 FF \wx0001 "Steam" 00
+
+17 * 6 01 15 \b1 FF \wx0001
+
+18 opengfx_generic_trams1.png 32bpp 96 56 28 15 -14 -8 normal
+
+// Name: sprite_steam - feature 15
+19 * 7 02 15 FE \b1 \b0
+\w0
+
+
+20 * 7 03 15 01 01 \b0
+\wx00FE        // sprite_steam;
+
+21 * 21 00 15 \b1 01 FF \wx0002
+08 "power/diesel" 00
+
+22 * 14 04 15 7F 01 FF \wx0002 "Diesel" 00
+
+23 * 23 00 15 \b1 01 FF \wx0003
+08 "power/electric" 00
+
+24 * 16 04 15 7F 01 FF \wx0003 "Electric" 00
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/regression/expected/example_industry.nfo 
new/nml-0.9.0/regression/expected/example_industry.nfo
--- old/nml-0.8.1/regression/expected/example_industry.nfo      2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/regression/expected/example_industry.nfo      2026-04-16 
00:54:52.000000000 +0200
@@ -86,11 +86,11 @@
 13 * 27 00 0A \b6 01 FF \wx0000
 08 06
 09 06
-25 \b2
+25 02
 09 05
-26 \b3
+26 03
 01 08 06
-27 \b2
+27 02
 00 00
 28 \b0 \b0
 14 * 11 00 0A \b2 01 FF \wx0000
@@ -138,9 +138,9 @@
 22 * 26 00 0A \b6 01 FF \wx0001
 08 09
 09 09
-25 \b3
+25 03
 04 06 07
-26 \b0
-27 \b3
+26 00
+27 03
 08 0C 04
 28 \b0 \b0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/nml-0.8.1/regression/expected/example_road_vehicle.nfo 
new/nml-0.9.0/regression/expected/example_road_vehicle.nfo
--- old/nml-0.8.1/regression/expected/example_road_vehicle.nfo  2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/regression/expected/example_road_vehicle.nfo  2026-04-16 
00:54:52.000000000 +0200
@@ -196,10 +196,10 @@
 16 \dx00000000
 1E \wx0081
 16 \dx00000000
-24 \b6
+24 06
 00 01 02 03 04 05
 16 \dx00000000
-25 \b0
+25 00
 16 \dx00000000
 07 05
 11 6C
@@ -295,10 +295,10 @@
 16 \dx00000000
 1E \wx0081
 16 \dx00000000
-24 \b6
+24 06
 00 01 02 03 04 05
 16 \dx00000000
-25 \b0
+25 00
 16 \dx00000000
 07 05
 11 6C
@@ -394,10 +394,10 @@
 16 \dx00000000
 1E \wx0081
 16 \dx00000000
-24 \b6
+24 06
 00 01 02 03 04 05
 16 \dx00000000
-25 \b0
+25 00
 16 \dx00000000
 07 05
 11 6C
@@ -493,10 +493,10 @@
 16 \dx00000000
 1E \wx0081
 16 \dx00000000
-24 \b6
+24 06
 00 01 02 03 04 05
 16 \dx00000000
-25 \b0
+25 00
 16 \dx00000000
 07 05
 11 6C
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/regression/expected/example_train.nfo 
new/nml-0.9.0/regression/expected/example_train.nfo
--- old/nml-0.8.1/regression/expected/example_train.nfo 2025-12-02 
20:17:25.000000000 +0100
+++ new/nml-0.9.0/regression/expected/example_train.nfo 2026-04-16 
00:54:52.000000000 +0200
@@ -305,9 +305,9 @@
 1D \dx00000000
 29 \wx0000
 1D \dx00000000
-2C \b0
+2C 00
 1D \dx00000000
-2D \b0
+2D 00
 1D \dx00000000
 07 06
 17 2D
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/regression/lang/english.lng 
new/nml-0.9.0/regression/lang/english.lng
--- old/nml-0.8.1/regression/lang/english.lng   2025-12-02 20:17:25.000000000 
+0100
+++ new/nml-0.9.0/regression/lang/english.lng   2026-04-16 00:54:52.000000000 
+0200
@@ -43,3 +43,9 @@
 STR_032_HOUSE                  :Example house
 
 STR_JUST_STRING                :{STRING}
+
+STR_NO_BADGE                   :Can only attach power/steam vehicles
+STR_POWER                      :Power
+STR_POWER_STEAM                :Steam
+STR_POWER_DIESEL               :Diesel
+STR_POWER_ELECTRIC             :Electric
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nml-0.8.1/setup.py new/nml-0.9.0/setup.py
--- old/nml-0.8.1/setup.py      2025-12-02 20:17:25.000000000 +0100
+++ new/nml-0.9.0/setup.py      2026-04-16 00:54:52.000000000 +0200
@@ -39,18 +39,19 @@
         "License :: OSI Approved :: GNU General Public License (GPL)",
         "Operating System :: OS Independent",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.5",
-        "Programming Language :: Python :: 3.6",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
+        "Programming Language :: Python :: 3.13",
+        "Programming Language :: Python :: 3.14",
         "Topic :: Software Development :: Compilers",
     ],
     url="https://github.com/OpenTTD/nml";,
     author="NML Development Team",
-    author_email="[email protected]",
+    author_email="[email protected]",
     entry_points={"console_scripts": ["nmlc = nml.main:run"]},
     ext_modules=[Extension("nml_lz77", ["nml/_lz77.c"], optional=True)],
-    python_requires=">=3.5",
+    python_requires=">=3.10",
     install_requires=[
         "Pillow>=3.4",
     ],

Reply via email to