-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Pathfinder for new commands system #6837
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
base: major-next
Are you sure you want to change the base?
Conversation
|
Is it safe to use non-mb_* prefixed methods while parsing? Haven't looked code deeply but I saw a lot of substr and strlen used in code - wouldn't this affect to non-english commands? |
|
It should be fine. We only care about byte lengths, and splittable whitespace chars will never be more than 1 byte. |
this was a problem with cmdalias, where just writing cmdalias alone would invoke cmdalias list. In itself this wasn't particularly harmful, but it could've been a problem for other commands like timings.
Since it only compares the exact literal bytes, it's possible to have trailing characters that don't match. For example, /timings ons would crash because the 'on' overload would match, but leave a trailing 's' behind.
we only care if it's in the exact position defined by $offset.
this doesn't support enum or player name auto complete for now since the API has no way to expose that information. however, we can get some of the benefits with basic types xp <level>L is not supported because the stupid fucking client crashes when it's used and I wasted all day trying to debug it. Someone else can fix that if they care.
|
@inxomnyaa raised concerns about performance when processing subcommands. I'm not sure to what extent this is a problem, but I have seen some plugins create much more complicated command signatures than vanilla. The way that overloading works, "subcommands" can be implemented as overloads using literals in this system, but it currently has to try each one, one at a time, to find a matching overload. This could become a problem if many overloads are present. This is sometimes the case with complex plugin commands, where nested commands blow up to create a large number of overload permutations. Possibly, it might be advantageous to detect which overloads have the same first N parameters, so that if other overloads have the same leading arguments, we can eliminate multiple overloads at once in some cases without having to reparse the entire command over and over again. It might also be worth exploring whether we can avoid bruteforce lookups when some or all overloads have a literal as their first parameter(s). This would be a common case for subcommands. |
|
Optimizing overload selection shouldn't be a blocker for this PR. Plugins that are particularly concerned about huge number of overloads could always fall back to RawParameter to leverage their own optimized parsing algorithm. |
Yeah, but then they'd lose client-side command hints, which we all know is the thing plugin devs really care about, for some reason... |
|
any plan to add Adds a PlayerArgument resolves actual Player instances, not raw name strings. This Implementation:
|
|
I don't know about the tick stuff, that doesn't seem necessary to me, but yes there are plans to expand the system. I'm currently unsure how to deal with target args because of the Enums, soft (dynamic) enums, and composite types like Vector3 are also not supported yet. This PR implements the minimum of what was needed to get PM's builtin commands moved over to the new system. |
john-bv
left a comment
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.
Just wanted to leave some of my thoughts on here :)
inxomnyaa
left a comment
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.
Over all, complex, but i can't say i hate it. Good job
|
I'm considering possible options to introduce this API in PM5 to let people adopt it early, but it's not too clear how it would work. Current thinking is to introduce However, a PM6 scrapping would then mean renaming Or maybe I'm thinking too hard about this, since changing |
|
Demo of how LuckPerms command tree might be implemented using the API in this PR as of e762b12: https://gist.github.com/dktapps/57a0a2ba161592d0e18577c4e93f6264 |
…plays in usage messages
|
in my opinion, a Target parameter is necessary for the initial release of this |
|
Yeah, target and enum are on my todo list. I've been more focused on initial system architecture so far though. |
|
Overview of changes since last reviews:
|
|
Remaining items of interest:
|
there's no need for these to extend Command anymore.
|
For enums, perhaps we could just pass closures that returns a list of string into the last parameter of MappedParameter? pseudo code be like BranchingOverloadBuilder::make(commonParameters: [
new StringParameter("target", "target")
])
->executor(
["clear"],
//TODO: our permission system isn't granular enough for this right now - the permission required
//differs not by the usage, but by the target selected
self::OVERLOAD_PERMS, self::removeEffect(...),
)
->executor([
new MappedParameter("effect", "effect name", static fn(string $v) : Effect =>
StringToEffectParser::getInstance()->parse($v) ??
throw new ParameterParseException("Invalid effect name")
, static fn() => StringToEffectParser::getInstance()->getKnownAliases()),
new IntRangeParameter("duration", "duration", 0, (int) (Limits::INT32_MAX / 20)),
new IntRangeParameter("amplifier", "amplifier", 0, 255),
new BoolParameter("bubbles", "bubbles")
//TODO: our permission system isn't granular enough for this right now - the permission required
//differs not by the usage, but by the target selected
], self::OVERLOAD_PERMS, self::modifyEffect(...))
->build()in MappedParameter /**
* @tempate T of \UnitEnum
* @phpstan-param class-string<T>
*/
public static function fromEnum(
string $codeName,
Translatable|string $printableName,
string $enumClass
) : MappedParameter{
$cases = $enumClass::cases();
return new MappedParameter($codeName, $printableName, static function(string $v) use ($cases){
foreach($cases as $case){
if(strtolower($case->name) === strtolower($v)){
return $case;
}
}
throw new ParameterParseException("Invalid enum name");
}, static fn() => array_map(static fn(\UnitEnum $enum) => strtolower($enum->name), $cases));
} |
|
Would prefer to bind a mapping for it. But also ideally I'd like to implement it in such a way that cases can't suddenly become invalid later on. There's also the matter of enum constraints to consider, although what we'd use those for I'm not very clear yet. |
ClosureCommand doesn't make any sense at this point. Perhaps it never did.
|
I haven't read the code that much so I'm sorry if I'm missing something. 3 years ago, I written a virion framework Ovommand that tried to do the same and recently, I came across this repo Blockception/BC-Minecraft-Bedrock-Command that may not help you much :L I'd like to address something along my old experients, tho... these are what I came up limited by virion scope so it may not suit this well: 1. Position
2. EnumsThe way I implemented it was having enum being a dictionary where upon parsing will return the value from the key if there is any.
I just create a native enum DefaultEnums and map out all of those... enum DefaultEnums : string{
case BOOLEAN = "Boolean";
case VANILLA_GAMEMODE = "GameMode";
case PM_GAMEMODE = "PMGameMode";
case ONLINE_PLAYERS = "OnlinePlayers";
public function encode() : IDynamicEnum | IStaticEnum{
return match ($this) {
self::BOOLEAN => new HardEnum($this->value, ["true" => true, "false" => false], isProtected: true, isDefault: true),
self::PM_GAMEMODE => new HardEnum($this->value,
["survival" => GameMode::SURVIVAL, "creative" => GameMode::CREATIVE, "adventure" => GameMode::ADVENTURE, "spectator" => GameMode::SPECTATOR],
visibleAliases: ["survival" => "s", "creative" => "c", "adventure" => "a", "spectator" => "v"],
hiddenAliases: ["survival" => "0", "creative" => "1", "adventure" => "2", "spectator" => "6"],
isProtected: true, isDefault: true
),
self::VANILLA_GAMEMODE => new HardEnum($this->value,
["survival" => GameMode::SURVIVAL, "creative" => GameMode::CREATIVE, "adventure" => GameMode::ADVENTURE, "spectator" => GameMode::SPECTATOR],
["survival" => "s", "creative" => "c", "adventure" => "a"],
isProtected: true, isDefault: true
),
self::ONLINE_PLAYERS => new SoftEnum("OnlinePlayers", isProtected: true, isDefault: true)
};
}
}where it maps some handy enums such as DefaultEnums::BOOLEAN returns So in my command setup, I can register the enum parameter with this code $this->registerParameters(
new PositionParameter("a"),
new EnumParameter("aa", DefaultEnums::PM_GAMEMODE),
);
They can save those newly created classes and reuse them :> jk. As two enum parameters can have different names, I think that new may be needed. I might misunderstand your words here; you might mean the same thing as returning a new HardEnum when the DefaultEnum gets encoded.
Were those cases where they become invalid due to the item being dynamically removed? Anyway, we can use soft enums instead of hard enums for those... but we need to update the new changes via UpdateSoftEnumPacket every time the list gets changed.
Can you explain this more? I may not understand this… Why not just create two overloads, where one has the optional literal and the other doesn't? Since literals are string enums? I also noticed these kind of enums are kind of buggy? There are cases where the hints break in when you try the string enum with other datatypes. |
That's what I did, but it's double the number of usages For the enums thing, as I said, I don't want to force people (or be forced myself) to define a new enum class every time they want to add a new set of options to a command. It should be possible to use an enum if desired, but not required.
Alvin's suggestion for binding StringToItemParser to an enum would have cases magically vanish from existence if the parser mappings were modified at runtime, and the command system wouldn't have any way to know about it. We don't want to use soft enums for everything because it'll complicate things for users who just want a fixed set of options. So the enums need to capture their valid value set and ensure that the mapper accepts every value specified. Not exactly sure how that's going to work currently.
Literals are not enums in this system, not on the server side anyway. They may be sent as enums over the network, but that's kinda irrelevant. My priority here is building a system for plugin devs to use. The client side command suggestions are a happy bonus, but if they get in the way I'm not gonna cry about breaking them. Already had this headache with --flags in the style that LuckPerms uses. Plugin use cases are far more complex than anything vanilla does. |
|
To be clear, the holdup isn't a lack of solutions, but that decisions haven't been made in affected areas Coordinate params were already implemented, those were quite trivial. I didn't initially implement them because they weren't necessary and I needed to refactor some code to avoid duplication. But that's dealt with now. Though, I wasn't aware that the inputs can be combined with no whitespace (which seems kinda sus to me, that's probably a bug in Bugrock), but if I wanted to support that here it'd just be a case of deleting some zero-whitespace checks in |
- Returning BranchingOverload from branch() callbacks is no longer allowed - OverloadBuilder now directly returns the single registered overload if only one is present, allowing it to be used for non-branching root overloads
…directly this allows us to consistently mark the overload classes as internal
This PR implements a command overloading system. Partially inspired by @alvin0319's prior (abandoned) work on #3871, but with deeper integration.
Relevant issues
Highlights
/tpcommandif(isset($args[$offset]))- your callback simply won't be called if the number of arguments is wrong or the arguments can't be parsed the way you asked for/me,/say,/tellnow handle quoted inputs correctly - previously/say hello "some user"would broadcasthello some user, but now broadcasts exactly what you typed, i.e.hello "some user"How it works
int $paramstringparameter, you can do?string $param = nulland check fornullto see if the parameter was specified or not.Itemobject; seeMappedParameter, and how it's used in commands like/enchant,/effectParameterobjects - these aren't given to the callback, but must be typed exactly in the position specified (e.g. a subcommand name); see how these are used for/effect <target> clear,/time set <timestamp>etcIntRangeParameter("xp")whose value is accepted byint $xp,?int $xp,int|string $xp, but notstring$xp)Command->executeOverloaded()is calledGotchas
Follow-up
x y z(which are actually 3xRelativeFloatparameters)pocketmine/locale-dataParameterinfos from a provided callback instead of having them explicitly specified#[Bounds(0, 100)] int $paramcould be explored, but this would only work for simple stuff known ahead of timeplugin.ymlcommands, which are hella dated, awful, and can't take advantage of this cool new system (Scrapplugin.ymlcommands #6506)API changes
Command->__construct()now requires a non-empty list inarray $overloadsCommand->executeOverloaded()- called bySimpleCommandMapCommand->getOverloads()Command->getUsages()- returns usages for all overloads the givenCommandSenderhas permissions forCommand::fetchPermittedPlayerTarget()- similar toVanillaCommand->fetchPermittedPlayerTarget(), but now accessible to all commands instead of justVanillaCommandinheritorsCommandStringHelper::parseQuoteAwareSingle()- parses a"quoted string \"like this\""intoquoted string "like this"- similar toparseQuoteAware()but only parses 1 arg instead of the entire commandCommand->getPermissions()- permissions are now defined per-overload instead of for the whole commandCommand->setPermissions()Command->testPermission()Command->testPermissionSilent()Command->getUsage()- command usages are now derived from overloads, and there may be more than one per commandCommand->setUsage()VanillaCommand- no longer served any purpose since it was all about manual arg parsingLegacyCommand- provides a similar API to the old-styleCommandfor a quick & dirty upgrade path for old code if you don't want to switch to the new system immediately (this is also still used byClosureCommand,FormattedCommandAliasandPluginCommandfor the time being)LegacyCommandwill use your old-style hardcoded usage message if provided, but you should consider switching to the new system. This is only provided to ease upgrading and won't stick around forever.Parameterand various subclasses inpocketmine\command\overloadpackageCommandOverload- accepts parameter info and callbacks to be given toCommand->__construct()ParameterParseException- thrown when aParametercan't parse a command tokenUtils::validateCallableSignature()now acceptsClosure|Prototypefor both arguments