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

Implement block states for commands and /testforblock command #579

Closed
wants to merge 9 commits into from

Conversation

aramperes
Copy link
Member

@aramperes aramperes commented Oct 22, 2017

This PR adds the system to parse and compare block state data as strings instead of numerical data values. This feature was added to some commands in 1.11, and allows using color=orange instead of the data value 1 for wool blocks, for example.

  • Implements a parsing system to read such block state strings and compare them
    • Currently only supports Wool block state as proof-of-concept, needs to be expanded to other blocks.
  • Implements the /testforblock command, which uses this new functionality.
    • Sample usage: /testforblock ~ ~-1 ~ minecraft:wool color=light_blue
  • Fixed an inconsistency with tilde notation (e.g. ~ ~-1 ~), where the relative Y value would be shifted by 0.5 blocks like the X and Z axis. Some tests needed to be fixed to pass with this correction.
  • Moved the block entity states inside the net.glowstone.block.entity.state package to avoid confusion.

I still need to add some documentation for the block state parsing system.

Related to #499

@aramperes aramperes added this to Review in 2017Q4 Oct 22, 2017
Copy link
Contributor

@smartboyathome smartboyathome left a comment

Choose a reason for hiding this comment

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

There you go, that's my feedback.


@Override
public boolean matches(BlockStateData state, Wool data) throws InvalidBlockStateException {
if (state.containsKey("color")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just checking whether the BlockStateData contains a key actually leads to the wrong results. This won't catch the case where BlockStateData contains an invalid property for this material. In this case, I would recommend copying all the keys into a new ArrayList, and then while looping over the iterator. This way, whenever a key is successfully matched, you can call iterator.remove() to remove that key from the array. The matches() method then can only return true when the keys list is empty, ensuring that this case is caught.

Copy link
Member Author

Choose a reason for hiding this comment

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

Doesn't getValidStates() solve this?


public abstract T read(Material material, BlockStateData data) throws InvalidBlockStateException;

public abstract boolean matches(BlockStateData state, T data) throws InvalidBlockStateException;
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like sticking this on the reader is a bit disjoint with how I implemented the Entity's tag matching. I'm not opposed to sticking it on the reader, but and if you want to keep it like this you should think about changing how the CompoundTag's matching works. Otherwise, I'd say you should implement the match method on the BlockStateData, similar to how CompoundTag implements its match method.


import java.util.HashMap;

public class BlockStateData extends HashMap<String, String> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Extending an implementation of Map<> seems like the wrong way to go here. In my experience, this makes it harder to refactor, especially if another implementation of map is needed, and can dilute the reason for having BlockStateData vs a regular map. It's convenient, yes, but often introduces a design smell. I usually prefer introducing the map as a field and exposing only the relevant delegate methods, making it clear what is going on.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good points, I will change this.

throw new InvalidBlockStateException(material, state);
}
BlockStateData data = new BlockStateData();
String[] split = state.trim().split(",");
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a case where I'd say a small Regex could make the expected syntax a bit clearer, and reduce the number of cases you need to check to throw InvalidBlockStateException. Looks like the regex you want is "^\\s*((\\w+)\\s*=\\s*(\\w+))\\s*(,\\s*(\\w+)\\s*=\\s*(\\w+)\\s*)*$". Looks intimidating, yes, but that is just keeping compatibility with the trimmed code above.

To use this regex, you'd be iterating through all the groups (starting at index 1 because the group at index 0 always matches the entire string), but taking only the second and third group out of every three, as the first will represent the entire "key=value" match.

Copy link
Member Author

Choose a reason for hiding this comment

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

I hadn't thought of using a regex here, I will consider it. My only concern would be readability, but that can be resolved with a comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

After checking, the regular expression you provided doesn't work all the time. For the string a=b,c=d,e=f:

Full match a=b,c=d,e=f
Group 1 a=b
Group 2 a
Group 3 b
Group 4 ,e=f
Group 5 e
Group 6 f

This would skip the middle section.

return false;
}
String itemName = args[3];
if (!itemName.startsWith("minecraft:")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not as familiar with the plugin side of things, but if a plugin introduces a new block, will it still start with "minecraft:"? If not, then this check would seem to fail when trying to test for a "foo-plugin:bar" block.

Copy link
Member Author

Choose a reason for hiding this comment

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

You can't register new blocks/items with Bukkit. The ids are registered in the ItemIds class and all start with the minecraft namespace. Adding this functionality is outside of the scope of the PR.

return false;
}
if (args.length > 4) {
String state = args[4];
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we know this same "check the data value or state argument" is used in at least three commands (clone and testforblock, and testforblocks), mind abstracting this check out into a CommandUtils method? May want to return a custom type with two properties, Integer data and BlockStateData stateData, for ease of use in these commands.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do.

}
}
if (args.length > 5 && block.getBlockEntity() != null) {
String dataTag = String.join(" ", new ArrayList<>(Arrays.asList(args)).subList(5, args.length));
Copy link
Contributor

Choose a reason for hiding this comment

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

Given we do this CompoundTag check in a few commands as well now, we might want to think about also abstracting this out into a CommandUtils method.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, but it's outside of the scope of the PR. I'll consider it once this is merged.

* Fix exception when parsing a state with no value (something=)
* Move some common state utils to CommandUtils
* Boxed map instead of extending it (BlockStateData)
@aramperes
Copy link
Member Author

I need approval to merge this for the release of 2017.10 on Wednesday. I can address the documentation and further design changes later on.

@aramperes
Copy link
Member Author

Merged in 18a3b49.

@aramperes aramperes closed this Oct 31, 2017
@aramperes aramperes deleted the states branch October 31, 2017 19:17
@aramperes aramperes moved this from Review to Completed in 2017Q4 Nov 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
2017Q4
Completed
Development

Successfully merging this pull request may close these issues.

None yet

3 participants