redruin1 / factorio-draftsman

A complete, well-tested, and up-to-date module to manipulate Factorio blueprint strings. Compatible with mods.
MIT License
91 stars 17 forks source link

Will Group get a .tiles attribute like Blueprint? #118

Open tephyr opened 5 months ago

tephyr commented 5 months ago

I see that Group handle entities like Blueprint, but there is no associated TileList attached to Group.

Would love to have Group reach feature-parity with Blueprint in regards to entities & tiles, so that containment and nesting work for both types of objects.

Is this on the roadmap? Does it need a volunteer?

redruin1 commented 5 months ago

Yes, this was always something that you'd think would be available by default. The reason I haven't implemented it has been because Group has inherited quite a bit of technical debt that I want to (or at least attempt to) remedy before adding this feature.

Blueprints and Groups have an entities list which contains all of their entities and owns the data associated with them, and a entity_map which is a acceleration structure to allow quicker spatial querying of blueprints. This entity_map is primarily used for user searches within given regions, as well as issuing OverlappingEntityWarnings when needed. Because the entity_map was added after the entities list as an acceleration structure, it means that any time you add an entity to the list it has to lookup what parent Group/Blueprint the entity is being added to, and get it's entity_map from there in a roundabout way.

This was kept this way due to legacy reasons, but also the hypothetical ability to swap out the entity_map structure for something else if your use case needed it (sparse quadtree instead of spatial hash, for example), but in terms of practical reality this has basically never actually happened in all the time I've been using the module and no-one else has ever expressed desire for this functionality.

Because of this, it seems like a better design philosophy would be to integrate the acceleration structure into EntityList directly instead of having it be a member of the parent blueprintable; this way I would also be able to remove all of the clunky callbacks like on_entity_insert from these classes and the whole thing would likely be much cleaner and clearer.

If you do decide to prototype this feature, I would either base it off of the 2.0 branch or wait a little bit longer for me to get my ducks in a row. 2.0 has changed a lot from 1.0 already, but ideally shouldn't change too much more until another major version, so using that as a head will probably be more productive.

redruin1 commented 2 months ago

I've gotten much closer to implementing this feature on 2.0. However, I'm wondering if simply adding a tiles component to Group is actually the smartest way forward.

group = Group()
group.tiles.append(...)
blueprint = Blueprint()
blueprint.entities.append(group)
# Accessing `tiles` will have to go through `entities`, confusingly
blueprint.entities[0].tiles = ... # ???
# And the root blueprint `tiles` will be unintuitively empty
print(blueprint.tiles) # -> []

The next solution I can think of is to split Group into two classes, similar to EntityCollection and TileCollection:

blueprint.entities.append(EntityGroup(...)) # Only accepts EntityGroup
blueprint.tiles.append(TileGroup(...)) # Only accepts TileGroup

In addition to cluttering up the namespace, this isn't really how you think about groups; most of the time you would want to consider a "Group" a logically connected set of entities and tiles. Splitting them for technical reasons feels inelegant, even if it is closer to how Factorio stores it's information internally.

I'm wondering if a better option would be to remove the distinction between entities and tiles altogether, since most of the time it's not important to the user; only Draftsman really needs to split them into two categories when it resolves them to a blueprint string. Hypothetically, we could create a super list of content that contains entities or tiles:

blueprint = Blueprint()
blueprint.content.append("some-entity", tile_position=(0, 0))
blueprint.content.append("some-tile", tile_position=(0, 0))
print(blueprint.to_dict())
# { "blueprint": {"entities": [{"some-entity", ...}], "tiles": [{"some-tile", ...}]}}

This idea very well could be the best, but it would be a massive API shift. Furthermore, it would add additional complications to behavior (what if an entity and a tile share the same name?) that would ultimately need to be resolved. If I'm going to go further on this project, I want some more viewpoints before I invest too much time into the wrong solution.