*/ private array $permissions; /** * @param Parameter[]|string[] $parameters * @param string|string[] $permission * @phpstan-param list|string> $parameters * @phpstan-param string|list $permission * @phpstan-param anyClosure $handler */ public function __construct( private array $parameters, string|array $permission, private \Closure $handler, private bool $acceptsAliasUsed = false ){ foreach($this->parameters as $k => $parameter){ if(!is_string($parameter) && $parameter->consumesAllRemainingInputs() && $k !== array_key_last($this->parameters)){ throw new \InvalidArgumentException($parameter::class . " can only be used as the final argument, because it consumes all remaining inputs"); } } $permissions = is_string($permission) ? [$permission] : $permission; if(count($permissions) === 0){ throw new \InvalidArgumentException("At least one permission must be provided"); } $permissionManager = PermissionManager::getInstance(); foreach($permissions as $perm){ if($permissionManager->getPermission($perm) === null){ throw new \InvalidArgumentException("Cannot use non-existing permission \"$perm\""); } } $this->permissions = $permissions; //TODO: auto infer parameter infos if they aren't provided? //TODO: allow the type of CommandSender to be constrained - this can be useful for player-only commands etc $nonInputParameters = self::alwaysPresentArgs($this->acceptsAliasUsed); $literalCount = 0; $inputParameters = []; foreach($this->parameters as $parameter){ if(is_string($parameter)){ $literalCount++; }else{ $inputParameters[] = new ParameterInfo( $parameter->getCodeName(), $parameter->getCodeType(), byReference: false, isOptional: false, isVariadic: false, ); } } $expectedPrototype = new Prototype( new ReturnInfo(new NamedType(BuiltInType::VOID), byReference: false), ...$nonInputParameters, ...$inputParameters ); $actualPrototype = Prototype::fromClosure($this->handler); if(!$expectedPrototype->isSatisfiedBy($actualPrototype)){ //validateCallableSignature() not used because we want a custom error message throw new \InvalidArgumentException("Expected handler signature $expectedPrototype from provided parameter info, but handler has signature $actualPrototype"); } //optionals are inferred from the prototype of the callable, not the parameter infos themselves //contravariance allows them to be optional even if they're required in the prototype //literals must always be provided $this->requiredInputCount = $actualPrototype->getRequiredParameterCount() + $literalCount - count($nonInputParameters); } /** * @return ParameterInfo[] */ private static function alwaysPresentArgs(bool $acceptsAliasUsed) : array{ $result = [new ParameterInfo("sender", new NamedType(CommandSender::class), byReference: false, isOptional: false, isVariadic: false)]; if($acceptsAliasUsed){ $result[] = new ParameterInfo("aliasUsed", new NamedType(BuiltInType::STRING), byReference: false, isOptional: false, isVariadic: false); } return $result; } /** * @return string[] * @phpstan-return list */ public function getPermissions() : array{ return $this->permissions; } public function senderHasAnyPermissions(CommandSender $sender) : bool{ foreach($this->permissions as $permission){ if($sender->hasPermission($permission)){ return true; } } return false; } public function getUsage() : Translatable{ $templates = []; $args = []; $pos = 0; foreach($this->parameters as $parameter){ if(is_string($parameter)){ //literal token $templates[] = $parameter; continue; } //TODO: printable type info would be nice if($pos < $this->requiredInputCount){ $template = "<{%$pos}>"; }else{ $template = "[{%$pos}]"; } $suffix = $parameter->getSuffix(); $template .= $suffix; $templates[] = $template; $args[] = $parameter->getPrintableName(); $pos++; } return new Translatable(implode(" ", $templates), $args); } private static function skipWhitespace(string $commandLine, int &$offset) : int{ if(preg_match('/\G\s+/', $commandLine, $matches, offset: $offset) > 0){ $offset += strlen($matches[0]); return strlen($matches[0]); } return 0; } /** * @throws InvalidCommandSyntaxException */ public function invoke(CommandSender $sender, string $aliasUsed, string $commandLine) : void{ $offset = 0; $args = []; $literals = 0; //skip preceding whitespace self::skipWhitespace($commandLine, $offset); if($offset < strlen($commandLine)){ foreach($this->parameters as $parameter){ if(is_string($parameter)){ if(strpos($commandLine, $parameter, $offset) === $offset){ $offset += strlen($parameter); $literals++; }else{ throw new ParameterParseException("Literal \"$parameter\" expected"); } }else{ try{ $args[] = $parameter->parse($commandLine, $offset); }catch(ParameterParseException $e){ throw new ParameterParseException( "Failed parsing argument \$" . $parameter->getCodeName() . ": " . $e->getMessage(), previous: $e ); } } if(self::skipWhitespace($commandLine, $offset) === 0){ if($offset === strlen($commandLine)){ //no more tokens, rest of the parameters must be optional break; }else{ if(is_string($parameter)){ throw new AssumptionFailedError(); } throw new ParameterParseException("Parameter " . get_class($parameter) . " for \$" . $parameter->getCodeName() . " didn't stop on a whitespace character"); } } } } if($offset !== strlen($commandLine)){ throw new InvalidCommandSyntaxException("Too many arguments provided for overload"); } if(count($args) + $literals < $this->requiredInputCount){ throw new InvalidCommandSyntaxException("Not enough arguments provided for overload"); } //Reflection magic here :) //TODO: maybe we don't want to invoke this directly, but hand the args back to the caller? //this would allow resolving by more than just overload order if($this->acceptsAliasUsed){ // @phpstan-ignore-next-line ($this->handler)($sender, $aliasUsed, ...$args); }else{ // @phpstan-ignore-next-line ($this->handler)($sender, ...$args); } } }