mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-10-19 23:37:45 +00:00
Merge branch 'next-major' into modern-world-support
This commit is contained in:
@@ -148,6 +148,9 @@ class Leaves extends Transparent{
|
||||
if(($this->treeType->equals(TreeType::OAK()) || $this->treeType->equals(TreeType::DARK_OAK())) && mt_rand(1, 200) === 1){ //Apples
|
||||
$drops[] = VanillaItems::APPLE();
|
||||
}
|
||||
if(mt_rand(1, 50) === 1){
|
||||
$drops[] = VanillaItems::STICK()->setCount(mt_rand(1, 2));
|
||||
}
|
||||
|
||||
return $drops;
|
||||
}
|
||||
|
@@ -23,15 +23,28 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\command;
|
||||
|
||||
use pocketmine\Server;
|
||||
use pocketmine\command\utils\CommandStringHelper;
|
||||
use pocketmine\command\utils\InvalidCommandSyntaxException;
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use function array_map;
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function ord;
|
||||
use function preg_match;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
|
||||
class FormattedCommandAlias extends Command{
|
||||
/**
|
||||
* - matches a $
|
||||
* - captures an optional second $ to indicate required/optional
|
||||
* - captures a series of digits which don't start with a 0
|
||||
* - captures an optional - to indicate variadic
|
||||
*/
|
||||
private const FORMAT_STRING_REGEX = '/\G\$(\$)?((?!0)+\d+)(-)?/';
|
||||
|
||||
/** @var string[] */
|
||||
private array $formatStrings = [];
|
||||
|
||||
@@ -44,103 +57,124 @@ class FormattedCommandAlias extends Command{
|
||||
}
|
||||
|
||||
public function execute(CommandSender $sender, string $commandLabel, array $args){
|
||||
|
||||
$commands = [];
|
||||
$result = false;
|
||||
$result = true;
|
||||
|
||||
foreach($this->formatStrings as $formatString){
|
||||
try{
|
||||
$commands[] = $this->buildCommand($formatString, $args);
|
||||
$formatArgs = CommandStringHelper::parseQuoteAware($formatString);
|
||||
$commands[] = array_map(fn(string $formatArg) => $this->buildCommand($formatArg, $args), $formatArgs);
|
||||
}catch(\InvalidArgumentException $e){
|
||||
$sender->sendMessage(TextFormat::RED . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach($commands as $command){
|
||||
$result |= Server::getInstance()->dispatchCommand($sender, $command, true);
|
||||
$commandMap = $sender->getServer()->getCommandMap();
|
||||
foreach($commands as $commandArgs){
|
||||
//this approximately duplicates the logic found in SimpleCommandMap::dispatch()
|
||||
//this is to allow directly invoking the commands without having to rebuild a command string and parse it
|
||||
//again for no reason
|
||||
//TODO: a method on CommandMap to invoke a command with pre-parsed arguments would probably be a good idea
|
||||
//for a future major version
|
||||
$commandLabel = array_shift($commandArgs);
|
||||
if($commandLabel === null){
|
||||
throw new AssumptionFailedError("This should have been checked before construction");
|
||||
}
|
||||
|
||||
if(($target = $commandMap->getCommand($commandLabel)) !== null){
|
||||
$target->timings->startTiming();
|
||||
|
||||
try{
|
||||
$target->execute($sender, $commandLabel, $args);
|
||||
}catch(InvalidCommandSyntaxException $e){
|
||||
$sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::commands_generic_usage($target->getUsage())));
|
||||
}finally{
|
||||
$target->timings->stopTiming();
|
||||
}
|
||||
}else{
|
||||
$sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_notFound($commandLabel, "/help")->prefix(TextFormat::RED)));
|
||||
|
||||
//to match the behaviour of SimpleCommandMap::dispatch()
|
||||
//this shouldn't normally happen, but might happen if the command was unregistered or modified after
|
||||
//the alias was installed
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $args
|
||||
*/
|
||||
private function buildCommand(string $formatString, array $args) : string{
|
||||
$index = strpos($formatString, '$');
|
||||
while($index !== false){
|
||||
$index = 0;
|
||||
while(($index = strpos($formatString, '$', $index)) !== false){
|
||||
$start = $index;
|
||||
if($index > 0 && $formatString[$start - 1] === "\\"){
|
||||
$formatString = substr($formatString, 0, $start - 1) . substr($formatString, $start);
|
||||
$index = strpos($formatString, '$', $index);
|
||||
//offset is now pointing at the next character because we just deleted the \
|
||||
continue;
|
||||
}
|
||||
|
||||
$required = false;
|
||||
if($formatString[$index + 1] == '$'){
|
||||
$required = true;
|
||||
|
||||
++$index;
|
||||
}
|
||||
|
||||
++$index;
|
||||
|
||||
$argStart = $index;
|
||||
|
||||
while($index < strlen($formatString) && self::inRange(ord($formatString[$index]) - 48, 0, 9)){
|
||||
++$index;
|
||||
}
|
||||
|
||||
if($argStart === $index){
|
||||
$info = self::extractPlaceholderInfo($formatString, $index);
|
||||
if($info === null){
|
||||
throw new \InvalidArgumentException("Invalid replacement token");
|
||||
}
|
||||
|
||||
$position = (int) substr($formatString, $argStart, $index);
|
||||
|
||||
if($position === 0){
|
||||
throw new \InvalidArgumentException("Invalid replacement token");
|
||||
}
|
||||
|
||||
--$position;
|
||||
|
||||
$rest = false;
|
||||
|
||||
if($index < strlen($formatString) && $formatString[$index] === "-"){
|
||||
$rest = true;
|
||||
++$index;
|
||||
}
|
||||
|
||||
$end = $index;
|
||||
[$fullPlaceholder, $required, $position, $rest] = $info;
|
||||
$position--; //array offsets start at 0, but placeholders start at 1
|
||||
|
||||
if($required && $position >= count($args)){
|
||||
throw new \InvalidArgumentException("Missing required argument " . ($position + 1));
|
||||
}
|
||||
|
||||
$replacement = "";
|
||||
if($rest && $position < count($args)){
|
||||
for($i = $position, $c = count($args); $i < $c; ++$i){
|
||||
if($i !== $position){
|
||||
$replacement .= " ";
|
||||
}
|
||||
|
||||
$replacement .= $args[$i];
|
||||
}
|
||||
}elseif($position < count($args)){
|
||||
$replacement .= $args[$position];
|
||||
}
|
||||
$replacement = self::buildReplacement($args, $position, $rest);
|
||||
|
||||
$end = $index + strlen($fullPlaceholder);
|
||||
$formatString = substr($formatString, 0, $start) . $replacement . substr($formatString, $end);
|
||||
|
||||
$index = $start + strlen($replacement);
|
||||
|
||||
$index = strpos($formatString, '$', $index);
|
||||
}
|
||||
|
||||
return $formatString;
|
||||
}
|
||||
|
||||
private static function inRange(int $i, int $j, int $k) : bool{
|
||||
return $i >= $j && $i <= $k;
|
||||
/**
|
||||
* @param string[] $args
|
||||
* @phpstan-param list<string> $args
|
||||
*/
|
||||
private static function buildReplacement(array $args, int $position, bool $rest) : string{
|
||||
$replacement = "";
|
||||
if($rest && $position < count($args)){
|
||||
for($i = $position, $c = count($args); $i < $c; ++$i){
|
||||
if($i !== $position){
|
||||
$replacement .= " ";
|
||||
}
|
||||
|
||||
$replacement .= $args[$i];
|
||||
}
|
||||
}elseif($position < count($args)){
|
||||
$replacement .= $args[$position];
|
||||
}
|
||||
|
||||
return $replacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{string, bool, int, bool}
|
||||
*/
|
||||
private static function extractPlaceholderInfo(string $commandString, int $offset) : ?array{
|
||||
if(preg_match(self::FORMAT_STRING_REGEX, $commandString, $matches, 0, $offset) !== 1){
|
||||
return null;
|
||||
}
|
||||
|
||||
$fullPlaceholder = $matches[0];
|
||||
|
||||
$required = ($matches[1] ?? "") !== "";
|
||||
$position = (int) $matches[2];
|
||||
$variadic = ($matches[3] ?? "") !== "";
|
||||
|
||||
return [$fullPlaceholder, $required, $position, $variadic];
|
||||
}
|
||||
}
|
||||
|
@@ -64,17 +64,15 @@ use pocketmine\command\defaults\TransferServerCommand;
|
||||
use pocketmine\command\defaults\VanillaCommand;
|
||||
use pocketmine\command\defaults\VersionCommand;
|
||||
use pocketmine\command\defaults\WhitelistCommand;
|
||||
use pocketmine\command\utils\CommandStringHelper;
|
||||
use pocketmine\command\utils\InvalidCommandSyntaxException;
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function preg_match_all;
|
||||
use function strcasecmp;
|
||||
use function stripslashes;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
@@ -197,16 +195,7 @@ class SimpleCommandMap implements CommandMap{
|
||||
}
|
||||
|
||||
public function dispatch(CommandSender $sender, string $commandLine) : bool{
|
||||
$args = [];
|
||||
preg_match_all('/"((?:\\\\.|[^\\\\"])*)"|(\S+)/u', $commandLine, $matches);
|
||||
foreach($matches[0] as $k => $_){
|
||||
for($i = 1; $i <= 2; ++$i){
|
||||
if($matches[$i][$k] !== ""){
|
||||
$args[$k] = $i === 1 ? stripslashes($matches[$i][$k]) : $matches[$i][$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$args = CommandStringHelper::parseQuoteAware($commandLine);
|
||||
|
||||
$sentCommandLabel = array_shift($args);
|
||||
if($sentCommandLabel !== null && ($target = $this->getCommand($sentCommandLabel)) !== null){
|
||||
@@ -259,8 +248,8 @@ class SimpleCommandMap implements CommandMap{
|
||||
$recursive = [];
|
||||
|
||||
foreach($commandStrings as $commandString){
|
||||
$args = explode(" ", $commandString);
|
||||
$commandName = array_shift($args);
|
||||
$args = CommandStringHelper::parseQuoteAware($commandString);
|
||||
$commandName = array_shift($args) ?? "";
|
||||
$command = $this->getCommand($commandName);
|
||||
|
||||
if($command === null){
|
||||
|
64
src/command/utils/CommandStringHelper.php
Normal file
64
src/command/utils/CommandStringHelper.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\command\utils;
|
||||
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function preg_last_error_msg;
|
||||
use function preg_match_all;
|
||||
use function preg_replace;
|
||||
|
||||
final class CommandStringHelper{
|
||||
|
||||
private function __construct(){
|
||||
//NOOP
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a command string into its component parts. Parts of the string which are inside unescaped quotes are
|
||||
* considered as one argument.
|
||||
*
|
||||
* Examples:
|
||||
* - `give "steve jobs" apple` -> ['give', 'steve jobs', 'apple']
|
||||
* - `say "This is a \"string containing quotes\""` -> ['say', 'This is a "string containing quotes"']
|
||||
*
|
||||
* @return string[]
|
||||
* @phpstan-return list<string>
|
||||
*/
|
||||
public static function parseQuoteAware(string $commandLine) : array{
|
||||
$args = [];
|
||||
preg_match_all('/"((?:\\\\.|[^\\\\"])*)"|(\S+)/u', $commandLine, $matches);
|
||||
foreach($matches[0] as $k => $_){
|
||||
for($i = 1; $i <= 2; ++$i){
|
||||
if($matches[$i][$k] !== ""){
|
||||
/** @var string $match */ //phpstan can't understand preg_match and friends by itself :(
|
||||
$match = $matches[$i][$k];
|
||||
$args[(int) $k] = preg_replace('/\\\\([\\\\"])/u', '$1', $match) ?? throw new AssumptionFailedError(preg_last_error_msg());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
}
|
@@ -282,6 +282,7 @@ abstract class Entity{
|
||||
|
||||
public function setNameTagVisible(bool $value = true) : void{
|
||||
$this->nameTagVisible = $value;
|
||||
$this->networkPropertiesDirty = true;
|
||||
}
|
||||
|
||||
public function setNameTagAlwaysVisible(bool $value = true) : void{
|
||||
|
@@ -42,5 +42,9 @@ class Boat extends Item{
|
||||
return 1200; //400 in PC
|
||||
}
|
||||
|
||||
public function getMaxStackSize() : int{
|
||||
return 1;
|
||||
}
|
||||
|
||||
//TODO
|
||||
}
|
||||
|
@@ -1759,6 +1759,13 @@ final class KnownTranslationFactory{
|
||||
]);
|
||||
}
|
||||
|
||||
public static function pocketmine_plugin_enableError(Translatable|string $param0, Translatable|string $param1) : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_ENABLEERROR, [
|
||||
0 => $param0,
|
||||
1 => $param1,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function pocketmine_plugin_extensionNotLoaded(Translatable|string $extensionName) : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_EXTENSIONNOTLOADED, [
|
||||
"extensionName" => $extensionName,
|
||||
@@ -1859,6 +1866,10 @@ final class KnownTranslationFactory{
|
||||
]);
|
||||
}
|
||||
|
||||
public static function pocketmine_plugin_suicide() : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_SUICIDE, []);
|
||||
}
|
||||
|
||||
public static function pocketmine_plugin_unknownDependency(Translatable|string $param0) : Translatable{
|
||||
return new Translatable(KnownTranslationKeys::POCKETMINE_PLUGIN_UNKNOWNDEPENDENCY, [
|
||||
0 => $param0,
|
||||
|
@@ -368,6 +368,7 @@ final class KnownTranslationKeys{
|
||||
public const POCKETMINE_PLUGIN_DUPLICATEPERMISSIONERROR = "pocketmine.plugin.duplicatePermissionError";
|
||||
public const POCKETMINE_PLUGIN_EMPTYEXTENSIONVERSIONCONSTRAINT = "pocketmine.plugin.emptyExtensionVersionConstraint";
|
||||
public const POCKETMINE_PLUGIN_ENABLE = "pocketmine.plugin.enable";
|
||||
public const POCKETMINE_PLUGIN_ENABLEERROR = "pocketmine.plugin.enableError";
|
||||
public const POCKETMINE_PLUGIN_EXTENSIONNOTLOADED = "pocketmine.plugin.extensionNotLoaded";
|
||||
public const POCKETMINE_PLUGIN_GENERICLOADERROR = "pocketmine.plugin.genericLoadError";
|
||||
public const POCKETMINE_PLUGIN_INCOMPATIBLEAPI = "pocketmine.plugin.incompatibleAPI";
|
||||
@@ -385,6 +386,7 @@ final class KnownTranslationKeys{
|
||||
public const POCKETMINE_PLUGIN_MAINCLASSWRONGTYPE = "pocketmine.plugin.mainClassWrongType";
|
||||
public const POCKETMINE_PLUGIN_RESTRICTEDNAME = "pocketmine.plugin.restrictedName";
|
||||
public const POCKETMINE_PLUGIN_SPACESDISCOURAGED = "pocketmine.plugin.spacesDiscouraged";
|
||||
public const POCKETMINE_PLUGIN_SUICIDE = "pocketmine.plugin.suicide";
|
||||
public const POCKETMINE_PLUGIN_UNKNOWNDEPENDENCY = "pocketmine.plugin.unknownDependency";
|
||||
public const POCKETMINE_SAVE_START = "pocketmine.save.start";
|
||||
public const POCKETMINE_SAVE_SUCCESS = "pocketmine.save.success";
|
||||
|
@@ -447,6 +447,13 @@ class PluginManager{
|
||||
$this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin;
|
||||
|
||||
(new PluginEnableEvent($plugin))->call();
|
||||
}else{
|
||||
$this->server->getLogger()->critical($this->server->getLanguage()->translate(
|
||||
KnownTranslationFactory::pocketmine_plugin_enableError(
|
||||
$plugin->getName(),
|
||||
KnownTranslationFactory::pocketmine_plugin_suicide()
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -59,6 +59,7 @@ use function interface_exists;
|
||||
use function is_a;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_float;
|
||||
use function is_infinite;
|
||||
use function is_int;
|
||||
use function is_nan;
|
||||
@@ -435,6 +436,19 @@ final class Utils{
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private static function stringifyValueForTrace(mixed $value, int $maxStringLength) : string{
|
||||
return match(true){
|
||||
is_object($value) => "object " . self::getNiceClassName($value) . "#" . spl_object_id($value),
|
||||
is_array($value) => "array[" . count($value) . "]",
|
||||
is_string($value) => "string[" . strlen($value) . "] " . substr(Utils::printable($value), 0, $maxStringLength),
|
||||
is_bool($value) => $value ? "true" : "false",
|
||||
is_int($value) => "int " . $value,
|
||||
is_float($value) => "float " . $value,
|
||||
$value === null => "null",
|
||||
default => gettype($value) . " " . Utils::printable((string) $value)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[][] $trace
|
||||
* @phpstan-param list<array<string, mixed>> $trace
|
||||
@@ -451,22 +465,15 @@ final class Utils{
|
||||
}else{
|
||||
$args = $trace[$i]["params"];
|
||||
}
|
||||
/** @var mixed[] $args */
|
||||
|
||||
$params = implode(", ", array_map(function($value) use($maxStringLength) : string{
|
||||
if(is_object($value)){
|
||||
return "object " . self::getNiceClassName($value) . "#" . spl_object_id($value);
|
||||
}
|
||||
if(is_array($value)){
|
||||
return "array[" . count($value) . "]";
|
||||
}
|
||||
if(is_string($value)){
|
||||
return "string[" . strlen($value) . "] " . substr(Utils::printable($value), 0, $maxStringLength);
|
||||
}
|
||||
if(is_bool($value)){
|
||||
return $value ? "true" : "false";
|
||||
}
|
||||
return gettype($value) . " " . Utils::printable((string) $value);
|
||||
}, $args));
|
||||
$paramsList = [];
|
||||
$offset = 0;
|
||||
foreach($args as $argId => $value){
|
||||
$paramsList[] = ($argId === $offset ? "" : "$argId: ") . self::stringifyValueForTrace($value, $maxStringLength);
|
||||
$offset++;
|
||||
}
|
||||
$params = implode(", ", $paramsList);
|
||||
}
|
||||
$messages[] = "#$i " . (isset($trace[$i]["file"]) ? Filesystem::cleanPath($trace[$i]["file"]) : "") . "(" . (isset($trace[$i]["line"]) ? $trace[$i]["line"] : "") . "): " . (isset($trace[$i]["class"]) ? $trace[$i]["class"] . (($trace[$i]["type"] === "dynamic" || $trace[$i]["type"] === "->") ? "->" : "::") : "") . $trace[$i]["function"] . "(" . Utils::printable($params) . ")";
|
||||
}
|
||||
|
@@ -3004,9 +3004,14 @@ class World implements ChunkManager{
|
||||
unset($this->activeChunkPopulationTasks[$index]);
|
||||
|
||||
if($dirtyChunks === 0){
|
||||
$promise = $this->chunkPopulationRequestMap[$index];
|
||||
unset($this->chunkPopulationRequestMap[$index]);
|
||||
$promise->resolve($chunk);
|
||||
$promise = $this->chunkPopulationRequestMap[$index] ?? null;
|
||||
if($promise !== null){
|
||||
unset($this->chunkPopulationRequestMap[$index]);
|
||||
$promise->resolve($chunk);
|
||||
}else{
|
||||
//Handlers of ChunkPopulateEvent, ChunkLoadEvent, or just ChunkListeners can cause this
|
||||
$this->logger->debug("Unable to resolve population promise for chunk x=$x,z=$z - populated chunk was forcibly unloaded while setting modified chunks");
|
||||
}
|
||||
}else{
|
||||
//request failed, stick it back on the queue
|
||||
//we didn't resolve the promise or touch it in any way, so any fake chunk loaders are still valid and
|
||||
|
Reference in New Issue
Block a user