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

feat!: turning List<Property> into CustomProperties #55

Merged
merged 9 commits into from Oct 31, 2022
11 changes: 9 additions & 2 deletions packages/tiled/lib/src/common/property.dart
Expand Up @@ -33,14 +33,21 @@ class Property {
}

extension PropertiesParser on Parser {
List<Property> getProperties() {
return formatSpecificParsing(
Map<String, Property> getProperties() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we minimize the breaking change by adding a new getPropertiesMap() that returns Map<String, Property> and making getProperties() return the values iterable from that map?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think anyone uses this getProperties method directly, but rather the properties public variable in Tile Object Layer etc.

We definitely could change the API to:

class Tile {
  ...

  Iterable<Property> get properties => propertiesByName.values;
  final Map<String, Property> propertiesByName;
}

But personally I think that's a worse API and there is really no value in an Iterable<Property> accessor, especially when you can get it via .values.

I was under the impression that we're in early stages with this library and want to do large refactors to polish the API before 1.0 so that's why I was leaning towards the more aggressive refactor. But, I'm happy either way 🤷

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think anyone uses this getProperties method directly, but rather the properties public variable in Tile Object Layer etc.

I get what you mean, but this is still just an assumption. If it is a public API, it is always better to assume that it is used by someone. Also, even if getProperties is not used by anyone, we are still breaking properties.

But personally I think that's a worse API and there is really no value in an Iterable<Property> accessor, especially when you can get it via .values.

I was under the impression that we're in early stages with this library and want to do large refactors to polish the API before 1.0 so that's why I was leaning towards the more aggressive refactor. But, I'm happy either way 🤷

@spydon can comment about plans for 1.0, but I feel making a breaking change and trying not to bump up the major version are conflicting goals here. If you make the changes non-breaking, it will automatically solve the versioning problem. A middle ground would be to let getProperties and properties work as they used to with a @deprecated tag saying that it will be removed in 1.0.

*I might have a hidden motive for making it non-breaking, because properties is used in my simple platformer series. It will be better if maintainers decide what to do about this 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't need to have a major bump when it is breaking and sub v1. I think that we can deprecate getProperties, but not remove it until we go to past v1, properties is more inline with the dart naming convention anyways.
So then your tutorials wouldn't break for a long time.

Poke me when you feel that a new tiled + flame_version should be released btw.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, keep getProperties and properties and add getPropertiesByName and propertiesByName?

To be clear getProperties and properties are two different things, getProperties is the function that parses the data which eventually gets set on the properties class level vars.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aah right, hard to review PRs properly on the phone. Should getProperties only be used internally, or is there a value in exposing that to the user? If it's not I would name it parseProperties and mark it as @internal.
And for properties I think that we should do a breaking change on the type then and not pollute the code with unnecessary members already.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, do you want me to rename ALL the parser methods?

  • Parser.getString
  • Parser.getStringOrNull
  • Parser.getInt
  • Parser.getColor
  • etc etc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah maybe not, can't come up with any better names for them at least, can you?
Do you know why PropertiesParser is an extensions method and doesn't simply exist on the Parser class instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, I can take a look.

For the method naming, I think the classname makes it self explanatory so I'm inclined to leave it as is, what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the method naming, I think the classname makes it self explanatory so I'm inclined to leave it as is, what do you think?

Agree, missed that it was on the parser class when I reviewed on mobile.

final properties = formatSpecificParsing(
(json) => json.getChildrenAs('properties', Property.parse),
(xml) =>
xml
.getSingleChildOrNull('properties')
?.getChildrenAs('property', Property.parse) ??
[],
);

return properties.groupFoldBy((prop) => prop.name, (previous, element) {
if (previous != null) {
throw ArgumentError("Can't have two properties with the same name.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a state error

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should throw an error here. If Tiled does not even allow creation of multiple properties with same name, this case should ideally never occur. Our input condition is a valid tmx file.

In the rare case someone manually tweaks their tmx and adds multiple properties with same name, instead of failing, we should follow what Tiled does. Tiled loads the map and displays only one of the duplicate properties (the latest that it finds). We can also do that by replacing the old value with latest value when a duplicate key is found.

If we really want to go the extra mile and inform the users, it can be done using an assert.

}
return element;
});
}
}
4 changes: 2 additions & 2 deletions packages/tiled/lib/src/layer.dart
Expand Up @@ -90,7 +90,7 @@ abstract class Layer {
bool visible;

/// List of [Property].
List<Property> properties;
Map<String, Property> properties;

Layer({
this.id,
Expand All @@ -109,7 +109,7 @@ abstract class Layer {
this.tintColor,
this.opacity = 1,
this.visible = true,
this.properties = const [],
this.properties = const {},
});

static Layer parse(Parser parser) {
Expand Down
4 changes: 2 additions & 2 deletions packages/tiled/lib/src/objects/tiled_object.dart
Expand Up @@ -65,7 +65,7 @@ class TiledObject {

List<Point> polygon;
List<Point> polyline;
List<Property> properties;
Map<String, Property> properties;

/// The "Class" property, a.k.a "Type" prior to Tiled 1.9.
/// Will be same as [type].
Expand All @@ -89,7 +89,7 @@ class TiledObject {
this.visible = true,
this.polygon = const [],
this.polyline = const [],
this.properties = const [],
this.properties = const {},
});

bool get isPolyline => polyline.isNotEmpty;
Expand Down
4 changes: 2 additions & 2 deletions packages/tiled/lib/src/tiled_map.dart
Expand Up @@ -89,7 +89,7 @@ class TiledMap {
RenderOrder renderOrder;

List<EditorSetting> editorSettings;
List<Property> properties;
Map<String, Property> properties;

// only for hexagonal maps:
int? hexSideLength;
Expand Down Expand Up @@ -118,7 +118,7 @@ class TiledMap {
this.staggerAxis,
this.staggerIndex,
this.editorSettings = const [],
this.properties = const [],
this.properties = const {},
});

/// Takes a string [contents] and converts it to a [TiledMap] with the help of
Expand Down
4 changes: 2 additions & 2 deletions packages/tiled/lib/src/tileset/terrain.dart
Expand Up @@ -12,12 +12,12 @@ part of tiled;
class Terrain {
String name;
int tile;
List<Property> properties;
Map<String, Property> properties;

Terrain({
required this.name,
required this.tile,
this.properties = const [],
this.properties = const {},
});

Terrain.parse(Parser parser)
Expand Down
4 changes: 2 additions & 2 deletions packages/tiled/lib/src/tileset/tile.dart
Expand Up @@ -29,7 +29,7 @@ class Tile {
TiledImage? image;
Layer? objectGroup;
List<Frame> animation;
List<Property> properties;
Map<String, Property> properties;

Tile({
required this.localId,
Expand All @@ -39,7 +39,7 @@ class Tile {
this.image,
this.objectGroup,
this.animation = const [],
this.properties = const [],
this.properties = const {},
});

bool get isEmpty => localId == -1;
Expand Down
4 changes: 2 additions & 2 deletions packages/tiled/lib/src/tileset/tileset.dart
Expand Up @@ -60,7 +60,7 @@ class Tileset {
TiledImage? image;
TileOffset? tileOffset;
Grid? grid;
List<Property> properties = [];
Map<String, Property> properties = {};
List<Terrain> terrains = [];
List<WangSet> wangSets = [];

Expand All @@ -85,7 +85,7 @@ class Tileset {
this.image,
this.tileOffset,
this.grid,
this.properties = const [],
this.properties = const {},
this.terrains = const [],
this.wangSets = const [],
this.version = '1.0',
Expand Down
4 changes: 2 additions & 2 deletions packages/tiled/lib/src/tileset/wang/wang_color.dart
Expand Up @@ -19,14 +19,14 @@ class WangColor {
int tile;
double probability;

List<Property> properties;
Map<String, Property> properties;

WangColor({
required this.name,
required this.color,
required this.tile,
this.probability = 0,
this.properties = const [],
this.properties = const {},
});

WangColor.parse(Parser parser)
Expand Down
4 changes: 2 additions & 2 deletions packages/tiled/lib/src/tileset/wang/wang_set.dart
Expand Up @@ -21,15 +21,15 @@ class WangSet {
List<WangColor> cornerColors;
List<WangColor> edgeColors;
List<WangTile> wangTiles;
List<Property> properties;
Map<String, Property> properties;

WangSet({
required this.name,
required this.tile,
this.cornerColors = const [],
this.edgeColors = const [],
this.wangTiles = const [],
this.properties = const [],
this.properties = const {},
});

factory WangSet.parse(Parser parser) {
Expand Down
10 changes: 5 additions & 5 deletions packages/tiled/test/map_test.dart
Expand Up @@ -41,13 +41,13 @@ void main() {
Tile(localId: 0),
Tile(
localId: 2,
properties: [
Property(
properties: {
'name': Property(
name: 'name',
type: PropertyType.string,
value: 'value',
value: 'tile2-prop-value',
),
],
},
),
],
),
Expand All @@ -66,7 +66,7 @@ void main() {
final tile = map.tileByGid(7);

expect(tile?.localId, equals(2));
expect(tile?.properties.first.name, equals('name'));
expect(tile?.properties['name']!.value, equals('tile2-prop-value'));
});
});

Expand Down
30 changes: 21 additions & 9 deletions packages/tiled/test/parser_test.dart
Expand Up @@ -79,11 +79,11 @@ void main() {
});

group('populates its properties correctly and', () {
late List<Property> properties;
late Map<String, Property> properties;
setUp(() => properties = tileset.properties);
test('has a key of "test_property" = "test_value"', () {
expect(properties[0].name, equals('test_property'));
expect(properties[0].value, equals('test_value'));
expect(properties.values.first.name, equals('test_property'));
expect(properties['test_property']!.value, equals('test_value'));
});
});

Expand All @@ -105,18 +105,30 @@ void main() {
});

group('populates its child tile properties correctly by', () {
late List<Property> tile1Properties;
late List<Property> tile2Properties;
late Map<String, Property> tile1Properties;
late Map<String, Property> tile2Properties;
setUp(() {
tile1Properties = tileset.tiles[0].properties;
tile2Properties = tileset.tiles[1].properties;
});

test('inserting properties into tileProperties based on Tile GID', () {
expect(tile1Properties[0].name, equals('tile_0_property_name'));
expect(tile1Properties[0].value, equals('tile_0_property_value'));
expect(tile2Properties[0].name, equals('tile_1_property_name'));
expect(tile2Properties[0].value, equals('tile_1_property_value'));
expect(
tile1Properties['tile_0_property_name']!.name,
equals('tile_0_property_name'),
);
expect(
tile1Properties['tile_0_property_name']!.value,
equals('tile_0_property_value'),
);
expect(
tile2Properties['tile_1_property_name']!.name,
equals('tile_1_property_name'),
);
expect(
tile2Properties['tile_1_property_name']!.value,
equals('tile_1_property_value'),
);
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/tiled/test/tile_test.dart
Expand Up @@ -23,6 +23,6 @@ void main() {

test('Tile.properties is present', () {
final tile = Tile(localId: -1);
expect(tile.properties, isA<List>());
expect(tile.properties, isA<Map<String, Property>>());
});
}
2 changes: 1 addition & 1 deletion packages/tiled/test/tileset_test.dart
Expand Up @@ -17,7 +17,7 @@ void main() {
test('spacing == 0', () => expect(tileset.spacing, equals(0)));
test('margin == 0', () => expect(tileset.margin, equals(0)));
test('tileProperties == {}', () {
expect(tileset.properties, equals(<Property>[]));
expect(tileset.properties, equals(<String, Property>{}));
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/tiled/test/tmx_object_test.dart
Expand Up @@ -48,8 +48,8 @@ void main() {

test('sets properties', () {
final props = tiledObject.properties;
expect(props[0].name, equals('property_name'));
expect(props[0].value, equals('property_value'));
expect(props['property_name']!.name, equals('property_name'));
expect(props['property_name']!.value, equals('property_value'));
});

test('sets isEllipse to true', () => expect(tiledObject.ellipse, isTrue));
Expand Down