-
Notifications
You must be signed in to change notification settings - Fork 106
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
Add template support #170
Add template support #170
Conversation
This change really makes me wonder whether I made some horrible design decisions, and whether it couldn't all be a lot simpler when we could make a few changes to the data format. If you have any ideas about this, please let me know so we could potentially simplify this in future versions of Tiled. Unfortunately, I won't have time to give this a thorough review until next week. One thing sticks out to me, which is the choice to make the templates invisible to the user. That sounds very convenient, but eventually I still have the idea that |
@bjorn If you ever do consider a new format, please count me in in the new specs team. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO ParseContext
should not be defined in this PR, as it is largely unrelated to templates. Yes, it simplifies things, but it also made reviewing the PR much harder, and it has many issues unrelated to the addition of templates. I'd suggest reverting these changes; I'd be happy to review a parse context PR after this one has been merged so we can get the big picture.
src/layers/tile/mod.rs
Outdated
@@ -15,13 +16,22 @@ mod util; | |||
pub use finite::*; | |||
pub use infinite::*; | |||
|
|||
/// The locatation of the tileset this tile is in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in location
src/layers/tile/mod.rs
Outdated
/// valid index of the map tileset container, but **isn't guaranteed to actually contain | ||
/// this tile**. | ||
tileset_index: usize, | ||
/// A valid TilesetLocation that contains this tile but **only if it is the tileset's range of tiles**. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// A valid TilesetLocation that contains this tile but **only if it is the tileset's range of tiles**. | |
/// A valid TilesetLocation that points to a tileset that **may or may not contain** this tile. |
src/objects.rs
Outdated
let s: String = template; | ||
let template_path = PathBuf::from(s); | ||
|
||
let template_path = parse_context.require_path("Template")?.join(template_path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let template_path = parse_context.require_path("Template")?.join(template_path); | |
let template_path = parse_context.require_path("Template")?.join(Path::new(s)); |
src/objects.rs
Outdated
let mut template_id: Option<usize> = None; | ||
|
||
// If the template attribute is there, we need to go fetch the template file | ||
if let Some(template) = template { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try returning template_id from this scope instead of using a mutable variable for it.
src/objects.rs
Outdated
@@ -95,11 +129,23 @@ impl ObjectData { | |||
"properties" => |_| { | |||
properties = parse_properties(parser)?; | |||
Ok(()) | |||
}, | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated change
src/template.rs
Outdated
pub(crate) fn new_external<CacheTy: ResourceCache>( | ||
path: &Path, | ||
parse_context: &mut ParseContext<CacheTy>, | ||
) -> Result<usize, TiledError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a new
function, it isn't very intuitive to see that it returns an usize
. It definitely should not insert itself into the context, but rather return the resulting template to be inserted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So there's an issue here in that tiles contained in a Template need to be able to store a reference of some kind to the template (so they can identify their tileset). I could have the Tile contain an Arc to either the Tileset or the Template, but that would make the Tile not be Copy, and may have performance implications.
I decided to copy how it was done with Tilesets, where during parsing a list of tilesets is generated, and tiles store an index into the list. For templates though, the parsing of the template's tile is done during parsing of the template itself, so the template needs to know its own id to tell the tile.
I agree this is a bit of a mess though, and will gladly change it if there is a better way. And yeah, the function names are not idiomatic, sorry!
src/template.rs
Outdated
// First, check if this template already exists in our list of templates (it will be uniquely identified by its | ||
// path). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not be done here... This creates a very tight binding between Template
and ParseContext
. The constructor should instead return the template, which then can be inserted into the cache externally. This is also more flexible since it allows loading templates without adding them to the cache (If we want to expose templates at some point, for instance)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should return Result<Template, TiledError>
.
src/template.rs
Outdated
err, | ||
})?; | ||
|
||
let mut tileset_parser = EventReader::new(file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tileset parser?
src/template.rs
Outdated
parse_context.templates.push(Template { | ||
path: path.to_path_buf(), | ||
tileset: None, | ||
object: None, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this shouldn't be here; These are changes you wouldn't expect from a function called parse_external_template
.
src/tile.rs
Outdated
@@ -47,15 +48,15 @@ impl Tile { | |||
let mut animation = None; | |||
parse_tag!(parser, "tile", { | |||
"image" => |attrs| { | |||
image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse:"Image".to_owned()})?)?); | |||
image = Some(Image::new(parser, attrs, &parse_context.require_path("Image")?)?); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't related to the addition of templates.
27a7a9a
to
e727499
Compare
I didn't really want to add it here as well, but we need to pass a lot more information around, and to a lot of locations. Specifically to parse templates we need:
Templates can be specified any time we need to process an Object, which comes up in a lot of places (although I guess we could just not support templates in more obscure places, like tilesets). Templates also introduce a new "context" which affects which tileset a tile is linked to (not the ones in the main map), which is another piece of information that needs to be propogated up and down the call stack. I'm open to ideas on how we can improve it, but I can't see any solution that doesn't involve giving each function 2-3 more arguments. Using a struct to store this information just makes it more cleaner IMO. I can refactor things so that arguments are passed directly between functions if that is what you prefer. |
From my (limited) experience of working with Tiled files, I think the format is pretty solid. I've found templates a really useful addition for managing object properties. The complexity with this MR is more with managing file locations and in-process parsing, neither of which I think are indicative of problems in the file format. With regards to the templates being resolved "automatically". It should be simple enough to change things so that objects store a template index number, and properties (x, y, width, etc.) on ObjectData being optional (which is more faithful to the original data in the file anyway). The getter methods would check for the data on the ObjectData, then the template, and finally a default value. I think if the library is automatically decoding and calculating gids to tileset indexes, it makes sense for it to also automatically parse and understand templates as well. |
It's okay, we'll deal with those later. What is important right now is the feature; refactors can come later down the line, which also allows us to think more about them as we have more information about the interface at our disposal. |
Alright, that's great to hear!
Rather than making each value optional, in Tiled there is a class member with bitflags indicating which values have been "overridden". In addition to being less fragmented, this also allows copying down values from the template so access can be more direct. Of course that does mean that when editing the template, the changes need to be copied down to all instances. Anyway, we can worry about that later, when we get around to #171.
Yes, that is definitely helpful. |
e727499
to
9a6c2f4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! This is definitely much better. However, there are still a couple of important issues that need to be tackled.
src/objects.rs
Outdated
object_to_parse: "Template".to_string(), | ||
})? | ||
.parent() | ||
.unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should return an error instead; I believe there was an InvalidPath
error or similar in the interface.
src/tileset.rs
Outdated
@@ -113,10 +136,23 @@ impl Tileset { | |||
pub(crate) fn parse_xml_in_map( | |||
parser: &mut impl Iterator<Item = XmlEventResult>, | |||
attrs: Vec<OwnedAttribute>, | |||
map_path: &Path, | |||
map_path: &Path, // Template or Map file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uuuuuuhm...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops! What should I call it? The paths are tracked in multiple places and now it can be a map, template or (I think) a tilesheet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name root_path
has appeared elsewhere, and I guess it would be appropriate here as well. However, I don't really agree with the term "root", and would also be fine with just path
(and would assume this path does not include the file name, actually).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function should be renamed to something that doesn't specify maps (e.g. parse_xml_in_file) and the path parameter could just be file_path then.
BTW, you don't need to force push on each commit; In fact, it's better for us that you keep the original commits so we can see more easily what's changed between each review. Don't worry, the commits will all be squeezed down to a single one when merging with master, so the PR history can be as messy as you want (Still, be a bit careful :) ) Thanks! |
This patch adds support for templates. Templates are a property on Objects that specify an external file to load object properties by default. In this implementation, templates are not visible to the end user, their behaviour happens "automatically". As it parses, the Map will generate a list of templates, with each being identified by their ID. Templates introduce the concept of a tile that is bound to a tileset that is not part of the main map's list of tilesets. For this reason, the tileset_index() function now returns Option<usize>. Internally, an enum is used to signify whether the tileset is map or template bound. I had to change the signature of the Resource cache, because the generator function now needs access to the cache.
9a6c2f4
to
6cf613c
Compare
Please consider doing this, otherwise I need to review the entire PR instead of the changed parts. |
Sorry about the force pushing, I didn't see your message before pushing that last one! I guess I'm used to gitlab where rewriting history is handled much more egonomically. You can click the secret hidden link on the word "force-pushed" to see the diff introduced by that commit, but it's nowhere near as good as gitlab's "change history thing". Anyway, I rushed it a bit, so there may be small mistakes I need to clean up. |
src/template.rs
Outdated
impl Template { | ||
pub(crate) fn parse_and_append_template( | ||
path: &Path, | ||
templates: &mut Vec<Template>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be in the cache, not here.
src/template.rs
Outdated
// First, check if this template already exists in our list of templates (it will be uniquely identified by its | ||
// path). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should return Result<Template, TiledError>
.
src/template.rs
Outdated
.find(|(_, template)| template.path == path) | ||
{ | ||
return Ok(id); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be moved to the caller using get_template
and then insert_template
.
src/template.rs
Outdated
|
||
fn parse_and_append_external_template( | ||
parser: &mut impl Iterator<Item = XmlEventResult>, | ||
_attrs: &Vec<OwnedAttribute>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the attributes aren't being used, they shouldn't be passed to the function.
src/template.rs
Outdated
let res = Tileset::parse_xml_in_map(parser, attrs, template_path, templates, Some(template_id), cache)?; | ||
match res.result_type { | ||
EmbeddedParseResultType::ExternalReference { tileset_path } => { | ||
let tileset = if let Some(ts) = cache.get_tileset(&tileset_path) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use Option::unwrap_or_else
here... Or cache.get_or_try_insert_tileset
.
So it seems there's a design issue here. Tiles must be able to identify which template they are "from" (so they can do the automatic gid resolving). I implemented this as an index into a vector that is collected as we parse, but that seems to be not the design you are looking for (which makes sense, I don't really like it either). Also complicating things is the fact that tiles inside templates can themselves include templates (Tiles can contain ObjectGroups). Any suggestions on how I can refactor that so it's less messy? |
src/template.rs
Outdated
pub(crate) struct Template { | ||
pub path: PathBuf, | ||
pub tileset: Option<Arc<Tileset>>, | ||
pub object: Option<ObjectData>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The object
attribute is mandatory, it just wasn't specified in the docs (oops): mapeditor/tiled@8f1162d
Yes; If you instead put templates in the cache, then the templates vector won't be needed and as such the template constructor won't need to insert itself anywhere (as the tiles it creates can do that themselves). This would imply making templates public, but I don't see how that is an issue, seeing how sharing templates between maps is a pretty common occurrence. |
What would LayerTileData::tileset_index be in that case? |
It can be the path of the template, as tiles in templates are uncommon enough to not have any impact on the memory and performance footprint. |
That'd also remove the dependency of the map when accessing the tileset of a template's tile. However, maybe for consistency's sake a Vec of Arc to the map templates can be stored in the map, just like tilesets, and then we could have tiles store the template index in the map. Without looking at the code (I'm on phone rn) I believe that is what your current impl is doing |
From the two options the second one sounds better, as you'd have access to the templates a map is referencing to and overall it'd be more consistent. |
In Tiled, the |
Neeeeat. Will finish the PR ASAP then. |
That should be it. Ready for final review. |
I'm more worried about the external interface and assumptions taken, the actual implementation can be upgraded later on without issues. |
/// # fn main() -> Result<()> { | ||
/// let mut loader = Loader::new(); | ||
/// let path = "assets/tilesheet.tsx"; | ||
/// | ||
/// assert!(loader.cache().get_tileset(path).is_none()); | ||
/// loader.load_tmx_map("assets/tiled_base64_external.tmx"); | ||
/// let tileset = Arc::new(loader.load_tsx_tileset(path)?); | ||
/// loader.cache_mut().insert_tileset(path, tileset); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe these changes should be done in a different PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a few minor suggestions and some questions.
@@ -22,6 +20,9 @@ impl GroupLayerData { | |||
infinite: bool, | |||
map_path: &Path, | |||
tilesets: &[MapTilesetGid], | |||
for_tileset: Option<Arc<Tileset>>, | |||
reader: &mut impl ResourceReader, | |||
cache: &mut impl ResourceCache, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that in many places we're passing around both a reader and a cache. Would it make sense to pass around a loader instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, Loader owns the reader/cache
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does that pose a problem for passing loader: &mut Loader
instead of the above two parameters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still has some internal quirks, but we'll deal with them later. Last review!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I found a few things could be improved about the actual inheritance from the template.
Thank you @RossBrunton and sorry for the wait! |
No problem, thanks for taking over this and improving it. |
* Added template support This patch adds support for templates. Templates are a property on Objects that specify an external file to load object properties by default. In this implementation, templates are not visible to the end user, their behaviour happens "automatically". As it parses, the Map will generate a list of templates, with each being identified by their ID. Templates introduce the concept of a tile that is bound to a tileset that is not part of the main map's list of tilesets. For this reason, the tileset_index() function now returns Option<usize>. Internally, an enum is used to signify whether the tileset is map or template bound. I had to change the signature of the Resource cache, because the generator function now needs access to the cache. * Handle error on template base_path.parent() * LayerTileData object now can contain a templ. arc Rather than an id into some list, this now contains an arc to the tileset directly. Makes the code cleaner, and allows caching to behave better, but can have performance implications. * Removed some more template list Vecs These should have been removed last commit, whoops! * Responded to review comments * Don't bind attributes property to a name Squishes an "unused" warning. * Cargo fmt and updated test * Update changelog * Clippy fixes * Simplify LayerTileData and create ObjectTileData * Fix clippy warnings * Simplify implementation * Apply suggestions from code review * Remove outdated comment * Address PR comments * Trigger CI & handle review comment Co-authored-by: alexdevteam <[email protected]> Co-authored-by: Thorbjørn Lindeijer <[email protected]> Co-authored-by: aleokdev <[email protected]>
* Added template support This patch adds support for templates. Templates are a property on Objects that specify an external file to load object properties by default. In this implementation, templates are not visible to the end user, their behaviour happens "automatically". As it parses, the Map will generate a list of templates, with each being identified by their ID. Templates introduce the concept of a tile that is bound to a tileset that is not part of the main map's list of tilesets. For this reason, the tileset_index() function now returns Option<usize>. Internally, an enum is used to signify whether the tileset is map or template bound. I had to change the signature of the Resource cache, because the generator function now needs access to the cache. * Handle error on template base_path.parent() * LayerTileData object now can contain a templ. arc Rather than an id into some list, this now contains an arc to the tileset directly. Makes the code cleaner, and allows caching to behave better, but can have performance implications. * Removed some more template list Vecs These should have been removed last commit, whoops! * Responded to review comments * Don't bind attributes property to a name Squishes an "unused" warning. * Cargo fmt and updated test * Update changelog * Clippy fixes * Simplify LayerTileData and create ObjectTileData * Fix clippy warnings * Simplify implementation * Apply suggestions from code review * Remove outdated comment * Address PR comments * Trigger CI & handle review comment Co-authored-by: alexdevteam <[email protected]> Co-authored-by: Thorbjørn Lindeijer <[email protected]> Co-authored-by: aleokdev <[email protected]>
Whew, this was a bigger change than I thought it would be. I've made a few assumptions about the best way to design and implement things, so feel free to let me know if it should be restructured any.
Relates to #145
======
This patch adds support for templates. Templates are a property on Objects
that specify an external file to load object properties by default. In
this implementation, templates are not visible to the end user, their
behaviour happens "automatically".
As it parses, the Map will generate a list of templates, with each being
identified by their ID.
Templates introduce the concept of a tile that is bound to a tileset
that is not part of the main map's list of tilesets. For this reason,
the tileset_index() function now returns Option. Internally, an
enum is used to signify whether the tileset is map or template bound.
I had to change the signature of the Resource cache, because the
generator function now needs access to the cache.