├── plugin.yml ├── .editorconfig ├── .poggit.yml ├── src └── muqsit │ └── tebex │ ├── handler │ ├── due │ │ ├── playerlist │ │ │ ├── indexer │ │ │ │ ├── PlayerIndexer.php │ │ │ │ ├── NameBasedPlayerIndexer.php │ │ │ │ └── XuidBasedPlayerIndexer.php │ │ │ ├── TebexDuePlayerHolder.php │ │ │ ├── TebexDuePlayerListListener.php │ │ │ └── TebexDuePlayerList.php │ │ ├── session │ │ │ ├── DelayedOnlineCommandHandler.php │ │ │ └── TebexPlayerSession.php │ │ ├── TebexLazyDueCommandsListener.php │ │ ├── TebexDueOfflineCommandsHandler.php │ │ └── TebexDueCommandsHandler.php │ ├── command │ │ ├── utils │ │ │ ├── TebexSubCommand.php │ │ │ └── ClosureCommandExecutor.php │ │ ├── TebexCommandSender.php │ │ ├── UnregisteredTebexCommandExecutor.php │ │ └── RegisteredTebexCommandExecutor.php │ ├── PmmpTebexLogger.php │ ├── TebexApiUtils.php │ ├── TebexHandler.php │ └── ThreadedTebexConnection.php │ ├── event │ ├── TebexEvent.php │ ├── TebexExecuteOfflineCommandEvent.php │ └── TebexExecuteOnlineCommandEvent.php │ ├── utils │ ├── TypeValidator.php │ └── TypedConfig.php │ ├── thread │ ├── TebexThreadPool.php │ └── TebexThread.php │ └── Loader.php ├── resources └── config.yml └── README.md /plugin.yml: -------------------------------------------------------------------------------- 1 | name: Tebex 2 | main: muqsit\tebex\Loader 3 | version: 0.1.15 4 | author: Muqsit 5 | api: 5.0.0 6 | permissions: 7 | tebex.admin: 8 | default: op 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = yes 3 | 4 | [*] 5 | indent_size = 4 6 | indent_style = tab 7 | 8 | [*.yml] 9 | indent_size = 2 10 | indent_style = space -------------------------------------------------------------------------------- /.poggit.yml: -------------------------------------------------------------------------------- 1 | --- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/Muqsit/Tebex 2 | build-by-default: true 3 | branches: 4 | - pmmp-api5 5 | - pmmp-api4 6 | - master 7 | projects: 8 | Tebex: 9 | path: "" 10 | libs: 11 | - src: muqsit/tebexapi/TebexApi 12 | version: ^0.2.1 13 | ... 14 | -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/playerlist/indexer/PlayerIndexer.php: -------------------------------------------------------------------------------- 1 | created = microtime(true); 17 | } 18 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/session/DelayedOnlineCommandHandler.php: -------------------------------------------------------------------------------- 1 | handler->onPlayerJoin(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/PmmpTebexLogger.php: -------------------------------------------------------------------------------- 1 | logger->logException($t); 20 | if($t instanceof TebexException && $t->extra_trace !== null){ 21 | echo $t->extra_trace; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/playerlist/indexer/NameBasedPlayerIndexer.php: -------------------------------------------------------------------------------- 1 | getName()); 17 | } 18 | 19 | public function fromTebexDuePlayer(TebexDuePlayer $player) : string{ 20 | return strtolower($player->name); 21 | } 22 | } -------------------------------------------------------------------------------- /resources/config.yml: -------------------------------------------------------------------------------- 1 | # Tebex Configuration 2 | secret: "" 3 | 4 | # Number of concurrent calls that can be requested 5 | worker-limit: 2 6 | 7 | # Sub-commands to disable. You may want to disable "secret" sub-command when running in production 8 | # Example to disable "tbx secret" and "tbx info": 9 | # disabled-sub-commands: [secret, info] 10 | disabled-sub-commands: [] 11 | 12 | # Permissions given to Tebex command sender 13 | # Example to grant Tebex the following permissions: "myplugin.command.addrandomtags", "myplugin.command.keypack": 14 | # permissions: 15 | # - myplugin.command.addrandomtags 16 | # - myplugin.command.keypack 17 | permissions: [] 18 | -------------------------------------------------------------------------------- /src/muqsit/tebex/event/TebexEvent.php: -------------------------------------------------------------------------------- 1 | executor)($sender, $command, $label, $args); 23 | } 24 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/playerlist/TebexDuePlayerListListener.php: -------------------------------------------------------------------------------- 1 | list->onPlayerJoin($event->getPlayer()); 23 | } 24 | 25 | /** 26 | * @param PlayerQuitEvent $event 27 | * @priority MONITOR 28 | */ 29 | public function onPlayerQuit(PlayerQuitEvent $event) : void{ 30 | $this->list->onPlayerQuit($event->getPlayer()); 31 | } 32 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/playerlist/indexer/XuidBasedPlayerIndexer.php: -------------------------------------------------------------------------------- 1 | getXuid(); 18 | $xuid !== "" || throw new InvalidArgumentException("Cannot retrieve XUID of player: {$player->getName()}"); 19 | return $xuid; 20 | } 21 | 22 | public function fromTebexDuePlayer(TebexDuePlayer $player) : string{ 23 | $xuid = $player->uuid; // Tebex player uuids in Minecraft: Bedrock Edition (Online) mode is xbox's xuid 24 | return $xuid ?? throw new InvalidArgumentException("Cannot retrieve XUID of Tebex due player: {$player->name}"); 25 | } 26 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/command/TebexCommandSender.php: -------------------------------------------------------------------------------- 1 | addAttachment($plugin, DefaultPermissions::ROOT_OPERATOR, true); 33 | } 34 | 35 | public function getName() : string{ 36 | return "TEBEX"; 37 | } 38 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/event/TebexExecuteOfflineCommandEvent.php: -------------------------------------------------------------------------------- 1 | $original_placeholders 21 | * @param array $placeholders 22 | */ 23 | public function __construct( 24 | Loader $plugin, 25 | readonly public TebexDuePlayer $due_player, 26 | readonly public TebexQueuedOfflineCommand $command, 27 | readonly public array $original_placeholders, 28 | public array $placeholders 29 | ){ 30 | parent::__construct($plugin); 31 | } 32 | 33 | public function getFinalCommand() : string{ 34 | return strtr($this->command->command->asRawString(), $this->placeholders); 35 | } 36 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/event/TebexExecuteOnlineCommandEvent.php: -------------------------------------------------------------------------------- 1 | $original_placeholders 23 | * @param array $placeholders 24 | */ 25 | public function __construct( 26 | Loader $plugin, 27 | readonly public Player $player, 28 | readonly public TebexDuePlayer $due_player, 29 | readonly public TebexQueuedOnlineCommand $command, 30 | readonly public array $original_placeholders, 31 | public array $placeholders 32 | ){ 33 | parent::__construct($plugin); 34 | } 35 | 36 | public function getFinalCommand() : string{ 37 | return strtr($this->command->command->asRawString(), $this->placeholders); 38 | } 39 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/utils/TypeValidator.php: -------------------------------------------------------------------------------- 1 | $max){ 21 | throw new InvalidArgumentException("{$key}'s value is out of range [{$min}, {$max}]"); 22 | } 23 | return $value; 24 | } 25 | 26 | public static function validateString(string $key, mixed $value) : string{ 27 | is_string($value) || throw new InvalidArgumentException("Invalid value for {$key}: " . self::printValue($value)); 28 | return $value; 29 | } 30 | 31 | /** 32 | * @param string $key 33 | * @param mixed $value 34 | * @return string[] 35 | */ 36 | public static function validateStringList(string $key, mixed $value) : array{ 37 | is_array($value) || throw new InvalidArgumentException("Invalid value for {$key}: " . self::printValue($value)); 38 | foreach($value as $index => $item){ 39 | self::validateString("{$key}.{$index}", $item); 40 | } 41 | return $value; 42 | } 43 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/command/UnregisteredTebexCommandExecutor.php: -------------------------------------------------------------------------------- 1 | setSecret($secret); 24 | }catch(TebexException $e){ 25 | $sender->sendMessage($e->getMessage()); 26 | } 27 | 28 | if($info !== null){ 29 | $config = $loader->getConfig(); 30 | $config->set("secret", $secret); 31 | $config->save(); 32 | 33 | $account = $info->account; 34 | $server = $info->server; 35 | $sender->sendMessage("Successfully logged in to server (#{$server->id}) {$server->name} as (#{$account->id}) {$account->name}!"); 36 | } 37 | } 38 | 39 | public function onCommand(CommandSender $sender, Command $command, string $label, array $args) : bool{ 40 | if(isset($args[0], $args[1]) && $args[0] === "secret"){ 41 | self::handleTypeSecret($this->plugin, $sender, $args[1]); 42 | return true; 43 | } 44 | 45 | $sender->sendMessage("Usage: /{$label} secret "); 46 | return false; 47 | } 48 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/utils/TypedConfig.php: -------------------------------------------------------------------------------- 1 | config->get($key, $default), $min, $max); 17 | } 18 | 19 | public function getNestedInt(string $key, int $default = 0, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX) : int{ 20 | return TypeValidator::validateInt($key, $this->config->getNested($key, $default), $min, $max); 21 | } 22 | 23 | public function getString(string $key) : string{ 24 | return TypeValidator::validateString($key, $this->config->get($key)); 25 | } 26 | 27 | public function getNestedString(string $key) : string{ 28 | return TypeValidator::validateString($key, $this->config->getNested($key)); 29 | } 30 | 31 | /** 32 | * @param string $key 33 | * @param string[] $default 34 | * @return string[] 35 | */ 36 | public function getStringList(string $key, array $default = []) : array{ 37 | return TypeValidator::validateStringList($key, $this->config->get($key, $default)); 38 | } 39 | 40 | /** 41 | * @param string $key 42 | * @param string[] $default 43 | * @return string[] 44 | */ 45 | public function getNestedStringList(string $key, array $default = []) : array{ 46 | return TypeValidator::validateStringList($key, $this->config->getNested($key, $default)); 47 | } 48 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/TebexApiUtils.php: -------------------------------------------------------------------------------- 1 | parse($gui_item->value); 21 | if($item === null){ 22 | $plugin = Server::getInstance()->getPluginManager()->getPlugin("Tebex"); 23 | if($plugin instanceof Loader){ 24 | $plugin->getLogger()->warning("Failed to parse GUI item \"{$gui_item->value}\", using PAPER as fallback"); 25 | } 26 | return VanillaItems::PAPER(); 27 | } 28 | return $item; 29 | } 30 | 31 | public static function onlineFormatCommand(TebexCommand $command, Player $player, TebexDuePlayer $due_player) : string{ 32 | return strtr($command->asRawString(), self::onlineCommandParameters($player, $due_player)); 33 | } 34 | 35 | /** 36 | * @param Player $player 37 | * @param TebexDuePlayer $due_player 38 | * @return array 39 | */ 40 | public static function onlineCommandParameters(Player $player, TebexDuePlayer $due_player) : array{ 41 | $gamertag = "\"{$player->getName()}\""; 42 | return [ 43 | "{name}" => $gamertag, 44 | "{player}" => $gamertag, 45 | "{username}" => "\"{$due_player->name}\"", 46 | "{id}" => $player->getXuid() 47 | ]; 48 | } 49 | 50 | public static function offlineFormatCommand(TebexCommand $command, TebexDuePlayer $due_player) : string{ 51 | return strtr($command->asRawString(), self::offlineCommandParameters($due_player)); 52 | } 53 | 54 | /** 55 | * @param TebexDuePlayer $due_player 56 | * @return array 57 | */ 58 | public static function offlineCommandParameters(TebexDuePlayer $due_player) : array{ 59 | $gamertag = "\"{$due_player->name}\""; 60 | return [ 61 | "{name}" => $gamertag, 62 | "{player}" => $gamertag, 63 | "{username}" => $gamertag, 64 | "{id}" => $due_player->uuid ?? "" 65 | ]; 66 | } 67 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/TebexHandler.php: -------------------------------------------------------------------------------- 1 | |null */ 19 | private ?array $command_ids = null; 20 | 21 | private int $pending_commands_batch_counter = 0; 22 | 23 | public function __construct( 24 | readonly private Loader $plugin 25 | ){ 26 | $this->init(); 27 | } 28 | 29 | private function init() : void{ 30 | $this->due_commands_handler = new TebexDueCommandsHandler($this->plugin, $this); 31 | } 32 | 33 | public function getDueCommandsHandler() : TebexDueCommandsHandler{ 34 | return $this->due_commands_handler; 35 | } 36 | 37 | public function queueCommandDeletion(int $command_id, int ...$command_ids) : void{ 38 | if($this->command_ids === null){ 39 | $this->command_ids = []; 40 | $this->plugin->getScheduler()->scheduleDelayedTask(new ClosureTask($this->deletePendingCommands(...)), 1); 41 | } 42 | 43 | array_push($this->command_ids, $command_id, ...$command_ids); 44 | } 45 | 46 | private function deletePendingCommands() : void{ 47 | if($this->command_ids !== null){ 48 | $command_ids = $this->command_ids; 49 | $this->command_ids = null; 50 | $batch_id = ++$this->pending_commands_batch_counter; 51 | $this->plugin->getLogger()->info("Executing pending command deletion batch #{$batch_id} consisting of: (" . count($command_ids) . ") [" . implode(", ", $command_ids) . "]"); 52 | $this->plugin->getApi()->deleteCommands($command_ids, new TebexResponseHandler(function(EmptyTebexResponse $_) use($batch_id) : void{ 53 | $this->plugin->getLogger()->info("Successfully executed pending command deletion batch #{$batch_id}"); 54 | }, function(TebexException $e) use($batch_id, $command_ids) : void{ 55 | $this->plugin->getLogger()->info("Failed to execute pending command deletion batch #{$batch_id} due to: {$e->getMessage()}, queueing into next batch"); 56 | $this->queueCommandDeletion(...$command_ids); 57 | })); 58 | } 59 | } 60 | 61 | public function shutdown() : void{ 62 | $this->deletePendingCommands(); 63 | } 64 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/ThreadedTebexConnection.php: -------------------------------------------------------------------------------- 1 | pool = new TebexThreadPool(new SimpleTebexConnectionHandler()); 25 | $this->ssl_config = $ssl_config; 26 | 27 | $class_loaders = []; 28 | $devirion = Server::getInstance()->getPluginManager()->getPlugin("DEVirion"); 29 | if($devirion !== null){ 30 | if(!method_exists($devirion, "getVirionClassLoader")){ 31 | throw new RuntimeException(); 32 | } 33 | $class_loaders[] = Server::getInstance()->getLoader(); 34 | $class_loaders[] = $devirion->getVirionClassLoader(); 35 | } 36 | 37 | for($i = 0; $i < $workers; $i++){ 38 | $thread = new TebexThread($logger, $this->pool->sleeper_handler_entry, $secret, $ssl_config, $this->pool->connection_handler); 39 | if(count($class_loaders) > 0){ 40 | $thread->setClassLoaders($class_loaders); 41 | } 42 | $this->pool->addWorker($thread); 43 | } 44 | $this->pool->start(); 45 | } 46 | 47 | public function request(TebexRequest $request, TebexResponseHandler $callback) : void{ 48 | $this->pool->getLeastBusyWorker()->push($request, $callback); 49 | } 50 | 51 | public function getLatency() : float{ 52 | return $this->pool->getLatency(); 53 | } 54 | 55 | public function waitAll(int $sleep_duration_ms = 50000) : void{ 56 | $this->pool->waitAll($sleep_duration_ms); 57 | } 58 | 59 | public function process() : void{ 60 | // NOOP, done on child thread 61 | } 62 | 63 | public function wait() : void{ 64 | $this->pool->waitAll(50_000); 65 | } 66 | 67 | public function disconnect() : void{ 68 | $this->pool->shutdown(); 69 | $this->ssl_config->close(); 70 | } 71 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/thread/TebexThreadPool.php: -------------------------------------------------------------------------------- 1 | */ 18 | private array $workers = []; 19 | 20 | private float $latency = 0.0; 21 | 22 | public function __construct( 23 | readonly public TebexConnectionHandler $connection_handler 24 | ){ 25 | $this->sleeper_handler_entry = Server::getInstance()->getTickSleeper()->addNotifier(function() : void{ 26 | foreach($this->workers as $thread){ 27 | $this->collectThread($thread); 28 | } 29 | }); 30 | } 31 | 32 | /** 33 | * @param TebexThread $thread 34 | */ 35 | public function addWorker(TebexThread $thread) : void{ 36 | $this->workers[spl_object_id($thread)] = $thread; 37 | } 38 | 39 | public function start() : void{ 40 | count($this->workers) > 0 || throw new UnderflowException("Cannot start an empty pool of workers"); 41 | foreach($this->workers as $thread){ 42 | $thread->start(); 43 | } 44 | } 45 | 46 | /** 47 | * @return TebexThread 48 | */ 49 | public function getLeastBusyWorker() : TebexThread{ 50 | $best = null; 51 | $best_score = INF; 52 | foreach($this->workers as $thread){ 53 | $score = $thread->busy_score; 54 | if($score < $best_score){ 55 | $best_score = $score; 56 | $best = $thread; 57 | if($score === 0){ 58 | break; 59 | } 60 | } 61 | } 62 | assert($best !== null); 63 | return $best; 64 | } 65 | 66 | public function getLatency() : float{ 67 | return $this->latency; 68 | } 69 | 70 | public function waitAll(int $sleep_duration_ms) : void{ 71 | foreach($this->workers as $thread){ 72 | while($thread->busy_score > 0){ 73 | usleep($sleep_duration_ms); 74 | $this->collectThread($thread); 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * @param TebexThread $thread 81 | */ 82 | private function collectThread(TebexThread $thread) : void{ 83 | foreach($thread->collectPending() as $latency){ 84 | $this->latency = $latency; 85 | } 86 | } 87 | 88 | public function shutdown() : void{ 89 | foreach($this->workers as $thread){ 90 | $thread->stop(); 91 | $thread->join(); 92 | } 93 | $this->workers = []; 94 | } 95 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tebex 2 | Tebex webstore integration for PocketMine-MP. 3 | 4 | ## Features 5 | - `disable-sub-commands` configuration so you can disable `/tebex secret` and other risky sub-commands when running in production. 6 | - Running tebex API calls over a dedicated child thread so the main thread doesn't lag when checking for pending commands, running `/tebex refresh` etc. 7 | 8 | ## Developer Docs 9 | At the moment, there isn't a method to interact with this plugin. The plugin however has the Tebex API and handling split — `tebex/handler/*` makes use of TebexAPI. 10 | You can create a new TebexAPI instance and supply any valid secret to it and call tebex endpoints. All method calls in TebexAPI are run on child thread(s) so you'll need to supply 11 | a `TebexResponseHandler $callback` to retrieve responses. 12 | ```php 13 | TebexAPI::getInformation(TebexResponseHandler $callback) : void 14 | ``` 15 | You may construct a `new TebexResponseHandler(Closure $on_success, Closure $on_failure)`, or use the helper methods: 16 | - `TebexResponseHandler::debug(string $expected_response_class = TebexResponse::class)` — `var_dump`s the response. 17 | - `TebexResponseHandler::onSuccess(Closure $on_success)` — Calls `$on_success` on success and logs (level: critical) error message on failure. 18 | ```php 19 | $secret = ""; 20 | $worker_limit = 1; 21 | $api = new TebexAPI(MainLogger::getLogger(), $secret, SSLConfiguration::recommended(), $worker_limit); 22 | 23 | $api->getInformation(TebexResponseHandler::onSuccess(function(TebexInformation $information) : void{ 24 | $account_info = $this->information->getAccount(); 25 | $server_info = $this->information->getServer(); 26 | })); 27 | 28 | $api->lookup("Steve", TebexResponseHandler::onSuccess(function(TebexUser $user) : void{ 29 | var_dump($user->getChargebackRate()); 30 | })); 31 | 32 | $api->lookup("Alex", TebexResponseHandler::debug(TebexUser::class)); // var_dump()s the TebexUser on success 33 | 34 | $api->waitAll(); // wait until all queued requests have received responses 35 | $api->shutdown(); // shutdown connection (stops all threads and unlinks temp SSL files) 36 | ``` 37 | Not all Tebex endpoints have been implemented in TebexAPI. You may create an issue or a PR adding the missing ones or create one yourself (take a look at classes in `tebex\api`, use `TebexAPI::request(TebexRequest, TebexResponseHandler)` to dispatch custom ones). 38 | 39 | ## Notes 40 | The [official Tebex plugin for PocketMine](https://github.com/tebexio/BuycraftPM) runs refresh tasks on main thread, freezing the server based on Tebex <-> Your Minecraft Server 41 | latency. It executes Tebex API calls while having `CURLOPT_SSL_VERIFYPEER` disabled, thereby allowing man-in-the-middle (MITM) attacks and has some unnecessary commands that 42 | do not suit Bedrock Edition (the `/buy` GUI command) which is why I decided to write a tebex integration plugin from scratch. 43 | -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/playerlist/TebexDuePlayerList.php: -------------------------------------------------------------------------------- 1 | */ 16 | private array $tebex_due_players_by_id = []; // indexes pending customers by TebexPlayerId => TebexDuePlayerHolder 17 | 18 | /** @var array */ 19 | private array $tebex_due_players_by_index = []; // indexes pending customers by TebexDuePlayerList::$indexer => TebexPlayerId 20 | 21 | /** @var array */ 22 | private array $online_players = []; // indexes online players on the server by TebexDuePlayerList::$indexer => TebexPlayerSession 23 | 24 | /** 25 | * @param Closure(Player, TebexDuePlayerHolder) : void $on_match 26 | * @param PlayerIndexer $indexer 27 | */ 28 | public function __construct( 29 | readonly private PlayerIndexer $indexer, 30 | readonly private Closure $on_match 31 | ){} 32 | 33 | private function onMatch(Player $player, TebexDuePlayerHolder $holder) : void{ 34 | ($this->on_match)($player, $holder); 35 | } 36 | 37 | public function onPlayerJoin(Player $player) : void{ 38 | $this->online_players[$this->indexer->fromPlayer($player)] = new TebexPlayerSession($player); 39 | $holder = $this->getTebexAwaitingPlayer($player); 40 | if($holder !== null){ 41 | $this->onMatch($player, $holder); 42 | } 43 | } 44 | 45 | public function onPlayerQuit(Player $player) : void{ 46 | if(isset($this->online_players[$index = $this->indexer->fromPlayer($player)])){ 47 | $this->online_players[$index]->destroy(); 48 | unset($this->online_players[$index]); 49 | } 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function getAll() : array{ 56 | return $this->tebex_due_players_by_id; 57 | } 58 | 59 | /** 60 | * @param TebexDuePlayer[] $due_players 61 | */ 62 | public function update(array $due_players) : void{ 63 | $this->tebex_due_players_by_id = []; 64 | $this->tebex_due_players_by_index = []; 65 | foreach($due_players as $player){ 66 | $holder = new TebexDuePlayerHolder($player); 67 | $this->tebex_due_players_by_id[$player->id] = $holder; 68 | $this->tebex_due_players_by_index[$index = $this->indexer->fromTebexDuePlayer($player)] = $player->id; 69 | if(isset($this->online_players[$index])){ 70 | $this->onMatch($this->online_players[$index]->getPlayer(), $holder); 71 | } 72 | } 73 | } 74 | 75 | public function remove(TebexDuePlayerHolder $holder) : void{ 76 | $player = $holder->player; 77 | unset($this->tebex_due_players_by_id[$player->id], $this->tebex_due_players_by_index[$this->indexer->fromTebexDuePlayer($player)]); 78 | } 79 | 80 | public function getTebexAwaitingPlayer(Player $player) : ?TebexDuePlayerHolder{ 81 | return isset($this->tebex_due_players_by_index[$index = $this->indexer->fromPlayer($player)]) ? $this->tebex_due_players_by_id[$this->tebex_due_players_by_index[$index]] : null; 82 | } 83 | 84 | public function getOnlinePlayer(Player $player) : ?TebexPlayerSession{ 85 | return $this->online_players[$this->indexer->fromPlayer($player)] ?? null; 86 | } 87 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/session/TebexPlayerSession.php: -------------------------------------------------------------------------------- 1 | getScheduler(); 24 | } 25 | 26 | /** @var array */ 27 | private array $delayed_online_command_handlers = []; 28 | 29 | public function __construct( 30 | readonly private Player $player 31 | ){} 32 | 33 | public function getPlayer() : Player{ 34 | return $this->player; 35 | } 36 | 37 | public function destroy() : void{ 38 | foreach($this->delayed_online_command_handlers as $handler){ 39 | $handler->handler->cancel(); 40 | } 41 | $this->delayed_online_command_handlers = []; 42 | } 43 | 44 | /** 45 | * @param Loader $plugin 46 | * @param TebexQueuedOnlineCommand $command 47 | * @param TebexDuePlayer $due_player 48 | * @param Closure(string|null) : void $callback 49 | */ 50 | public function executeOnlineCommand(Loader $plugin, TebexQueuedOnlineCommand $command, TebexDuePlayer $due_player, Closure $callback) : void{ 51 | $conditions = $command->conditions; 52 | $delay = $conditions->delay; 53 | if($delay > 0){ 54 | $this->scheduleCommandForDelay($plugin, $command, $due_player, $delay * 20, $callback); 55 | }else{ 56 | $callback($this->instantlyExecuteOnlineCommand($plugin, $command, $due_player)); 57 | } 58 | } 59 | 60 | /** 61 | * @param Loader $plugin 62 | * @param TebexQueuedOnlineCommand $command 63 | * @param TebexDuePlayer $due_player 64 | * @param int $delay 65 | * @param Closure(string|null) : void $callback 66 | * @return bool 67 | */ 68 | private function scheduleCommandForDelay(Loader $plugin, TebexQueuedOnlineCommand $command, TebexDuePlayer $due_player, int $delay, Closure $callback) : bool{ 69 | if(isset($this->delayed_online_command_handlers[$id = $command->id])){ 70 | return false; 71 | } 72 | 73 | $this->delayed_online_command_handlers[$id] = new DelayedOnlineCommandHandler($command, self::$scheduler->scheduleDelayedTask(new ClosureTask(function() use($id, $command, $due_player, $callback, $plugin) : void{ 74 | $callback($this->instantlyExecuteOnlineCommand($plugin, $command, $due_player)); 75 | unset($this->delayed_online_command_handlers[$id]); 76 | }), $delay)); 77 | return true; 78 | } 79 | 80 | private function instantlyExecuteOnlineCommand(Loader $plugin, TebexQueuedOnlineCommand $command, TebexDuePlayer $due_player) : ?string{ 81 | $conditions = $command->conditions; 82 | $slots = $conditions->slots; 83 | if($slots > 0){ 84 | $inventory = $this->player->getInventory(); 85 | $free_slots = $inventory->getSize() - count($inventory->getContents()); 86 | if($free_slots < $slots){ 87 | return null; 88 | } 89 | } 90 | 91 | $original_placeholders = TebexApiUtils::onlineCommandParameters($this->player, $due_player); 92 | $event = new TebexExecuteOnlineCommandEvent($plugin, $this->player, $due_player, $command, $original_placeholders, $original_placeholders); 93 | $event->call(); 94 | if($event->isCancelled()){ 95 | return null; 96 | } 97 | 98 | $command_string = $event->getFinalCommand(); 99 | if(!$this->player->getServer()->dispatchCommand(TebexCommandSender::getInstance(), $command_string)){ 100 | return null; 101 | } 102 | 103 | return $command_string; 104 | } 105 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/TebexDueOfflineCommandsHandler.php: -------------------------------------------------------------------------------- 1 | */ 28 | private array $delayed = []; 29 | 30 | public function __construct(Loader $plugin, TebexHandler $handler, int $check_period = 60 * 20){ 31 | $this->plugin = $plugin; 32 | $this->logger = $plugin->getLogger(); 33 | $this->handler = $handler; 34 | $plugin->getScheduler()->scheduleRepeatingTask(new ClosureTask($this->check(...)), $check_period); 35 | } 36 | 37 | /** 38 | * @param (Closure(int|TebexException) : void)|null $callback 39 | */ 40 | public function check(?Closure $callback = null) : void{ 41 | $this->plugin->getApi()->getQueuedOfflineCommands(new TebexResponseHandler(function(TebexQueuedOfflineCommandsInfo $info) use($callback) : void{ 42 | if($callback !== null){ 43 | $callback(count($info->commands)); 44 | } 45 | $this->onFetchDueOfflineCommands($info); 46 | }, function(TebexException $exception) use($callback) : void{ 47 | if($callback !== null){ 48 | $callback($exception); 49 | } 50 | })); 51 | } 52 | 53 | /** 54 | * @param (Closure(int) : void)|null $callback 55 | */ 56 | public function markAllAsExecuted(?Closure $callback = null) : void{ 57 | $this->plugin->getApi()->getQueuedOfflineCommands(TebexResponseHandler::onSuccess(function(TebexQueuedOfflineCommandsInfo $info) use($callback) : void{ 58 | $commands = $info->commands; 59 | foreach($commands as $command){ 60 | $this->handler->queueCommandDeletion($command->id); 61 | } 62 | if($callback !== null){ 63 | $callback(count($commands)); 64 | } 65 | })); 66 | } 67 | 68 | private function onFetchDueOfflineCommands(TebexQueuedOfflineCommandsInfo $info) : void{ 69 | $commands = $info->commands; 70 | 71 | $commands_c = count($commands); 72 | $this->logger->debug("Fetched {$commands_c} offline command" . ($commands_c === 1 ? "" : "s")); 73 | 74 | foreach($commands as $command){ 75 | $this->executeCommand($command, function(?string $command_string) use($command) : void{ 76 | if($command_string !== null){ 77 | $command_id = $command->id; 78 | $this->handler->queueCommandDeletion($command_id); 79 | $this->logger->info("Executed offline command #{$command_id}: {$command_string}"); 80 | }else{ 81 | $command_string = TebexApiUtils::offlineFormatCommand($command->command, $command->player); 82 | $this->logger->warning("Failed to execute offline command: {$command_string}"); 83 | } 84 | }); 85 | } 86 | } 87 | 88 | /** 89 | * @param TebexQueuedOfflineCommand $command 90 | * @param Closure(string|null) : void $callback 91 | */ 92 | private function executeCommand(TebexQueuedOfflineCommand $command, Closure $callback) : void{ 93 | $delay = $command->conditions->delay; 94 | if($delay > 0){ 95 | if(!isset($this->delayed[$id = $command->id])){ 96 | $this->delayed[$id] = $id; 97 | $this->plugin->getScheduler()->scheduleDelayedTask(new ClosureTask(function() use($id, $command, $callback) : void{ 98 | $callback($this->instantlyExecuteCommand($command)); 99 | unset($this->delayed[$id]); 100 | }), $delay * 20); 101 | } 102 | }else{ 103 | $callback($this->instantlyExecuteCommand($command)); 104 | } 105 | } 106 | 107 | private function instantlyExecuteCommand(TebexQueuedOfflineCommand $command) : ?string{ 108 | $original_placeholders = TebexApiUtils::offlineCommandParameters($command->player); 109 | $event = new TebexExecuteOfflineCommandEvent($this->plugin, $command->player, $command, $original_placeholders, $original_placeholders); 110 | $event->call(); 111 | if($event->isCancelled()){ 112 | return null; 113 | } 114 | $command_string = $event->getFinalCommand(); 115 | if(!Server::getInstance()->dispatchCommand(TebexCommandSender::getInstance(), $command_string)){ 116 | return null; 117 | } 118 | return $command_string; 119 | } 120 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/Loader.php: -------------------------------------------------------------------------------- 1 | getConfig()); 40 | 41 | if(!TebexCommandSender::hasInstance()){ 42 | $sender = new TebexCommandSender($this, $this->getServer()->getLanguage()); 43 | $attachment = $sender->addAttachment($this, self::PERMISSION_GROUP_CONFIGURED); 44 | $attachment->setPermissions(array_fill_keys($config->getStringList("permissions"), true)); 45 | TebexCommandSender::setInstance($sender); 46 | } 47 | 48 | TebexApiStatics::setLogger(new PmmpTebexLogger($this->getLogger())); 49 | 50 | $command = new PluginCommand("tebex", $this, new UnregisteredTebexCommandExecutor($this)); 51 | $command->setAliases(["tbx", "bc", "buycraft"]); 52 | $command->setPermission("tebex.admin"); 53 | $this->getServer()->getCommandMap()->register($this->getName(), $command); 54 | $this->command = $command; 55 | 56 | $this->worker_limit = $config->getInt("worker-limit", 2); 57 | 58 | $secret = $config->getString("secret"); 59 | 60 | try{ 61 | $this->setSecret($secret); 62 | }catch(TebexException $e){ 63 | $this->getLogger()->notice(($secret !== "" ? "{$e->getMessage()} " : "") . "Please configure your server's secret using: /{$this->command->getName()} secret "); 64 | $this->command->setExecutor(new UnregisteredTebexCommandExecutor($this)); 65 | } 66 | } 67 | 68 | protected function onDisable() : void{ 69 | $this->handler?->shutdown(); 70 | $this->api?->disconnect(); 71 | } 72 | 73 | /** 74 | * @param string $secret 75 | * @return TebexInformation 76 | * @throws TebexException 77 | */ 78 | public function setSecret(string $secret) : TebexInformation{ 79 | /** @var TebexInformation|TebexException $result */ 80 | $result = null; 81 | 82 | $ssl_data = file_get_contents($this->getResourcePath("cacert.pem")); 83 | $ssl_data !== false || throw new RuntimeException("Failed to read SSL file cacert.pem"); 84 | 85 | $api = new ConnectionBasedTebexApi(new ThreadedTebexConnection($this->getServer()->getLogger(), $secret, SslConfiguration::fromData($ssl_data), $this->worker_limit)); 86 | $api->getInformation(new TebexResponseHandler( 87 | static function(TebexInformation $information) use(&$result) : void{ $result = $information; }, 88 | static function(TebexException $e) use(&$result) : void{ $result = $e; } 89 | )); 90 | $api->wait(); 91 | 92 | if($result instanceof TebexException){ 93 | $api->disconnect(); 94 | throw $result; 95 | } 96 | 97 | $this->init($api, $result); 98 | return $this->information; 99 | } 100 | 101 | private function init(ConnectionBasedTebexApi $api, TebexInformation $information) : void{ 102 | $this->handler?->shutdown(); 103 | $this->api?->disconnect(); 104 | 105 | $this->api = $api; 106 | $this->information = $information; 107 | $this->handler = new TebexHandler($this); 108 | 109 | $executor = new RegisteredTebexCommandExecutor($this, $this->handler); 110 | foreach((new TypedConfig($this->getConfig()))->getStringList("disabled-sub-commands", []) as $disabled_sub_command){ 111 | $executor->unregisterSubCommand($disabled_sub_command); 112 | } 113 | $this->command->setExecutor($executor); 114 | 115 | $account = $this->information->account; 116 | $server = $this->information->server; 117 | $this->getLogger()->debug("Listening to events of \"{$server->name}\"[#{$server->id}] server as \"{$account->name}\"[#{$account->id}] (latency: " . round($this->getApi()->getLatency() * 1000) . "ms)"); 118 | } 119 | 120 | public function getApi() : TebexApi{ 121 | return $this->api ?? throw new LogicException("API is not ready"); 122 | } 123 | 124 | public function getInformation() : TebexInformation{ 125 | return $this->information; 126 | } 127 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/thread/TebexThread.php: -------------------------------------------------------------------------------- 1 | > */ 28 | private static array $handlers = []; 29 | 30 | private static int $handler_ids = 0; 31 | 32 | readonly private SleeperHandlerEntry $sleeper_handler_entry; 33 | 34 | /** @var ThreadSafeArray */ 35 | private ThreadSafeArray $incoming; 36 | 37 | /** @var ThreadSafeArray */ 38 | private ThreadSafeArray $outgoing; 39 | 40 | readonly private ThreadSafeLogger $logger; 41 | public int $busy_score = 0; 42 | private bool $running = false; 43 | readonly private string $secret; 44 | readonly private string $ca_path; 45 | readonly private string $_connection_handler; 46 | 47 | /** 48 | * @param ThreadSafeLogger $logger 49 | * @param SleeperHandlerEntry $sleeper_handler_entry 50 | * @param string $secret 51 | * @param SslConfiguration $ssl_config 52 | * @param TebexConnectionHandler $connection_handler 53 | */ 54 | public function __construct(ThreadSafeLogger $logger, SleeperHandlerEntry $sleeper_handler_entry, string $secret, SslConfiguration $ssl_config, TebexConnectionHandler $connection_handler){ 55 | $this->_connection_handler = igbinary_serialize($connection_handler); 56 | 57 | $this->sleeper_handler_entry = $sleeper_handler_entry; 58 | $this->ca_path = $ssl_config->getCAInfoPath(); 59 | $this->incoming = new ThreadSafeArray(); 60 | $this->outgoing = new ThreadSafeArray(); 61 | $this->logger = $logger; 62 | $this->secret = $secret; 63 | } 64 | 65 | /** 66 | * @template TTebexResponse of TebexResponse 67 | * @param TebexRequest $request 68 | * @param TebexResponseHandler $handler 69 | */ 70 | public function push(TebexRequest $request, TebexResponseHandler $handler) : void{ 71 | $handler_id = ++self::$handler_ids; 72 | $this->incoming[] = igbinary_serialize(new TebexRequestHolder($request, $handler_id)); 73 | self::$handlers[$handler_id] = $handler; 74 | ++$this->busy_score; 75 | $this->synchronized($this->notify(...)); 76 | } 77 | 78 | protected function onRun() : void{ 79 | $this->running = true; 80 | 81 | $notifier = $this->sleeper_handler_entry->createNotifier(); 82 | 83 | /** @var TebexConnectionHandler $connection_handler */ 84 | $connection_handler = igbinary_unserialize($this->_connection_handler); 85 | 86 | $default_curl_opts = TebexConnectionHelper::buildDefaultCurlOptions($this->secret, $this->ca_path); 87 | while($this->running){ 88 | while(($request_serialized = $this->incoming->shift()) !== null){ 89 | assert(is_string($request_serialized)); 90 | /** @var TebexRequestHolder $request_holder */ 91 | $request_holder = igbinary_unserialize($request_serialized); 92 | $this->logger->debug("[cURL] Executing request: {$request_holder->request->getEndpoint()}"); 93 | 94 | try{ 95 | $response_holder = $connection_handler->handle($request_holder, $default_curl_opts); 96 | }catch(TebexException $e){ 97 | $response_holder = new TebexResponseFailureHolder($request_holder->handler_id, $e->getLatency(), $e->getMessage(), $e->getCode(), $e->getTraceAsString()); 98 | }catch(Exception $e){ 99 | $response_holder = new TebexResponseFailureHolder($request_holder->handler_id, 5000, $e->getMessage(), $e->getCode(), $e->getTraceAsString()); 100 | } 101 | 102 | $this->outgoing[] = igbinary_serialize($response_holder); 103 | $notifier->wakeupSleeper(); 104 | } 105 | $this->sleep(); 106 | } 107 | } 108 | 109 | public function sleep() : void{ 110 | $this->synchronized(function() : void{ 111 | if($this->running){ 112 | $this->wait(); 113 | } 114 | }); 115 | } 116 | 117 | public function stop() : void{ 118 | $this->running = false; 119 | $this->synchronized($this->notify(...)); 120 | } 121 | 122 | /** 123 | * Collects all responses and returns the total latency 124 | * (in seconds) in sending request and getting response. 125 | * 126 | * @return Generator 127 | */ 128 | public function collectPending() : Generator{ 129 | while(($holder_serialized = $this->outgoing->shift()) !== null){ 130 | assert(is_string($holder_serialized)); 131 | 132 | /** @var TebexResponseHolder $holder */ 133 | $holder = igbinary_unserialize($holder_serialized); 134 | 135 | $holder->trigger(self::$handlers[$holder->handler_id]); 136 | unset(self::$handlers[$holder->handler_id]); 137 | --$this->busy_score; 138 | 139 | yield $holder->latency; 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/command/RegisteredTebexCommandExecutor.php: -------------------------------------------------------------------------------- 1 | */ 22 | private array $sub_commands = []; 23 | 24 | /** @var array */ 25 | private array $aliases = []; 26 | 27 | public function __construct( 28 | readonly private Loader $plugin, 29 | readonly private TebexHandler $handler 30 | ){ 31 | $this->registerDefaultSubCommands(); 32 | } 33 | 34 | private function registerDefaultSubCommands() : void{ 35 | $this->registerSubCommand(new TebexSubCommand("secret", "Set Tebex server secret", new ClosureCommandExecutor( 36 | function(CommandSender $sender, Command $command, string $label, array $args) : bool{ 37 | if(isset($args[1])){ 38 | UnregisteredTebexCommandExecutor::handleTypeSecret($this->plugin, $sender, $args[1]); 39 | }else{ 40 | $sender->sendMessage("Usage: /{$label} {$args[0]} "); 41 | } 42 | return true; 43 | } 44 | ))); 45 | 46 | $this->registerSubCommand(new TebexSubCommand("info", "Fetch Tebex account, server and API info", new ClosureCommandExecutor( 47 | function(CommandSender $sender, Command $command, string $label, array $args) : bool{ 48 | $info = $this->plugin->getInformation(); 49 | $account = $info->account; 50 | $server = $info->server; 51 | 52 | $sender->sendMessage( 53 | "" . TextFormat::EOL . 54 | TextFormat::BOLD . TextFormat::WHITE . "Tebex Account" . TextFormat::RESET . TextFormat::EOL . 55 | TextFormat::WHITE . "ID: " . TextFormat::GRAY . $account->id . TextFormat::EOL . 56 | TextFormat::WHITE . "Domain: " . TextFormat::GRAY . $account->domain . TextFormat::EOL . 57 | TextFormat::WHITE . "Name: " . TextFormat::GRAY . $account->name . TextFormat::EOL . 58 | TextFormat::WHITE . "Currency: " . TextFormat::GRAY . "{$account->currency->iso_4217} ({$account->currency->symbol})" . TextFormat::EOL . 59 | TextFormat::WHITE . "Online Mode: " . TextFormat::GRAY . ($account->online_mode ? "Enabled" : "Disabled") . TextFormat::EOL . 60 | TextFormat::WHITE . "Game Type: " . TextFormat::GRAY . $account->game_type . TextFormat::EOL . 61 | TextFormat::WHITE . "Event Logging: " . TextFormat::GRAY . ($account->log_events ? "Enabled" : "Disabled") . TextFormat::EOL . 62 | "" . TextFormat::EOL . 63 | TextFormat::BOLD . TextFormat::WHITE . "Tebex Server" . TextFormat::RESET . TextFormat::EOL . 64 | TextFormat::WHITE . "ID: " . TextFormat::GRAY . $server->id . TextFormat::EOL . 65 | TextFormat::WHITE . "Name: " . TextFormat::GRAY . $server->name . TextFormat::EOL . 66 | "" . TextFormat::EOL . 67 | TextFormat::BOLD . TextFormat::WHITE . "Tebex API" . TextFormat::RESET . TextFormat::EOL . 68 | TextFormat::WHITE . "Latency: " . TextFormat::GRAY . round($this->plugin->getApi()->getLatency() * 1000) . "ms" . TextFormat::EOL . 69 | "" . TextFormat::EOL 70 | ); 71 | return true; 72 | } 73 | ))); 74 | 75 | $this->registerSubCommand(new TebexSubCommand("refresh", "Refresh offline and online command queues", new ClosureCommandExecutor( 76 | function(CommandSender $sender, Command $command, string $label, array $args) : bool{ 77 | /** @var array|null $command_senders_force_check */ 78 | static $command_senders_force_check = null; 79 | if($command_senders_force_check === null){ 80 | $command_senders_force_check = []; 81 | $this->handler->getDueCommandsHandler()->refresh(static function(int|TebexException $offline_commands, int|TebexException $online_players) use(&$command_senders_force_check) : void{ 82 | if($command_senders_force_check !== null){ 83 | foreach($command_senders_force_check as $sender){ 84 | if(!($sender instanceof Player) || $sender->isOnline()){ 85 | $sender->sendMessage( 86 | TextFormat::WHITE . "Refreshed command queue" . TextFormat::EOL . 87 | TextFormat::WHITE . "Offline commands fetched: " . TextFormat::GRAY . ($offline_commands instanceof TebexException ? TextFormat::RED . TextFormat::ITALIC . $offline_commands->getMessage() . TextFormat::RESET : $offline_commands) . TextFormat::EOL . 88 | TextFormat::WHITE . "Online players due: " . TextFormat::GRAY . ($online_players instanceof TebexException ? TextFormat::RED . TextFormat::ITALIC . $online_players->getMessage() . TextFormat::RESET : $online_players) 89 | ); 90 | } 91 | } 92 | $command_senders_force_check = null; 93 | } 94 | }); 95 | } 96 | 97 | $command_senders_force_check[spl_object_id($sender)] = $sender; 98 | $sender->sendMessage(TextFormat::GRAY . "Refreshing command queue..."); 99 | return true; 100 | } 101 | ), ["forcecheck"])); 102 | 103 | $this->registerSubCommand(new TebexSubCommand("dropall", "Drop all queued commands", new ClosureCommandExecutor( 104 | function(CommandSender $sender, Command $command, string $label, array $args) : bool{ 105 | /** @var array|null $command_senders_dropall */ 106 | static $command_senders_dropall = null; 107 | if($command_senders_dropall === null){ 108 | $command_senders_dropall = []; 109 | $this->handler->getDueCommandsHandler()->markAllAsExecuted(static function(int $marked) use(&$command_senders_dropall) : void{ 110 | if($command_senders_dropall !== null){ 111 | foreach($command_senders_dropall as $sender){ 112 | if(!($sender instanceof Player) || $sender->isOnline()){ 113 | $sender->sendMessage(TextFormat::WHITE . "Marked " . TextFormat::GRAY . $marked . TextFormat::WHITE . " command(s) as executed."); 114 | } 115 | } 116 | $command_senders_dropall = null; 117 | } 118 | }); 119 | } 120 | 121 | $command_senders_dropall[spl_object_id($sender)] = $sender; 122 | $sender->sendMessage(TextFormat::GRAY . "Dropping all queued commands"); 123 | return true; 124 | } 125 | ))); 126 | } 127 | 128 | public function registerSubCommand(TebexSubCommand $sub_command) : void{ 129 | $this->sub_commands[$sub_command->name] = $sub_command; 130 | foreach($sub_command->aliases as $alias){ 131 | $this->aliases[$alias] = $sub_command->name; 132 | } 133 | } 134 | 135 | public function unregisterSubCommand(string $name) : void{ 136 | isset($this->sub_commands[$name]) || throw new InvalidArgumentException("Tried unregistering an unregistered sub-command: {$name}"); 137 | foreach($this->sub_commands[$name]->aliases as $alias){ 138 | unset($this->aliases[$alias]); 139 | } 140 | unset($this->sub_commands[$name]); 141 | } 142 | 143 | public function getSubCommand(string $name) : ?TebexSubCommand{ 144 | return $this->sub_commands[$name] ?? (isset($this->aliases[$name]) ? $this->sub_commands[$this->aliases[$name]] : null); 145 | } 146 | 147 | public function onCommand(CommandSender $sender, Command $command, string $label, array $args) : bool{ 148 | if(isset($args[0])){ 149 | $sub_command = $this->getSubCommand($args[0]); 150 | if($sub_command !== null){ 151 | return $sub_command->executor->onCommand($sender, $command, $label, $args); 152 | } 153 | } 154 | 155 | $help = TextFormat::BOLD . TextFormat::WHITE . "Tebex Commands" . TextFormat::RESET . TextFormat::EOL; 156 | foreach($this->sub_commands as $sub_command){ 157 | $help .= TextFormat::WHITE . "/{$label} {$sub_command->name}" . TextFormat::GRAY . " - {$sub_command->description}" . TextFormat::EOL; 158 | } 159 | $sender->sendMessage(rtrim($help, TextFormat::EOL)); 160 | return false; 161 | } 162 | } -------------------------------------------------------------------------------- /src/muqsit/tebex/handler/due/TebexDueCommandsHandler.php: -------------------------------------------------------------------------------- 1 | logger = $plugin->getLogger(); 42 | $this->offline_commands_handler = new TebexDueOfflineCommandsHandler($plugin, $handler); 43 | 44 | $api = $plugin->getApi(); 45 | $this->list = new TebexDuePlayerList($this->selectIndexer(), function(Player $player, TebexDuePlayerHolder $holder) use($api, $handler) : void{ 46 | $session = $this->list->getOnlinePlayer($player); 47 | assert($session !== null); 48 | $api->getQueuedOnlineCommands($holder->player->id, TebexResponseHandler::onSuccess(function(TebexQueuedOnlineCommandsInfo $info) use($player, $session, $holder, $handler) : void{ 49 | if(!$player->isOnline()){ 50 | return; 51 | } 52 | 53 | $commands = $info->commands; 54 | $total_commands = count($commands); 55 | $timestamp = microtime(true); 56 | foreach($commands as $tebex_command){ 57 | $session->executeOnlineCommand($this->plugin, $tebex_command, $holder->player, function(?string $command) use($tebex_command, $handler, &$total_commands, $player, $holder, $timestamp) : void{ 58 | if($command === null){ 59 | $command_string = TebexApiUtils::onlineFormatCommand($tebex_command->command, $player, $holder->player); 60 | $this->logger->warning("Failed to execute online command: {$command_string}"); 61 | return; 62 | } 63 | 64 | $command_id = $tebex_command->id; 65 | $handler->queueCommandDeletion($command_id); 66 | if(--$total_commands === 0){ 67 | $current_holder = $this->list->getTebexAwaitingPlayer($player); 68 | if($current_holder !== null && $current_holder->created < $timestamp){ 69 | $this->list->remove($current_holder); 70 | } 71 | } 72 | $this->logger->info("Executed online command #{$command_id}: {$command}"); 73 | }); 74 | } 75 | })); 76 | }); 77 | 78 | $plugin->getServer()->getPluginManager()->registerEvents(new TebexDuePlayerListListener($this->list), $plugin); 79 | $plugin->getServer()->getPluginManager()->registerEvents(new TebexLazyDueCommandsListener($this), $plugin); 80 | } 81 | 82 | private function selectIndexer() : PlayerIndexer{ 83 | $information = $this->plugin->getInformation(); 84 | $game_type = $information->account->game_type; 85 | if($game_type === "Minecraft (Bedrock)"){ 86 | if($this->plugin->getServer()->getOnlineMode()){ 87 | return new XuidBasedPlayerIndexer(); 88 | } 89 | $this->plugin->getLogger()->warning("Your webstore (" . $information->server->name . "#" . $information->server->id . ") is configured with the game type '{$game_type}', but your server is not running in online-mode."); 90 | $this->plugin->getLogger()->warning("Consider using a 'Minecraft (Offline/Geyser)' Tebex webstore, or set 'xbox-auth' to 'on' in server.properties"); 91 | $this->plugin->getLogger()->warning("The plugin will switch to use 'Minecraft (Offline/Geyser)' configuration on this server."); 92 | return new NameBasedPlayerIndexer(); 93 | } 94 | if($game_type === "Minecraft (Offline/Geyser)"){ 95 | return new NameBasedPlayerIndexer(); 96 | } 97 | throw new InvalidArgumentException("Unsupported game server type {$game_type}"); 98 | } 99 | 100 | /** 101 | * @param (Closure(int) : void)|null $callback 102 | */ 103 | public function markAllAsExecuted(?Closure $callback = null) : void{ 104 | $this->plugin->getApi()->getDuePlayersList(TebexResponseHandler::onSuccess(function(TebexDuePlayersInfo $result) use($callback) : void{ 105 | $marked = 0; 106 | $batches = count($result->players) + 1; 107 | 108 | $cb = static function(int $done) use(&$marked, &$batches, $callback) : void{ 109 | $marked += $done; 110 | if(--$batches === 0){ 111 | if($callback !== null){ 112 | $callback($marked); 113 | } 114 | } 115 | }; 116 | 117 | $this->offline_commands_handler->markAllAsExecuted($cb); 118 | 119 | foreach($result->players as $player){ 120 | $this->plugin->getApi()->getQueuedOnlineCommands($player->id, TebexResponseHandler::onSuccess(function(TebexQueuedOnlineCommandsInfo $info) use($cb) : void{ 121 | $commands = $info->commands; 122 | foreach($commands as $command){ 123 | $this->handler->queueCommandDeletion($command->id); 124 | } 125 | $cb(count($commands)); 126 | })); 127 | } 128 | })); 129 | } 130 | 131 | private function scheduleDuePlayersCheck() : bool{ 132 | if(!$this->is_idle){ 133 | return false; 134 | } 135 | 136 | $this->is_idle = false; 137 | $server = $this->plugin->getServer(); 138 | $this->checkDuePlayers(function() use($server) : bool{ 139 | if(count($server->getOnlinePlayers()) === 0){ 140 | $this->is_idle = true; 141 | $this->logger->debug("Online commands handler is now idle"); 142 | return false; 143 | } 144 | return true; 145 | }); 146 | return true; 147 | } 148 | 149 | public function getList() : TebexDuePlayerList{ 150 | return $this->list; 151 | } 152 | 153 | /** 154 | * @param (Closure(int|TebexException, int|TebexException) : void)|null $callback 155 | */ 156 | public function refresh(?Closure $callback = null) : void{ 157 | $this->offline_commands_handler->check(function(int|TebexException $offline_cmds_count) use($callback) : void{ 158 | $this->checkDuePlayers(null, static function(int|TebexException $response) use($offline_cmds_count, $callback) : void{ 159 | if($callback !== null){ 160 | $callback($offline_cmds_count, $response); 161 | } 162 | }); 163 | }); 164 | } 165 | 166 | /** 167 | * @param (Closure() : bool)|null $reschedule_condition 168 | * @param (Closure(int|TebexException) : void)|null $callback 169 | */ 170 | public function checkDuePlayers(?Closure $reschedule_condition = null, ?Closure $callback = null) : void{ 171 | $try_rescheduling = function(int $next_check) use($reschedule_condition, $callback) : void{ 172 | if($reschedule_condition !== null && $reschedule_condition()){ 173 | $this->plugin->getScheduler()->scheduleDelayedTask(new ClosureTask(function() use($reschedule_condition, $callback) : void{ $this->checkDuePlayers($reschedule_condition, $callback); }), max(1, $next_check) * 20); 174 | } 175 | }; 176 | $this->plugin->getApi()->getDuePlayersList(new TebexResponseHandler(function(TebexDuePlayersInfo $result) use($try_rescheduling, $callback) : void{ 177 | $this->onFetchDuePlayers($result); 178 | if($callback !== null){ 179 | $callback(count($result->players)); 180 | } 181 | $try_rescheduling($result->meta->next_check); 182 | }, function(TebexException $exception) use($callback, $try_rescheduling) : void{ 183 | if($callback !== null){ 184 | $callback($exception); 185 | } 186 | $try_rescheduling(5); 187 | })); 188 | } 189 | 190 | private function onFetchDuePlayers(TebexDuePlayersInfo $result) : void{ 191 | $players = $result->players; 192 | $this->list->update($players); 193 | 194 | $players_c = count($players); 195 | $this->logger->debug("{$players_c} player" . ($players_c === 1 ? " is " : "s are") . " in the online commands queue"); 196 | } 197 | 198 | public function onPlayerJoin() : void{ 199 | if($this->scheduleDuePlayersCheck()){ 200 | $this->logger->debug("Online commands handler is no longer idle"); 201 | } 202 | } 203 | } --------------------------------------------------------------------------------