Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change: Use extended Action1s #309

Merged
merged 2 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 84 additions & 89 deletions nml/actions/action1.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,7 @@
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
from nml.actions import base_action, real_sprite, action2


class Action1(base_action.BaseAction):
Expand All @@ -30,27 +23,43 @@ class Action1(base_action.BaseAction):
@ivar feature: Feature of this action1
@type feature: C{int}

@ivar first_set: Number of the first sprite set in this action 1.
@type first_set: 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):
def __init__(self, feature, first_set, num_sets, num_ent):
self.feature = feature
self.first_set = first_set
self.num_sets = num_sets
self.num_ent = num_ent

def write(self, file):
# <Sprite-number> * <Length> 01 <feature> <num-sets> <num-ent>
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()
if self.first_set == 0 and self.num_sets < 256:
# <Sprite-number> * <Length> 01 <feature> <num-sets> <num-ent>
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()
else:
# <Sprite-number> * <Length> 01 <feature> 00 <first_set> <num-sets> <num-ent>
file.start_sprite(12)
file.print_bytex(1)
file.print_bytex(self.feature)
file.print_bytex(0)
file.print_varx(self.first_set, 3)
file.print_varx(self.num_sets, 3)
file.print_varx(self.num_ent, 3)
file.newline()
file.end_sprite()


class SpritesetCollection(base_action.BaseAction):
Expand All @@ -61,6 +70,9 @@ class SpritesetCollection(base_action.BaseAction):
@ivar feature: The feature number the action1 will get.
@type feature: C{int}

@ivar first_set: Number of the first sprite set in this action 1.
@type first_set: C{int}

@ivar num_sprites_per_spriteset: The number of sprites in each spriteset.
@type num_sprites_per_spriteset: C{int}

Expand All @@ -71,10 +83,12 @@ class SpritesetCollection(base_action.BaseAction):
@type spritesets: C{dict} mapping L{SpriteSet} to C{int}.
"""

def __init__(self, feature, num_sprites_per_spriteset):
def __init__(self, feature, first_set, num_sprites_per_spriteset):
self.feature = feature
self.first_set = first_set
self.num_sprites_per_spriteset = num_sprites_per_spriteset
self.spritesets = {}
self.max_id = 0x3FFF if feature in action2.features_sprite_layout else 0xFFFF

def skip_action7(self):
return False
Expand All @@ -85,41 +99,33 @@ def skip_action9(self):
def skip_needed(self):
return False

def can_add(self, spritesets, feature):
def can_add(self, spriteset):
"""
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}
@param spriteset: The spriteset to test for addition.
@type spriteset: L{SpriteSet}

@return: True iff the given spritesets can be added to this collection.
@return: True iff the given spriteset can be added to this collection.
@rtype: C{bool}
"""
assert len(spritesets) <= max_sprite_block_size
if feature != self.feature:
assert self.first_set + 1 <= self.max_id
if len(real_sprite.parse_sprite_data(spriteset)) != self.num_sprites_per_spriteset:
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
return self.first_set + len(self.spritesets) + (1 if spriteset not in self.spritesets else 0) <= self.max_id

def add(self, spritesets):
def add(self, spriteset):
"""
Add a list of spritesets to this collection.
Add a spriteset to this collection.

@param spritesets: The list of spritesets to add.
@type spritesets: C{list} of L{SpriteSet}
@param spriteset: The spriteset to add.
@type spriteset: L{SpriteSet}

@pre: can_add(spritesets, self.feature).
@pre: can_add(spriteset).
"""
assert self.can_add(spritesets, self.feature)
for spriteset in spritesets:
if spriteset not in self.spritesets:
self.spritesets[spriteset] = len(self.spritesets)
assert self.can_add(spriteset)
if spriteset not in self.spritesets:
self.spritesets[spriteset] = len(self.spritesets)

def get_index(self, spriteset):
"""
Expand All @@ -132,7 +138,7 @@ def get_index(self, spriteset):
collection via #add.
"""
assert spriteset in self.spritesets
return self.spritesets[spriteset]
return self.first_set + self.spritesets[spriteset]

def get_action_list(self):
"""
Expand All @@ -142,7 +148,7 @@ def get_action_list(self):
@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)]
actions = [Action1(self.feature, self.first_set, len(self.spritesets), self.num_sprites_per_spriteset)]
for idx in range(len(self.spritesets)):
for spriteset, spriteset_offset in self.spritesets.items():
if idx == spriteset_offset:
Expand All @@ -151,35 +157,13 @@ def get_action_list(self):
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
spriteset_collections = {}


def add_to_action1(spritesets, feature, pos):
Expand All @@ -202,29 +186,36 @@ def add_to_action1(spritesets, feature, pos):
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)
global spriteset_collections
if feature not in spriteset_collections:
spriteset_collections[feature] = [
SpritesetCollection(feature, 0, len(real_sprite.parse_sprite_data(spritesets[0])))
]
actions.append(spriteset_collections[feature][-1])

current_collection = spriteset_collections[feature][-1]
for spriteset in spritesets:
for spriteset_collection in spriteset_collections[feature]:
if spriteset in spriteset_collection.spritesets:
continue
if not current_collection.can_add(spriteset):
spriteset_collections[feature].append(
SpritesetCollection(
feature,
current_collection.first_set + len(current_collection.spritesets),
len(real_sprite.parse_sprite_data(spriteset)),
)
)
current_collection = spriteset_collections[feature][-1]
actions.append(current_collection)
current_collection.add(spriteset)

return actions


def get_action1_index(spriteset):
def get_action1_index(spriteset, feature):
Fixed Show fixed Hide fixed
"""
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
Expand All @@ -234,11 +225,17 @@ def get_action1_index(spriteset):
@param spriteset: The spriteset to get the index of.
@type spriteset: L{SpriteSet}.

@param feature: Feature of the spriteset.
@type feature: C{int}

@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)
assert feature in spriteset_collections
for spriteset_collection in spriteset_collections[feature]:
if spriteset in spriteset_collection.spritesets:
return spriteset_collection.get_index(spriteset)
assert False


def make_cb_failure_action1(feature):
Expand All @@ -253,10 +250,8 @@ def make_cb_failure_action1(feature):
@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:
if feature in spriteset_collections:
actions = []
else:
last_spriteset_collection = None
actions = [Action1(feature, 1, 0)]
actions = [Action1(feature, 0, 1, 0)]
return (actions, 0) # Index is currently always 0, but will change with ext. A1
4 changes: 2 additions & 2 deletions nml/actions/action2layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def resolve_spritegroup_ref(self, sg_ref):
"""
spriteset = action2.resolve_spritegroup(sg_ref.name)
offset = self._validate_offset(sg_ref.param_list, spriteset, sg_ref.pos)
num = action1.get_action1_index(spriteset)
num = action1.get_action1_index(spriteset, self.feature)
generic.check_range(num, 0, (1 << 14) - 1, "sprite", sg_ref.pos)
return expression.ConstantNumeric(num), offset

Expand Down Expand Up @@ -662,7 +662,7 @@ def append_mapping(self, mapping, feature, actions, default, custom_spritesets):
feature,
spriteset.name.value + " - feature {:02X}".format(feature),
None,
action1.get_action1_index(spriteset),
action1.get_action1_index(spriteset, feature),
)
actions.append(real_action2)
spriteset.set_action2(real_action2, feature)
Expand Down
4 changes: 2 additions & 2 deletions nml/actions/action2real.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def get_real_action2s(spritegroup, feature):
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)
action1_index = action1.get_action1_index(spriteset, feature)
if view.name.value == "loading":
loading_list.append(action1_index)
else:
Expand Down Expand Up @@ -152,7 +152,7 @@ def create_spriteset_actions(spritegroup):
feature,
spriteset.name.value + " - feature {:02X}".format(feature),
spritegroup.pos,
action1.get_action1_index(spriteset),
action1.get_action1_index(spriteset, feature),
)
action_list.append(real_action2)
spriteset.set_action2(real_action2, feature)
Expand Down
1 change: 0 additions & 1 deletion nml/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,6 @@ def nml(
action0.print_stats()
actionF.print_stats()
action7.print_stats()
action1.print_stats()
action2.print_stats()
action6.print_stats()
grf.print_stats()
Expand Down
3 changes: 0 additions & 3 deletions regression/030_house.nml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ spriteset(brewery_spriteset_ground) {
tmpl_ground_tile( 10, 10, "groundtiles.png") //bare
tmpl_ground_tile(150, 10, "groundtiles.png") //stones
tmpl_ground_tile(220, 10, "groundtiles.png") //snowed
[] /* pad with empty sprites, as all spritesets have to have the same no. of sprites (=6) */
[]
[]
}

spriteset(brewery_spriteset_building) {
Expand Down
Binary file modified regression/expected/030_house.grf
Binary file not shown.
Loading