Merge branch 'next-major' into modern-world-support

This commit is contained in:
Dylan K. Taylor
2022-05-11 13:14:42 +01:00
19 changed files with 332 additions and 120 deletions

View File

@@ -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;
}

View File

@@ -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];
}
}

View File

@@ -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){

View 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;
}
}

View File

@@ -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{

View File

@@ -42,5 +42,9 @@ class Boat extends Item{
return 1200; //400 in PC
}
public function getMaxStackSize() : int{
return 1;
}
//TODO
}

View File

@@ -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,

View File

@@ -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";

View File

@@ -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()
)
));
}
}
}

View File

@@ -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) . ")";
}

View File

@@ -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