diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d191d0a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,26 @@ +name: Build phar +on: + push: + branches: [master] +jobs: + pharynx: + name: build phar + permissions: + contents: write + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + - run: composer install --ignore-platform-reqs + - uses: SOF3/pharynx@v0.2 + id: pharynx + with: + additional-assets: | + icon.png + - uses: actions/upload-artifact@v4 + with: + name: Smaccer.phar + path: ${{steps.pharynx.outputs.output-phar}} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db7a216 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignore Composer's vendor directory +vendor/ diff --git a/.poggit.yml b/.poggit.yml new file mode 100644 index 0000000..fdeaab8 --- /dev/null +++ b/.poggit.yml @@ -0,0 +1,8 @@ +--- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/AIPTU/Smaccer +branches: +- poggit/master +projects: + Smaccer: + path: "" + icon: "icon.png" +... diff --git a/README.md b/README.md index 459d93b..207c853 100644 --- a/README.md +++ b/README.md @@ -1 +1,174 @@ -# Smaccer \ No newline at end of file +# Smaccer + +Smaccer is a powerful and easy-to-use PocketMine-MP plugin designed for managing NPCs (Non-Player Characters) in your Minecraft world. Whether you want to create interactive characters, organize your NPCs, or automate tasks, Smaccer provides all the necessary tools to bring your world to life. + +## Features + +- **Create NPCs**: Easily create new NPCs to populate your world. +- **Edit NPCs**: Modify existing NPCs to update their appearance, behavior, or properties. +- **Delete NPCs**: Remove unwanted NPCs from your world with simple commands. +- **Move NPCs**: Effortlessly move NPCs to different locations or players. +- **List NPCs**: View a comprehensive list of all NPCs in your world. +- **Teleport**: Quickly teleport to NPCs or move other players to NPCs. +- **Customizable Configuration**: Tailor the plugin's settings to fit your needs without restarting the server. +- **Manage Permissions**: Fine-tune permissions to control who can interact with and manage NPCs. +- **Server Queries**: Check the status of remote servers, including player counts and server uptime. +- **World Queries**: Monitor player counts and world statuses across multiple worlds. +- **Customizable Messages**: Define and customize the display messages for server and world queries through configuration files. +- **Asynchronous Tasks**: Perform server queries asynchronously to avoid blocking the main server thread. + +## Commands + +- **`/smaccer about`**: Displays information about the plugin. +- **`/smaccer create`**: Creates a new NPC entity. +- **`/smaccer delete`**: Deletes an NPC entity. +- **`/smaccer edit`**: Edits an NPC entity. +- **`/smaccer id`**: Retrieves the ID of an NPC entity. +- **`/smaccer list`**: Lists all NPC entities in the world. +- **`/smaccer move`**: Moves an NPC entity to a specified player or location. +- **`/smaccer reload`**: Reloads the plugin configuration or emotes. +- **`/smaccer teleport`**: Teleports a player to an NPC entity or vice versa. + +## Permissions + +Grant these permissions to specific player groups or individuals using a permissions management plugin of your choice. + +| Permission | Description | Default | +|------------|-------------|---------| +| `smaccer.bypass.cooldown` | Allows players to bypass cooldown. | op | +| `smaccer.command.about` | Allows players to display information about the plugin (`/smaccer about`). | op | +| `smaccer.command.create.self` | Allows players to create their own entities (`/smaccer create`). | op | +| `smaccer.command.create.others` | Allows players to create entities owned by others (`/smaccer create`). | op | +| `smaccer.command.delete.self` | Allows players to delete their own entities (`/smaccer delete`). | op | +| `smaccer.command.delete.others` | Allows players to delete entities owned by others (`/smaccer delete`). | op | +| `smaccer.command.edit.self` | Allows players to edit their own entities (`/smaccer edit`). | op | +| `smaccer.command.edit.others` | Allows players to edit entities owned by others (`/smaccer edit`). | op | +| `smaccer.command.id` | Allows players to retrieve entity IDs (`/smaccer id`). | op | +| `smaccer.command.list` | Allows players to list all entities in the worlds (`/smaccer list`). | op | +| `smaccer.command.move.self` | Allows players to move an entity to themselves (`/smaccer move`). | op | +| `smaccer.command.move.others` | Allows players to move an entity to another player (`/smaccer move`). | op | +| `smaccer.command.reload.config` | Allows players to reload the configuration (`/smaccer reload`). | op | +| `smaccer.command.reload.emotes` | Allows players to reload the emotes (`/smaccer reload`). | op | +| `smaccer.command.teleport.self` | Allows players to teleport to an entity (`/smaccer teleport`). | op | +| `smaccer.command.teleport.others` | Allows players to teleport other players to an entity (`/smaccer teleport`). | op | + +## Configuration + +Smaccer offers a customizable configuration to tailor the NPC settings to your preferences. Below is an example of the configuration file: + +```yaml +# Smaccer Configuration + +# Do not change this (Only for internal use)! +config-version: 1.2 + +# Enable or disable the auto update checker notifier. +update_notifier: true + +# World Query Message Formats +# Customize how world information is displayed in the nametag. + +# When all specified worlds are loaded. +# Placeholders: +# - {world_names}: Comma-separated list of loaded world names. +# - {count}: Total player count across loaded worlds. +world_message_format: "§aWorlds: §b{world_names} §a| Players: §e{count}" + +# When some or all specified worlds are not loaded. +# Placeholders: +# - {world_names}: Comma-separated list of loaded world names. +# - {not_loaded_worlds}: Comma-separated list of worlds not loaded. +# - {count}: Total player count across loaded worlds. +world_not_loaded_format: "§cWorlds: §b{world_names} §c| Not Loaded: §7{not_loaded_worlds} §c| Players: §e{count}" + +# Server Query Message Formats +# Customize how server information is displayed in the nametag. + +# When the server is online. +# Placeholders: +# - {online}: Current number of players online. +# - {max_online}: Maximum number of players allowed online. +server_online_format: "§aServer: §b{online}§a/§b{max_online} §aonline" + +# When the server is offline. +server_offline_format: "§cServer: Offline" + +# Default settings for NPCs. +npc-default-settings: + # Cooldown settings for NPC commands. + # - enabled: Whether command cooldown is enabled or not. + # - value: Cooldown duration in seconds. + commandCooldown: + enabled: true + value: 3 + + # Rotation settings for NPC behavior. + # - enabled: Whether rotation is enabled or not. + # - maxDistance: Maximum distance for NPC rotation. + rotation: + enabled: true + maxDistance: 8 + + # Nametag visibility settings for NPCs. + # - enabled: Whether nametag visibility is enabled or not. + nametagVisible: + enabled: true + + # Default entity visibility settings. + # - value: Integer representing visibility level. + # 0: Visible to everyone. + # 1: Visible only to the creator. + # 2: Invisible to everyone. + entityVisibility: + value: 0 + + # Slap settings for NPCs. + # - enabled: Whether slap-back action is enabled or not. + # Note: Set to true if slap action is intended for human NPCs. + slapBack: + enabled: true + + # Cooldown settings for NPC emotes. + # - enabled: Whether emote cooldown is enabled or not. + # - value: Cooldown duration in seconds. + # Note: Emotes are non-interactive gestures or expressions performed by NPCs. + emoteCooldown: + enabled: true + value: 5 + + # Cooldown settings for NPC action emotes. + # - enabled: Whether action emote cooldown is enabled or not. + # - value: Cooldown duration in seconds. + # Note: Action emotes are interactive gestures or expressions that trigger specific actions when performed by NPCs. + actionEmoteCooldown: + enabled: true + value: 5 + + # Gravity settings for NPCs. + # - enabled: Whether gravity is enabled or not. + gravity: + enabled: true + +``` + +## Images + + + + + + + +## Upcoming Features + +- Currently none planned. You can contribute or suggest for new features. + +## Credits + +- [Bedrock-Emotes by TwistedAsylumMC](https://github.com/TwistedAsylumMC/Bedrock-Emotes) for providing the emotes. +- [CPlot by ColinHDev](https://github.com/ColinHDev/CPlot) for implementing promises. + +## Additional Notes + +- If you find bugs or want to give suggestions, please visit [here](https://github.com/AIPTU/Smaccer/issues). +- We accept all contributions! If you want to contribute, please make a pull request in [here](https://github.com/AIPTU/Smaccer/pulls). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2fb78ea --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "aiptu/smaccer", + "description": "A PocketMine-MP plugin NPCs", + "license": "MIT", + "type": "project", + "require": { + "frago9876543210/forms": "dev-master", + "ifera-mc/update-notifier": "dev-master", + "paroxity/commando": "^3.2", + "pocketmine/pocketmine-mp": "^5.16" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/AIPTU/forms" + }, + { + "type": "vcs", + "url": "https://github.com/ifera-mc/UpdateNotifier.git" + } + ], + "autoload": { + "classmap": ["src"] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..d628f99 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1445 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4a3219896de3940a1cf60ff0626906bd", + "packages": [ + { + "name": "adhocore/json-comment", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/adhocore/php-json-comment.git", + "reference": "651023f9fe52e9efa2198cbaf6e481d1968e2377" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/adhocore/php-json-comment/zipball/651023f9fe52e9efa2198cbaf6e481d1968e2377", + "reference": "651023f9fe52e9efa2198cbaf6e481d1968e2377", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^7.5 || ^8.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ahc\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jitendra Adhikari", + "email": "jiten.adhikary@gmail.com" + } + ], + "description": "Lightweight JSON comment stripper library for PHP", + "keywords": [ + "comment", + "json", + "strip-comment" + ], + "support": { + "issues": "https://github.com/adhocore/php-json-comment/issues", + "source": "https://github.com/adhocore/php-json-comment/tree/1.2.1" + }, + "funding": [ + { + "url": "https://paypal.me/ji10", + "type": "custom" + }, + { + "url": "https://github.com/adhocore", + "type": "github" + } + ], + "time": "2022-10-02T11:22:07+00:00" + }, + { + "name": "brick/math", + "version": "0.12.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" + }, + { + "name": "frago9876543210/forms", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/AIPTU/forms.git", + "reference": "ac3da80e1613c309acd143cdbb62eedb7625b3cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/AIPTU/forms/zipball/ac3da80e1613c309acd143cdbb62eedb7625b3cd", + "reference": "ac3da80e1613c309acd143cdbb62eedb7625b3cd", + "shasum": "" + }, + "require": { + "pocketmine/pocketmine-mp": "^5.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-strict-rules": "^1.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "virion": { + "namespace-root": "frago9876543210\\forms", + "spec": "3.0" + } + }, + "autoload": { + "psr-4": { + "frago9876543210\\forms\\": "/src/frago9876543210/forms" + } + }, + "support": { + "source": "https://github.com/AIPTU/forms/tree/master" + }, + "time": "2024-07-29T11:46:02+00:00" + }, + { + "name": "ifera-mc/update-notifier", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/ifera-mc/UpdateNotifier.git", + "reference": "da9a187fedc0811afb0a0b264b31f933fe4e6a43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ifera-mc/UpdateNotifier/zipball/da9a187fedc0811afb0a0b264b31f933fe4e6a43", + "reference": "da9a187fedc0811afb0a0b264b31f933fe4e6a43", + "shasum": "" + }, + "require": { + "pocketmine/pocketmine-mp": "^4.0.0 || ^5.0.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "virion": { + "spec": "3.0", + "namespace-root": "JackMD\\UpdateNotifier" + } + }, + "autoload": { + "classmap": [ + "src" + ] + }, + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "JackMD", + "email": "jackmtaylor.jmt@gmail.com" + }, + { + "name": "Ifera", + "email": "contact@tayyab.dev" + }, + { + "name": "Sandertv", + "email": "st.ten.veldhuis@gmail.com" + } + ], + "description": "A handy virion for PocketMine-MP plugin developers that checks if a new release for a plugin is available on Poggit. If so then it notifies the user about the new release.", + "support": { + "source": "https://github.com/ifera-mc/UpdateNotifier/tree/master" + }, + "time": "2024-07-29T20:17:07+00:00" + }, + { + "name": "muqsit/simple-packet-handler", + "version": "0.1.4", + "source": { + "type": "git", + "url": "https://github.com/Muqsit/SimplePacketHandler.git", + "reference": "8121eca3f21cb9912c3ac8406a11f70cf105c905" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Muqsit/SimplePacketHandler/zipball/8121eca3f21cb9912c3ac8406a11f70cf105c905", + "reference": "8121eca3f21cb9912c3ac8406a11f70cf105c905", + "shasum": "" + }, + "require": { + "pocketmine/pocketmine-mp": "^5.0.0" + }, + "type": "library", + "extra": { + "virion": { + "spec": "3.0", + "namespace-root": "muqsit\\simplepackethandler" + } + }, + "autoload": { + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "authors": [ + { + "name": "Muqsit", + "email": "hackhack05@gmail.com" + } + ], + "description": "Handle specific data packets (virion for PMMP API 4.0.0)", + "support": { + "issues": "https://github.com/Muqsit/SimplePacketHandler/issues", + "source": "https://github.com/Muqsit/SimplePacketHandler/tree/0.1.4" + }, + "time": "2024-06-14T08:39:49+00:00" + }, + { + "name": "paroxity/commando", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/Paroxity/Commando.git", + "reference": "625f33888132cfadc570be33001488d10e84f0d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Paroxity/Commando/zipball/625f33888132cfadc570be33001488d10e84f0d0", + "reference": "625f33888132cfadc570be33001488d10e84f0d0", + "shasum": "" + }, + "require": { + "muqsit/simple-packet-handler": "^0.1.4", + "pocketmine/pocketmine-mp": "^5.0" + }, + "type": "library", + "extra": { + "virion": { + "spec": "3.0", + "namespace-root": "CortexPE\\Commando" + } + }, + "autoload": { + "psr-4": { + "CortexPE\\Commando\\": [ + "src/CortexPE/Commando/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-only" + ], + "description": "A Command Framework virion for PocketMine-MP ", + "support": { + "issues": "https://github.com/Paroxity/Commando/issues", + "source": "https://github.com/Paroxity/Commando/tree/3.2.1" + }, + "time": "2024-06-15T00:02:27+00:00" + }, + { + "name": "pocketmine/bedrock-block-upgrade-schema", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockBlockUpgradeSchema.git", + "reference": "53d3a41c37ce90d58b33130cdadad08e442d7c47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/53d3a41c37ce90d58b33130cdadad08e442d7c47", + "reference": "53d3a41c37ce90d58b33130cdadad08e442d7c47", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "description": "Schemas describing how to upgrade saved block data in older Minecraft: Bedrock Edition world saves", + "support": { + "issues": "https://github.com/pmmp/BedrockBlockUpgradeSchema/issues", + "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/4.3.0" + }, + "time": "2024-08-13T18:04:27+00:00" + }, + { + "name": "pocketmine/bedrock-data", + "version": "2.12.0+bedrock-1.21.20", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockData.git", + "reference": "d4ee3d08964fa16fbbdd04af1fb52bbde540b665" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/d4ee3d08964fa16fbbdd04af1fb52bbde540b665", + "reference": "d4ee3d08964fa16fbbdd04af1fb52bbde540b665", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP", + "support": { + "issues": "https://github.com/pmmp/BedrockData/issues", + "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.20" + }, + "time": "2024-08-15T12:50:26+00:00" + }, + { + "name": "pocketmine/bedrock-item-upgrade-schema", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockItemUpgradeSchema.git", + "reference": "35c18d093fc2b12da8737b2edb2c3ad6a14a53dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/35c18d093fc2b12da8737b2edb2c3ad6a14a53dd", + "reference": "35c18d093fc2b12da8737b2edb2c3ad6a14a53dd", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "description": "JSON schemas for upgrading items found in older Minecraft: Bedrock world saves", + "support": { + "issues": "https://github.com/pmmp/BedrockItemUpgradeSchema/issues", + "source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.11.0" + }, + "time": "2024-08-13T18:06:25+00:00" + }, + { + "name": "pocketmine/bedrock-protocol", + "version": "33.0.0+bedrock-1.21.20", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BedrockProtocol.git", + "reference": "e2264137c5cd0522de2c6ee4921a3a803818ea32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/e2264137c5cd0522de2c6ee4921a3a803818ea32", + "reference": "e2264137c5cd0522de2c6ee4921a3a803818ea32", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.1", + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/color": "^0.2.0 || ^0.3.0", + "pocketmine/math": "^0.3.0 || ^0.4.0 || ^1.0.0", + "pocketmine/nbt": "^1.0.0", + "ramsey/uuid": "^4.1" + }, + "require-dev": { + "phpstan/phpstan": "1.11.9", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpstan/phpstan-strict-rules": "^1.0.0", + "phpunit/phpunit": "^9.5 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\network\\mcpe\\protocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP", + "support": { + "issues": "https://github.com/pmmp/BedrockProtocol/issues", + "source": "https://github.com/pmmp/BedrockProtocol/tree/33.0.0+bedrock-1.21.20" + }, + "time": "2024-08-15T23:07:53+00:00" + }, + { + "name": "pocketmine/binaryutils", + "version": "0.2.6", + "source": { + "type": "git", + "url": "https://github.com/pmmp/BinaryUtils.git", + "reference": "ccfc1899b859d45814ea3592e20ebec4cb731c84" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/ccfc1899b859d45814ea3592e20ebec4cb731c84", + "reference": "ccfc1899b859d45814ea3592e20ebec4cb731c84", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-64bit": "*" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "~1.10.3", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0.0", + "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\utils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Classes and methods for conveniently handling binary data", + "support": { + "issues": "https://github.com/pmmp/BinaryUtils/issues", + "source": "https://github.com/pmmp/BinaryUtils/tree/0.2.6" + }, + "time": "2024-03-04T15:04:17+00:00" + }, + { + "name": "pocketmine/callback-validator", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/pmmp/CallbackValidator.git", + "reference": "64787469766bcaa7e5885242e85c23c25e8c55a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/CallbackValidator/zipball/64787469766bcaa7e5885242e85c23c25e8c55a2", + "reference": "64787469766bcaa7e5885242e85c23c25e8c55a2", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 || ^8.0" + }, + "replace": { + "daverandom/callback-validator": "*" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "0.12.59", + "phpstan/phpstan-strict-rules": "^0.12.4", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "DaveRandom\\CallbackValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "cw@daverandom.com" + } + ], + "description": "Fork of daverandom/callback-validator - Tools for validating callback signatures", + "support": { + "issues": "https://github.com/pmmp/CallbackValidator/issues", + "source": "https://github.com/pmmp/CallbackValidator/tree/1.0.3" + }, + "time": "2020-12-11T01:45:37+00:00" + }, + { + "name": "pocketmine/color", + "version": "0.3.1", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Color.git", + "reference": "a0421f1e9e0b0c619300fb92d593283378f6a5e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Color/zipball/a0421f1e9e0b0c619300fb92d593283378f6a5e1", + "reference": "a0421f1e9e0b0c619300fb92d593283378f6a5e1", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.3", + "phpstan/phpstan-strict-rules": "^1.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\color\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Color handling library used by PocketMine-MP and related projects", + "support": { + "issues": "https://github.com/pmmp/Color/issues", + "source": "https://github.com/pmmp/Color/tree/0.3.1" + }, + "time": "2023-04-10T11:38:05+00:00" + }, + { + "name": "pocketmine/errorhandler", + "version": "0.7.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/ErrorHandler.git", + "reference": "cae94884368a74ece5294b9ff7fef18732dcd921" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/ErrorHandler/zipball/cae94884368a74ece5294b9ff7fef18732dcd921", + "reference": "cae94884368a74ece5294b9ff7fef18732dcd921", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "~1.10.3", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\errorhandler\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Utilities to handle nasty PHP E_* errors in a usable way", + "support": { + "issues": "https://github.com/pmmp/ErrorHandler/issues", + "source": "https://github.com/pmmp/ErrorHandler/tree/0.7.0" + }, + "time": "2024-04-02T18:29:54+00:00" + }, + { + "name": "pocketmine/locale-data", + "version": "2.19.6", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Language.git", + "reference": "93e473e20e7f4515ecf45c5ef0f9155b9247a86e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Language/zipball/93e473e20e7f4515ecf45c5ef0f9155b9247a86e", + "reference": "93e473e20e7f4515ecf45c5ef0f9155b9247a86e", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "description": "Language resources used by PocketMine-MP", + "support": { + "issues": "https://github.com/pmmp/Language/issues", + "source": "https://github.com/pmmp/Language/tree/2.19.6" + }, + "time": "2023-08-08T16:53:23+00:00" + }, + { + "name": "pocketmine/log", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Log.git", + "reference": "e6c912c0f9055c81d23108ec2d179b96f404c043" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Log/zipball/e6c912c0f9055c81d23108ec2d179b96f404c043", + "reference": "e6c912c0f9055c81d23108ec2d179b96f404c043", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "pocketmine/spl": "<0.4" + }, + "require-dev": { + "phpstan/phpstan": "0.12.88", + "phpstan/phpstan-strict-rules": "^0.12.2" + }, + "type": "library", + "autoload": { + "classmap": [ + "./src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Logging components used by PocketMine-MP and related projects", + "support": { + "issues": "https://github.com/pmmp/Log/issues", + "source": "https://github.com/pmmp/Log/tree/0.4.0" + }, + "time": "2021-06-18T19:08:09+00:00" + }, + { + "name": "pocketmine/math", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Math.git", + "reference": "dc132d93595b32e9f210d78b3c8d43c662a5edbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Math/zipball/dc132d93595b32e9f210d78b3c8d43c662a5edbf", + "reference": "dc132d93595b32e9f210d78b3c8d43c662a5edbf", + "shasum": "" + }, + "require": { + "php": "^8.0", + "php-64bit": "*" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "~1.10.3", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "PHP library containing math related code used in PocketMine-MP", + "support": { + "issues": "https://github.com/pmmp/Math/issues", + "source": "https://github.com/pmmp/Math/tree/1.0.0" + }, + "time": "2023-08-03T12:56:33+00:00" + }, + { + "name": "pocketmine/nbt", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/NBT.git", + "reference": "20540271cb59e04672cb163dca73366f207974f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/NBT/zipball/20540271cb59e04672cb163dca73366f207974f1", + "reference": "20540271cb59e04672cb163dca73366f207974f1", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-64bit": "*", + "pocketmine/binaryutils": "^0.2.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "1.10.25", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\nbt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "PHP library for working with Named Binary Tags", + "support": { + "issues": "https://github.com/pmmp/NBT/issues", + "source": "https://github.com/pmmp/NBT/tree/1.0.0" + }, + "time": "2023-07-14T13:01:49+00:00" + }, + { + "name": "pocketmine/netresearch-jsonmapper", + "version": "v4.4.999", + "source": { + "type": "git", + "url": "https://github.com/pmmp/netresearch-jsonmapper.git", + "reference": "9a6610033d56e358e86a3e4fd5f87063c7318833" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/netresearch-jsonmapper/zipball/9a6610033d56e358e86a3e4fd5f87063c7318833", + "reference": "9a6610033d56e358e86a3e4fd5f87063c7318833", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "replace": { + "netresearch/jsonmapper": "~4.2.0" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Fork of netresearch/jsonmapper with security fixes needed by pocketmine/pocketmine-mp", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/pmmp/netresearch-jsonmapper/tree/v4.4.999" + }, + "time": "2024-02-23T13:17:01+00:00" + }, + { + "name": "pocketmine/pocketmine-mp", + "version": "5.18.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/PocketMine-MP.git", + "reference": "9176b2494a4a84729f9cc4dd1f75dd3ec2c8618d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/PocketMine-MP/zipball/9176b2494a4a84729f9cc4dd1f75dd3ec2c8618d", + "reference": "9176b2494a4a84729f9cc4dd1f75dd3ec2c8618d", + "shasum": "" + }, + "require": { + "adhocore/json-comment": "~1.2.0", + "composer-runtime-api": "^2.0", + "ext-chunkutils2": "^0.3.1", + "ext-crypto": "^0.3.1", + "ext-ctype": "*", + "ext-curl": "*", + "ext-date": "*", + "ext-gmp": "*", + "ext-hash": "*", + "ext-igbinary": "^3.0.1", + "ext-json": "*", + "ext-leveldb": "^0.2.1 || ^0.3.0", + "ext-mbstring": "*", + "ext-morton": "^0.1.0", + "ext-openssl": "*", + "ext-pcre": "*", + "ext-phar": "*", + "ext-pmmpthread": "^6.1.0", + "ext-reflection": "*", + "ext-simplexml": "*", + "ext-sockets": "*", + "ext-spl": "*", + "ext-yaml": ">=2.0.0", + "ext-zip": "*", + "ext-zlib": ">=1.2.11", + "php": "^8.1", + "php-64bit": "*", + "pocketmine/bedrock-block-upgrade-schema": "~4.3.0+bedrock-1.21.20", + "pocketmine/bedrock-data": "~2.12.0+bedrock-1.21.20", + "pocketmine/bedrock-item-upgrade-schema": "~1.11.0+bedrock-1.21.20", + "pocketmine/bedrock-protocol": "~33.0.0+bedrock-1.21.20", + "pocketmine/binaryutils": "^0.2.1", + "pocketmine/callback-validator": "^1.0.2", + "pocketmine/color": "^0.3.0", + "pocketmine/errorhandler": "^0.7.0", + "pocketmine/locale-data": "~2.19.0", + "pocketmine/log": "^0.4.0", + "pocketmine/math": "~1.0.0", + "pocketmine/nbt": "~1.0.0", + "pocketmine/netresearch-jsonmapper": "~v4.4.999", + "pocketmine/raklib": "~1.1.0", + "pocketmine/raklib-ipc": "~1.0.0", + "pocketmine/snooze": "^0.5.0", + "ramsey/uuid": "~4.7.0", + "symfony/filesystem": "~6.4.0" + }, + "require-dev": { + "phpstan/phpstan": "1.11.10", + "phpstan/phpstan-phpunit": "^1.1.0", + "phpstan/phpstan-strict-rules": "^1.2.0", + "phpunit/phpunit": "^10.5.24" + }, + "type": "project", + "autoload": { + "files": [ + "src/CoreConstants.php" + ], + "psr-4": { + "pocketmine\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "A server software for Minecraft: Bedrock Edition written in PHP", + "homepage": "https://pmmp.io", + "support": { + "issues": "https://github.com/pmmp/PocketMine-MP/issues", + "source": "https://github.com/pmmp/PocketMine-MP/tree/5.18.0" + }, + "funding": [ + { + "url": "https://github.com/pmmp/PocketMine-MP#donate", + "type": "custom" + }, + { + "url": "https://www.patreon.com/pocketminemp", + "type": "patreon" + } + ], + "time": "2024-08-16T12:54:59+00:00" + }, + { + "name": "pocketmine/raklib", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/pmmp/RakLib.git", + "reference": "be2783be516bf6e2872ff5c81fb9048596617b97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/RakLib/zipball/be2783be516bf6e2872ff5c81fb9048596617b97", + "reference": "be2783be516bf6e2872ff5c81fb9048596617b97", + "shasum": "" + }, + "require": { + "ext-sockets": "*", + "php": "^8.1", + "php-64bit": "*", + "php-ipv6": "*", + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/log": "^0.3.0 || ^0.4.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.1", + "phpstan/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "raklib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "description": "A RakNet server implementation written in PHP", + "support": { + "issues": "https://github.com/pmmp/RakLib/issues", + "source": "https://github.com/pmmp/RakLib/tree/1.1.1" + }, + "time": "2024-03-04T14:02:14+00:00" + }, + { + "name": "pocketmine/raklib-ipc", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/pmmp/RakLibIpc.git", + "reference": "ce632ef2c6743e71eddb5dc329c49af6555f90bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/RakLibIpc/zipball/ce632ef2c6743e71eddb5dc329c49af6555f90bc", + "reference": "ce632ef2c6743e71eddb5dc329c49af6555f90bc", + "shasum": "" + }, + "require": { + "php": "^8.0", + "php-64bit": "*", + "pocketmine/binaryutils": "^0.2.0", + "pocketmine/raklib": "^0.15.0 || ^1.0.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.1", + "phpstan/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "raklib\\server\\ipc\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "description": "Channel-based protocols for inter-thread/inter-process communication with RakLib", + "support": { + "issues": "https://github.com/pmmp/RakLibIpc/issues", + "source": "https://github.com/pmmp/RakLibIpc/tree/1.0.1" + }, + "time": "2024-03-01T15:55:05+00:00" + }, + { + "name": "pocketmine/snooze", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/pmmp/Snooze.git", + "reference": "a86d9ee60ce44755d166d3c7ba4b8b8be8360915" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pmmp/Snooze/zipball/a86d9ee60ce44755d166d3c7ba4b8b8be8360915", + "reference": "a86d9ee60ce44755d166d3c7ba4b8b8be8360915", + "shasum": "" + }, + "require": { + "ext-pmmpthread": "^6.0", + "php-64bit": "^8.1" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "1.10.3", + "phpstan/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "pocketmine\\snooze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "Thread notification management library for code using the pthreads extension", + "support": { + "issues": "https://github.com/pmmp/Snooze/issues", + "source": "https://github.com/pmmp/Snooze/tree/0.5.0" + }, + "time": "2023-05-22T23:43:01+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.6", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2024-04-27T21:32:50+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b51ef8059159330b74a4d52f68e671033c0fe463" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463", + "reference": "b51ef8059159330b74a4d52f68e671033c0fe463", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-06-28T09:49:33+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T15:07:36+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-06-19T12:30:46+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "frago9876543210/forms": 20, + "ifera-mc/update-notifier": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/faces/Allay.png b/faces/Allay.png deleted file mode 100644 index 024b82d..0000000 Binary files a/faces/Allay.png and /dev/null differ diff --git a/faces/Armadillo.png b/faces/Armadillo.png deleted file mode 100644 index 6c1d7e2..0000000 Binary files a/faces/Armadillo.png and /dev/null differ diff --git a/faces/Axolotl.png b/faces/Axolotl.png deleted file mode 100644 index eae1b08..0000000 Binary files a/faces/Axolotl.png and /dev/null differ diff --git a/faces/Bat.png b/faces/Bat.png deleted file mode 100644 index b765ed9..0000000 Binary files a/faces/Bat.png and /dev/null differ diff --git a/faces/Bee.png b/faces/Bee.png deleted file mode 100644 index 2d95cce..0000000 Binary files a/faces/Bee.png and /dev/null differ diff --git a/faces/Blaze.png b/faces/Blaze.png deleted file mode 100644 index 54ad7c6..0000000 Binary files a/faces/Blaze.png and /dev/null differ diff --git a/faces/Bogged.png b/faces/Bogged.png deleted file mode 100644 index 2b9ec98..0000000 Binary files a/faces/Bogged.png and /dev/null differ diff --git a/faces/Breeze.png b/faces/Breeze.png deleted file mode 100644 index d7947a4..0000000 Binary files a/faces/Breeze.png and /dev/null differ diff --git a/faces/Camel.png b/faces/Camel.png deleted file mode 100644 index 6305e56..0000000 Binary files a/faces/Camel.png and /dev/null differ diff --git a/faces/Cat.png b/faces/Cat.png deleted file mode 100644 index 1f5c58c..0000000 Binary files a/faces/Cat.png and /dev/null differ diff --git a/faces/CaveSpider.png b/faces/CaveSpider.png deleted file mode 100644 index 5cc13f2..0000000 Binary files a/faces/CaveSpider.png and /dev/null differ diff --git a/faces/Chicken.png b/faces/Chicken.png deleted file mode 100644 index 7c8d0d8..0000000 Binary files a/faces/Chicken.png and /dev/null differ diff --git a/faces/Cod.png b/faces/Cod.png deleted file mode 100644 index 70e3287..0000000 Binary files a/faces/Cod.png and /dev/null differ diff --git a/faces/Cow.png b/faces/Cow.png deleted file mode 100644 index 657dc4c..0000000 Binary files a/faces/Cow.png and /dev/null differ diff --git a/faces/Creeper.png b/faces/Creeper.png deleted file mode 100644 index 7607708..0000000 Binary files a/faces/Creeper.png and /dev/null differ diff --git a/faces/Dolphin.png b/faces/Dolphin.png deleted file mode 100644 index 97dce63..0000000 Binary files a/faces/Dolphin.png and /dev/null differ diff --git a/faces/Donkey.png b/faces/Donkey.png deleted file mode 100644 index c6d33c4..0000000 Binary files a/faces/Donkey.png and /dev/null differ diff --git a/faces/Drowned.png b/faces/Drowned.png deleted file mode 100644 index c8c2fdf..0000000 Binary files a/faces/Drowned.png and /dev/null differ diff --git a/faces/ElderGuardian.png b/faces/ElderGuardian.png deleted file mode 100644 index f2aab98..0000000 Binary files a/faces/ElderGuardian.png and /dev/null differ diff --git a/faces/EnderDragon.png b/faces/EnderDragon.png deleted file mode 100644 index 43c7d37..0000000 Binary files a/faces/EnderDragon.png and /dev/null differ diff --git a/faces/Enderman.png b/faces/Enderman.png deleted file mode 100644 index 385b327..0000000 Binary files a/faces/Enderman.png and /dev/null differ diff --git a/faces/Endermite.png b/faces/Endermite.png deleted file mode 100644 index 4215a4b..0000000 Binary files a/faces/Endermite.png and /dev/null differ diff --git a/faces/EvocationIllager.png b/faces/EvocationIllager.png deleted file mode 100644 index 3f2df72..0000000 Binary files a/faces/EvocationIllager.png and /dev/null differ diff --git a/faces/Fox.png b/faces/Fox.png deleted file mode 100644 index ab561f9..0000000 Binary files a/faces/Fox.png and /dev/null differ diff --git a/faces/Frog.png b/faces/Frog.png deleted file mode 100644 index bf5b660..0000000 Binary files a/faces/Frog.png and /dev/null differ diff --git a/faces/Ghast.png b/faces/Ghast.png deleted file mode 100644 index 7c43789..0000000 Binary files a/faces/Ghast.png and /dev/null differ diff --git a/faces/GlowSquid.png b/faces/GlowSquid.png deleted file mode 100644 index f480a56..0000000 Binary files a/faces/GlowSquid.png and /dev/null differ diff --git a/faces/Goat.png b/faces/Goat.png deleted file mode 100644 index 04ac785..0000000 Binary files a/faces/Goat.png and /dev/null differ diff --git a/faces/Guardian.png b/faces/Guardian.png deleted file mode 100644 index 0c16a02..0000000 Binary files a/faces/Guardian.png and /dev/null differ diff --git a/faces/Hoglin.png b/faces/Hoglin.png deleted file mode 100644 index d758664..0000000 Binary files a/faces/Hoglin.png and /dev/null differ diff --git a/faces/Horse.png b/faces/Horse.png deleted file mode 100644 index 1e956f1..0000000 Binary files a/faces/Horse.png and /dev/null differ diff --git a/faces/Human.png b/faces/Human.png deleted file mode 100644 index 4188850..0000000 Binary files a/faces/Human.png and /dev/null differ diff --git a/faces/Husk.png b/faces/Husk.png deleted file mode 100644 index 62e2dae..0000000 Binary files a/faces/Husk.png and /dev/null differ diff --git a/faces/IronGolem.png b/faces/IronGolem.png deleted file mode 100644 index 942e2dd..0000000 Binary files a/faces/IronGolem.png and /dev/null differ diff --git a/faces/Llama.png b/faces/Llama.png deleted file mode 100644 index 102eec1..0000000 Binary files a/faces/Llama.png and /dev/null differ diff --git a/faces/MagmaCube.png b/faces/MagmaCube.png deleted file mode 100644 index 5631a68..0000000 Binary files a/faces/MagmaCube.png and /dev/null differ diff --git a/faces/Mooshroom.png b/faces/Mooshroom.png deleted file mode 100644 index d8d3933..0000000 Binary files a/faces/Mooshroom.png and /dev/null differ diff --git a/faces/Mule.png b/faces/Mule.png deleted file mode 100644 index 22b03dc..0000000 Binary files a/faces/Mule.png and /dev/null differ diff --git a/faces/Ocelot.png b/faces/Ocelot.png deleted file mode 100644 index 87fe3e8..0000000 Binary files a/faces/Ocelot.png and /dev/null differ diff --git a/faces/Panda.png b/faces/Panda.png deleted file mode 100644 index 9a49498..0000000 Binary files a/faces/Panda.png and /dev/null differ diff --git a/faces/Parrot.png b/faces/Parrot.png deleted file mode 100644 index e862e0d..0000000 Binary files a/faces/Parrot.png and /dev/null differ diff --git a/faces/Phantom.png b/faces/Phantom.png deleted file mode 100644 index 508a7fe..0000000 Binary files a/faces/Phantom.png and /dev/null differ diff --git a/faces/Pig.png b/faces/Pig.png deleted file mode 100644 index c27d4e1..0000000 Binary files a/faces/Pig.png and /dev/null differ diff --git a/faces/Piglin.png b/faces/Piglin.png deleted file mode 100644 index b1757a6..0000000 Binary files a/faces/Piglin.png and /dev/null differ diff --git a/faces/PiglinBrute.png b/faces/PiglinBrute.png deleted file mode 100644 index 0ba14f6..0000000 Binary files a/faces/PiglinBrute.png and /dev/null differ diff --git a/faces/Pillager.png b/faces/Pillager.png deleted file mode 100644 index 976dac5..0000000 Binary files a/faces/Pillager.png and /dev/null differ diff --git a/faces/PolarBear.png b/faces/PolarBear.png deleted file mode 100644 index 87bfd5f..0000000 Binary files a/faces/PolarBear.png and /dev/null differ diff --git a/faces/Pufferfish.png b/faces/Pufferfish.png deleted file mode 100644 index 7b0573d..0000000 Binary files a/faces/Pufferfish.png and /dev/null differ diff --git a/faces/Rabbit.png b/faces/Rabbit.png deleted file mode 100644 index e3d717f..0000000 Binary files a/faces/Rabbit.png and /dev/null differ diff --git a/faces/Ravager.png b/faces/Ravager.png deleted file mode 100644 index 8499e6b..0000000 Binary files a/faces/Ravager.png and /dev/null differ diff --git a/faces/Salmon.png b/faces/Salmon.png deleted file mode 100644 index b1df7c0..0000000 Binary files a/faces/Salmon.png and /dev/null differ diff --git a/faces/Sheep.png b/faces/Sheep.png deleted file mode 100644 index 9b861f4..0000000 Binary files a/faces/Sheep.png and /dev/null differ diff --git a/faces/Shulker.png b/faces/Shulker.png deleted file mode 100644 index cada159..0000000 Binary files a/faces/Shulker.png and /dev/null differ diff --git a/faces/Silverfish.png b/faces/Silverfish.png deleted file mode 100644 index a68fa19..0000000 Binary files a/faces/Silverfish.png and /dev/null differ diff --git a/faces/Skeleton.png b/faces/Skeleton.png deleted file mode 100644 index 380e018..0000000 Binary files a/faces/Skeleton.png and /dev/null differ diff --git a/faces/SkeletonHorse.png b/faces/SkeletonHorse.png deleted file mode 100644 index 47b1022..0000000 Binary files a/faces/SkeletonHorse.png and /dev/null differ diff --git a/faces/Slime.png b/faces/Slime.png deleted file mode 100644 index f17b7de..0000000 Binary files a/faces/Slime.png and /dev/null differ diff --git a/faces/Sniffer.png b/faces/Sniffer.png deleted file mode 100644 index f3fa539..0000000 Binary files a/faces/Sniffer.png and /dev/null differ diff --git a/faces/SnowGolem.png b/faces/SnowGolem.png deleted file mode 100644 index 39a377c..0000000 Binary files a/faces/SnowGolem.png and /dev/null differ diff --git a/faces/Spider.png b/faces/Spider.png deleted file mode 100644 index 529d266..0000000 Binary files a/faces/Spider.png and /dev/null differ diff --git a/faces/Squid.png b/faces/Squid.png deleted file mode 100644 index a1c90aa..0000000 Binary files a/faces/Squid.png and /dev/null differ diff --git a/faces/Stray.png b/faces/Stray.png deleted file mode 100644 index 12fc00a..0000000 Binary files a/faces/Stray.png and /dev/null differ diff --git a/faces/Strider.png b/faces/Strider.png deleted file mode 100644 index 8b8177f..0000000 Binary files a/faces/Strider.png and /dev/null differ diff --git a/faces/Tadpole.png b/faces/Tadpole.png deleted file mode 100644 index a52e891..0000000 Binary files a/faces/Tadpole.png and /dev/null differ diff --git a/faces/TraderLlama.png b/faces/TraderLlama.png deleted file mode 100644 index fd45984..0000000 Binary files a/faces/TraderLlama.png and /dev/null differ diff --git a/faces/Tropicalfish.png b/faces/Tropicalfish.png deleted file mode 100644 index ea8ab0e..0000000 Binary files a/faces/Tropicalfish.png and /dev/null differ diff --git a/faces/Turtle.png b/faces/Turtle.png deleted file mode 100644 index e3e9639..0000000 Binary files a/faces/Turtle.png and /dev/null differ diff --git a/faces/Vex.png b/faces/Vex.png deleted file mode 100644 index bf25065..0000000 Binary files a/faces/Vex.png and /dev/null differ diff --git a/faces/Villager.png b/faces/Villager.png deleted file mode 100644 index 2d9ee82..0000000 Binary files a/faces/Villager.png and /dev/null differ diff --git a/faces/VillagerV2.png b/faces/VillagerV2.png deleted file mode 100644 index 7ae4099..0000000 Binary files a/faces/VillagerV2.png and /dev/null differ diff --git a/faces/Vindicator.png b/faces/Vindicator.png deleted file mode 100644 index fe8738f..0000000 Binary files a/faces/Vindicator.png and /dev/null differ diff --git a/faces/WanderingTrader.png b/faces/WanderingTrader.png deleted file mode 100644 index 409cb86..0000000 Binary files a/faces/WanderingTrader.png and /dev/null differ diff --git a/faces/Warden.png b/faces/Warden.png deleted file mode 100644 index 118c963..0000000 Binary files a/faces/Warden.png and /dev/null differ diff --git a/faces/Witch.png b/faces/Witch.png deleted file mode 100644 index 0659e56..0000000 Binary files a/faces/Witch.png and /dev/null differ diff --git a/faces/Wither.png b/faces/Wither.png deleted file mode 100644 index 5a58fa9..0000000 Binary files a/faces/Wither.png and /dev/null differ diff --git a/faces/WitherSkeleton.png b/faces/WitherSkeleton.png deleted file mode 100644 index 25eb5d5..0000000 Binary files a/faces/WitherSkeleton.png and /dev/null differ diff --git a/faces/Wolf.png b/faces/Wolf.png deleted file mode 100644 index 9c4c853..0000000 Binary files a/faces/Wolf.png and /dev/null differ diff --git a/faces/Zoglin.png b/faces/Zoglin.png deleted file mode 100644 index 79667a2..0000000 Binary files a/faces/Zoglin.png and /dev/null differ diff --git a/faces/Zombie.png b/faces/Zombie.png deleted file mode 100644 index ab2a75b..0000000 Binary files a/faces/Zombie.png and /dev/null differ diff --git a/faces/ZombieHorse.png b/faces/ZombieHorse.png deleted file mode 100644 index d8f9e28..0000000 Binary files a/faces/ZombieHorse.png and /dev/null differ diff --git a/faces/ZombieVillager.png b/faces/ZombieVillager.png deleted file mode 100644 index e2241e5..0000000 Binary files a/faces/ZombieVillager.png and /dev/null differ diff --git a/faces/ZombieVillagerV2.png b/faces/ZombieVillagerV2.png deleted file mode 100644 index 72c6c04..0000000 Binary files a/faces/ZombieVillagerV2.png and /dev/null differ diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..2fa6c68 Binary files /dev/null and b/icon.png differ diff --git a/image1.jpg b/image1.jpg deleted file mode 100644 index f4440f6..0000000 Binary files a/image1.jpg and /dev/null differ diff --git a/image2.jpg b/image2.jpg deleted file mode 100644 index 398f662..0000000 Binary files a/image2.jpg and /dev/null differ diff --git a/image3.jpg b/image3.jpg deleted file mode 100644 index 3831dd5..0000000 Binary files a/image3.jpg and /dev/null differ diff --git a/image4.jpg b/image4.jpg deleted file mode 100644 index 9acdf2a..0000000 Binary files a/image4.jpg and /dev/null differ diff --git a/image5.jpg b/image5.jpg deleted file mode 100644 index b946660..0000000 Binary files a/image5.jpg and /dev/null differ diff --git a/plugin.yml b/plugin.yml new file mode 100644 index 0000000..a8e1588 --- /dev/null +++ b/plugin.yml @@ -0,0 +1,54 @@ +name: Smaccer +main: aiptu\smaccer\Smaccer +version: 1.0.1 +api: [5.16.0] +author: AIPTU +permissions: + smaccer.bypass.cooldown: + description: Allows players to bypass cooldown. + default: op + smaccer.command.about: + description: Allows players to displays information about the plugin (/smaccer about). + default: op + smaccer.command.create.self: + description: Allows players to create their own entities (/smaccer create). + default: op + smaccer.command.create.others: + description: Allows players to create entities owned by others (/smaccer create). + default: op + smaccer.command.delete.self: + description: Allows players to delete their own entities (/smaccer delete). + default: op + smaccer.command.delete.others: + description: Allows players to delete entities owned by others (/smaccer delete). + default: op + smaccer.command.edit.self: + description: Allows players to edit their own entities (/smaccer edit). + default: op + smaccer.command.edit.others: + description: Allows players to edit entities owned by others (/smaccer edit). + default: op + smaccer.command.id: + description: Allows players to retrieve entity IDs (/smaccer id). + default: op + smaccer.command.list: + description: Allows players to list all entities in the worlds (/smaccer list). + default: op + smaccer.command.move.self: + description: Allows players to move an entity to themselves (/smaccer move). + default: op + smaccer.command.move.others: + description: Allows players to move an entity to another player (/smaccer move). + default: op + smaccer.command.reload.config: + description: Allows players to reload the configuration (/smaccer reload). + default: op + smaccer.command.reload.emotes: + description: Allows players to teleport to reload the emotes (/smaccer reload). + default: op + smaccer.command.teleport.self: + description: Allows players to teleport to an entity (/smaccer teleport). + default: op + smaccer.command.teleport.others: + description: Allows players to teleport other players to an entity (/smaccer teleport). + default: op diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..11b22cb --- /dev/null +++ b/renovate.json @@ -0,0 +1,19 @@ +{ + "extends": [ + "config:recommended", + ":disableDependencyDashboard" + ], + "labels": [ + "dependencies" + ], + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "patch" + ], + "automerge": true + } + ], + "automergeStrategy": "squash" +} \ No newline at end of file diff --git a/resources/config.yml b/resources/config.yml new file mode 100644 index 0000000..10d2112 --- /dev/null +++ b/resources/config.yml @@ -0,0 +1,91 @@ +# Smaccer Configuration + +# Do not change this (Only for internal use)! +config-version: 1.2 + +# Enable or disable the auto update checker notifier. +update_notifier: true + +# World Query Message Formats +# Customize how world information is displayed in the nametag. + +# When all specified worlds are loaded. +# Placeholders: +# - {world_names}: Comma-separated list of loaded world names. +# - {count}: Total player count across loaded worlds. +world_message_format: "§aWorlds: §b{world_names} §a| Players: §e{count}" + +# When some or all specified worlds are not loaded. +# Placeholders: +# - {world_names}: Comma-separated list of loaded world names. +# - {not_loaded_worlds}: Comma-separated list of worlds not loaded. +# - {count}: Total player count across loaded worlds. +world_not_loaded_format: "§cWorlds: §b{world_names} §c| Not Loaded: §7{not_loaded_worlds} §c| Players: §e{count}" + +# Server Query Message Formats +# Customize how server information is displayed in the nametag. + +# When the server is online. +# Placeholders: +# - {online}: Current number of players online. +# - {max_online}: Maximum number of players allowed online. +server_online_format: "§aServer: §b{online}§a/§b{max_online} §aonline" + +# When the server is offline. +server_offline_format: "§cServer: Offline" + +# Default settings for NPCs. +npc-default-settings: + # Cooldown settings for NPC commands. + # - enabled: Whether command cooldown is enabled or not. + # - value: Cooldown duration in seconds. + commandCooldown: + enabled: true + value: 3 + + # Rotation settings for NPC behavior. + # - enabled: Whether rotation is enabled or not. + # - maxDistance: Maximum distance for NPC rotation. + rotation: + enabled: true + maxDistance: 8 + + # Nametag visibility settings for NPCs. + # - enabled: Whether nametag visibility is enabled or not. + nametagVisible: + enabled: true + + # Default entity visibility settings. + # - value: Integer representing visibility level. + # 0: Visible to everyone. + # 1: Visible only to the creator. + # 2: Invisible to everyone. + entityVisibility: + value: 0 + + # Slap settings for NPCs. + # - enabled: Whether slap-back action is enabled or not. + # Note: Set to true if slap action is intended for human NPCs. + slapBack: + enabled: true + + # Cooldown settings for NPC emotes. + # - enabled: Whether emote cooldown is enabled or not. + # - value: Cooldown duration in seconds. + # Note: Emotes are non-interactive gestures or expressions performed by NPCs. + emoteCooldown: + enabled: true + value: 5 + + # Cooldown settings for NPC action emotes. + # - enabled: Whether action emote cooldown is enabled or not. + # - value: Cooldown duration in seconds. + # Note: Action emotes are interactive gestures or expressions that trigger specific actions when performed by NPCs. + actionEmoteCooldown: + enabled: true + value: 5 + + # Gravity settings for NPCs. + # - enabled: Whether gravity is enabled or not. + gravity: + enabled: true diff --git a/src/aiptu/smaccer/EventHandler.php b/src/aiptu/smaccer/EventHandler.php new file mode 100644 index 0000000..a3e1851 --- /dev/null +++ b/src/aiptu/smaccer/EventHandler.php @@ -0,0 +1,207 @@ +getPlayer(); + $message = trim($event->getMessage()); + + if (str_starts_with(strtolower($message), 'cancel')) { + $playerName = $player->getName(); + + if (Queue::isInAnyQueue($playerName)) { + Queue::removeFromAllQueues($playerName); + $player->sendMessage(TextFormat::GREEN . 'You have successfully left the queue.'); + } + + $event->cancel(); + } + } + + public function onQuit(PlayerQuitEvent $event) : void { + $player = $event->getPlayer(); + Queue::removeFromAllQueues($player->getName()); + } + + public function onMove(PlayerMoveEvent $event) : void { + $player = $event->getPlayer(); + + if ($event->getFrom()->distance($event->getTo()) < 0.1) { + return; + } + + $world = $player->getWorld(); + $playerLocation = $player->getLocation(); + $maxLookDistance = Smaccer::getInstance()->getDefaultSettings()->getMaxDistance(); + $boundingBox = $player->getBoundingBox()->expandedCopy($maxLookDistance, $maxLookDistance, $maxLookDistance); + + foreach ($world->getNearbyEntities($boundingBox, $player) as $entity) { + if (($entity instanceof HumanSmaccer || $entity instanceof EntitySmaccer) && $entity->canRotateToPlayers()) { + $visibility = $entity->getVisibility(); + $creator = $entity->getCreator(); + + if ($visibility === EntityVisibility::INVISIBLE_TO_EVERYONE + || ($visibility === EntityVisibility::VISIBLE_TO_CREATOR && ($creator === null || $creator !== $player))) { + continue; + } + + $entityLocation = $entity->getLocation(); + $xdiff = $playerLocation->x - $entityLocation->x; + $zdiff = $playerLocation->z - $entityLocation->z; + $ydiff = $playerLocation->y - $entityLocation->y; + + $yaw = rad2deg(atan2($zdiff, $xdiff)) - 90; + $dist = sqrt($xdiff ** 2 + $zdiff ** 2); + $pitch = rad2deg(atan2($dist, $ydiff)) - 90; + + $entity->setRotation($yaw, $pitch); + } + } + } + + public function onEffectAdd(EntityEffectAddEvent $event) : void { + $entity = $event->getEntity(); + + if ($entity instanceof HumanSmaccer || $entity instanceof EntitySmaccer) { + $event->cancel(); + } + } + + public function onAttack(EntityDamageEvent $event) : void { + if (!$event instanceof EntityDamageByEntityEvent) { + return; + } + + $damager = $event->getDamager(); + $entity = $event->getEntity(); + + if (!($entity instanceof HumanSmaccer || $entity instanceof EntitySmaccer) + || $entity->getVisibility() === EntityVisibility::INVISIBLE_TO_EVERYONE + || !$damager instanceof Player) { + return; + } + + $npcAttackEvent = new NPCAttackEvent($damager, $entity); + $npcAttackEvent->call(); + if ($npcAttackEvent->isCancelled()) { + $event->cancel(); + return; + } + + $npcId = $entity->getId(); + $playerName = $damager->getName(); + $action = Queue::getCurrentAction($playerName); + + if ($action === null) { + $event->cancel(); + return; + } + + switch ($action) { + case Queue::ACTION_EDIT: + if (!$entity->isOwnedBy($damager) && !$damager->hasPermission(Permissions::COMMAND_EDIT_OTHERS)) { + $damager->sendMessage(TextFormat::RED . "You don't have permission to edit this entity!"); + } else { + FormManager::sendEditMenuForm($damager, $entity); + } + + break; + case Queue::ACTION_DELETE: + if (!$entity->isOwnedBy($damager) && !$damager->hasPermission(Permissions::COMMAND_DELETE_OTHERS)) { + $damager->sendMessage(TextFormat::RED . "You don't have permission to delete this entity!"); + } else { + SmaccerHandler::getInstance()->despawnNPC($entity->getCreatorId(), $entity)->onCompletion( + function (bool $success) use ($damager, $npcId, $entity) : void { + $damager->sendMessage(TextFormat::GREEN . 'NPC ' . $entity->getName() . ' with ID ' . $npcId . ' despawned successfully.'); + }, + function (\Throwable $e) use ($damager) : void { + $damager->sendMessage(TextFormat::RED . 'Failed to despawn NPC: ' . $e->getMessage()); + } + ); + } + + break; + case Queue::ACTION_RETRIEVE: + $damager->sendMessage(TextFormat::GREEN . 'NPC Entity ID: ' . $npcId); + break; + } + + Queue::removeFromQueue($playerName, $action); + $event->cancel(); + } + + public function onInteract(PlayerEntityInteractEvent $event) : void { + $player = $event->getPlayer(); + $entity = $event->getEntity(); + + if (($entity instanceof HumanSmaccer || $entity instanceof EntitySmaccer) + && $entity->getVisibility() !== EntityVisibility::INVISIBLE_TO_EVERYONE) { + $npcInteractEvent = new NPCInteractEvent($player, $entity); + $npcInteractEvent->call(); + if ($npcInteractEvent->isCancelled()) { + return; + } + + if ($entity->canExecuteCommands($player)) { + $entity->executeCommands($player); + } + + if ($entity instanceof HumanSmaccer) { + $entity->slapBack(); + + if ($entity->getActionEmote() !== null) { + $emoteUuid = $entity->getActionEmote()->getUuid(); + $settings = Smaccer::getInstance()->getDefaultSettings(); + + if ($settings->isActionEmoteCooldownEnabled()) { + if ($player->hasPermission(Permissions::BYPASS_COOLDOWN) || $entity->canPerformActionEmote($emoteUuid)) { + $entity->performActionEmote($emoteUuid, [$player]); + } + } else { + $entity->performActionEmote($emoteUuid, [$player]); + } + } + } + + $event->cancel(); + } + } +} diff --git a/src/aiptu/smaccer/NPCDefaultSettings.php b/src/aiptu/smaccer/NPCDefaultSettings.php new file mode 100644 index 0000000..7465b22 --- /dev/null +++ b/src/aiptu/smaccer/NPCDefaultSettings.php @@ -0,0 +1,81 @@ +commandCooldownEnabled; + } + + public function getCommandCooldownValue() : float { + return $this->commandCooldownValue; + } + + public function isRotationEnabled() : bool { + return $this->rotationEnabled; + } + + public function getMaxDistance() : float { + return $this->maxDistance; + } + + public function isNametagVisible() : bool { + return $this->nametagVisible; + } + + public function getEntityVisibility() : EntityVisibility { + return $this->entityVisibility; + } + + public function isSlapEnabled() : bool { + return $this->slapEnabled; + } + + public function isEmoteCooldownEnabled() : bool { + return $this->emoteCooldownEnabled; + } + + public function getEmoteCooldownValue() : float { + return $this->emoteCooldownValue; + } + + public function isActionEmoteCooldownEnabled() : bool { + return $this->actionEmoteCooldownEnabled; + } + + public function getActionEmoteCooldownValue() : float { + return $this->actionEmoteCooldownValue; + } + + public function isGravityEnabled() : bool { + return $this->gravityEnabled; + } +} diff --git a/src/aiptu/smaccer/Smaccer.php b/src/aiptu/smaccer/Smaccer.php new file mode 100644 index 0000000..4196e25 --- /dev/null +++ b/src/aiptu/smaccer/Smaccer.php @@ -0,0 +1,276 @@ +registerAll(); + + if (!PacketHooker::isRegistered()) { + PacketHooker::register($this); + } + + try { + $this->loadConfig(); + } catch (\Throwable $e) { + $this->getLogger()->error('An error occurred while loading the configuration: ' . $e->getMessage()); + throw new DisablePluginException(); + } + + $this->loadEmotes(); + + $this->getServer()->getCommandMap()->register('Smaccer', new SmaccerCommand($this, 'smaccer', 'Smaccer commands.')); + + $this->getServer()->getPluginManager()->registerEvents(new EventHandler(), $this); + + if ($this->updateNotifierEnabled) { + UpdateNotifier::checkUpdate($this->getDescription()->getName(), $this->getDescription()->getVersion()); + } + } + + /** + * Loads and validates the plugin configuration from the `config.yml` file. + * If the configuration is invalid, an exception will be thrown. + * + * @throws InvalidArgumentException when the configuration is invalid + */ + private function loadConfig() : void { + $this->checkConfig(); + + $config = $this->getConfig(); + + $updateNotifierEnabled = $config->get('update_notifier'); + if (!is_bool($updateNotifierEnabled)) { + throw new InvalidArgumentException('Invalid or missing "update_notifier" value in the configuration. Please provide a boolean (true/false) value.'); + } + + $this->updateNotifierEnabled = $updateNotifierEnabled; + + $worldMessageFormat = $config->get('world_message_format'); + if (!is_string($worldMessageFormat)) { + throw new InvalidArgumentException("Invalid value for 'world_message_format'. Expected a string."); + } + + $this->worldMessageFormat = $worldMessageFormat; + + $worldNotLoadedFormat = $config->get('world_not_loaded_format'); + if (!is_string($worldNotLoadedFormat)) { + throw new InvalidArgumentException("Invalid value for 'world_not_loaded_format'. Expected a string."); + } + + $this->worldNotLoadedFormat = $worldNotLoadedFormat; + + $serverOnlineFormat = $config->get('server_online_format'); + if (!is_string($serverOnlineFormat)) { + throw new InvalidArgumentException("Invalid value for 'server_online_format'. Expected a string."); + } + + $this->serverOnlineFormat = $serverOnlineFormat; + + $serverOfflineFormat = $config->get('server_offline_format'); + if (!is_string($serverOfflineFormat)) { + throw new InvalidArgumentException("Invalid value for 'server_offline_format'. Expected a string."); + } + + $this->serverOfflineFormat = $serverOfflineFormat; + + /** + * @var array{ + * commandCooldown: array{enabled: bool, value: float|int}, + * rotation: array{enabled: bool, 'max-distance': float|int}, + * nametagVisible: array{enabled: bool}, + * entityVisibility: array{value: int}, + * slapBack: array{enabled: bool}, + * emoteCooldown: array{enabled: bool, value: float|int}, + * actionEmoteCooldown: array{enabled: bool, value: float|int} + * } $npcSettings + */ + $npcSettings = $config->get('npc-default-settings', []); + + $commandCooldownEnabled = $npcSettings['commandCooldown']['enabled'] ?? null; + $commandCooldownValue = $npcSettings['commandCooldown']['value'] ?? null; + + if (!isset($commandCooldownEnabled) || !is_bool($commandCooldownEnabled) || !isset($commandCooldownValue) || !is_numeric($commandCooldownValue)) { + throw new InvalidArgumentException("Invalid command cooldown settings. 'enabled' must be a boolean and 'value' must be provided and numeric."); + } + + $commandCooldownValue = (float) $commandCooldownValue; + + $rotationEnabled = $npcSettings['rotation']['enabled'] ?? null; + $maxDistance = $npcSettings['rotation']['maxDistance'] ?? null; + if (!isset($rotationEnabled) || !is_bool($rotationEnabled) || !isset($maxDistance) || !is_numeric($maxDistance)) { + throw new InvalidArgumentException("Invalid rotation settings. 'enabled' must be a boolean and 'maxDistance' must be provided and numeric."); + } + + $maxDistance = (float) $maxDistance; + + $nametagVisible = $npcSettings['nametagVisible']['enabled'] ?? null; + + if (!isset($nametagVisible) || !is_bool($nametagVisible)) { + throw new InvalidArgumentException("Invalid nametag visibility settings. 'enabled' must be a boolean."); + } + + $entityVisibility = $npcSettings['entityVisibility']['value'] ?? null; + + if (!isset($entityVisibility) || !is_int($entityVisibility) || $entityVisibility < 0 || $entityVisibility > 2) { + throw new InvalidArgumentException("Invalid entity visibility settings. 'value' must be an integer between 0 and 2."); + } + + $slapEnabled = $npcSettings['slapBack']['enabled'] ?? null; + if (!isset($slapEnabled) || !is_bool($slapEnabled)) { + throw new InvalidArgumentException("Invalid slap settings. 'enabled' must be a boolean."); + } + + $emoteCooldownEnabled = $npcSettings['emoteCooldown']['enabled'] ?? null; + $emoteCooldownValue = $npcSettings['emoteCooldown']['value'] ?? null; + + if (!isset($emoteCooldownEnabled) || !is_bool($emoteCooldownEnabled) || !isset($emoteCooldownValue) || !is_numeric($emoteCooldownValue)) { + throw new InvalidArgumentException("Invalid emote cooldown settings. 'enabled' must be a boolean and 'value' must be provided and numeric."); + } + + $emoteCooldownValue = (float) $emoteCooldownValue; + + $actionEmoteCooldownEnabled = $npcSettings['actionEmoteCooldown']['enabled'] ?? null; + $actionEmoteCooldownValue = $npcSettings['actionEmoteCooldown']['value'] ?? null; + + if (!isset($actionEmoteCooldownEnabled) || !is_bool($actionEmoteCooldownEnabled) || !isset($actionEmoteCooldownValue) || !is_numeric($actionEmoteCooldownValue)) { + throw new InvalidArgumentException("Invalid action emote cooldown settings. 'enabled' must be a boolean and 'value' must be provided and numeric."); + } + + $actionEmoteCooldownValue = (float) $actionEmoteCooldownValue; + + $gravityEnabled = $npcSettings['gravity']['enabled'] ?? null; + if (!isset($gravityEnabled) || !is_bool($gravityEnabled)) { + throw new InvalidArgumentException("Invalid gravity settings. 'enabled' must be a boolean."); + } + + $this->npcDefaultSettings = new NPCDefaultSettings( + $commandCooldownEnabled, + $commandCooldownValue, + $rotationEnabled, + $maxDistance, + $nametagVisible, + EntityVisibility::fromInt($entityVisibility), + $slapEnabled, + $emoteCooldownEnabled, + $emoteCooldownValue, + $actionEmoteCooldownEnabled, + $actionEmoteCooldownValue, + $gravityEnabled + ); + } + + /** + * Checks and manages the configuration for the plugin. + * Generates a new configuration if an outdated one is provided and backs up the old config. + */ + private function checkConfig() : void { + $config = $this->getConfig(); + + if (!$config->exists('config-version') || $config->get('config-version', self::CONFIG_VERSION) !== self::CONFIG_VERSION) { + $this->getLogger()->warning('An outdated config was provided; attempting to generate a new one...'); + + $oldConfigPath = Path::join($this->getDataFolder(), 'config.old.yml'); + $newConfigPath = Path::join($this->getDataFolder(), 'config.yml'); + + $filesystem = new Filesystem(); + try { + $filesystem->rename($newConfigPath, $oldConfigPath); + } catch (IOException $e) { + $this->getLogger()->critical('An error occurred while attempting to generate the new config: ' . $e->getMessage()); + throw new DisablePluginException(); + } + + $this->reloadConfig(); + } + } + + /** + * Checks if emotes are already cached and loads them synchronously if available. + * If not, it submits the LoadEmotesTask to the async task pool. + */ + private function loadEmotes() : void { + $cachedFile = EmoteUtils::getEmotesFromCache(EmoteUtils::getEmoteCachePath()); + if ($cachedFile !== null) { + /** @var array{array{uuid: string, title: string, image: string}} $emotes */ + $emotes = $cachedFile['emotes']; + $this->emoteManager = new EmoteManager($emotes); + } else { + $this->getServer()->getAsyncPool()->submitTask(new LoadEmotesTask(EmoteUtils::getEmoteCachePath())); + } + } + + public function getDefaultSettings() : NPCDefaultSettings { + return $this->npcDefaultSettings; + } + + public function getEmoteManager() : EmoteManager { + return $this->emoteManager; + } + + public function setEmoteManager(EmoteManager $emoteManager) : void { + $this->emoteManager = $emoteManager; + } + + public function getWorldMessageFormat() : string { + return $this->worldMessageFormat; + } + + public function getWorldNotLoadedFormat() : string { + return $this->worldNotLoadedFormat; + } + + public function getServerOnlineFormat() : string { + return $this->serverOnlineFormat; + } + + public function getServerOfflineFormat() : string { + return $this->serverOfflineFormat; + } +} diff --git a/src/aiptu/smaccer/command/SmaccerCommand.php b/src/aiptu/smaccer/command/SmaccerCommand.php new file mode 100644 index 0000000..0754aef --- /dev/null +++ b/src/aiptu/smaccer/command/SmaccerCommand.php @@ -0,0 +1,96 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $commandLabel, array $args) : void { + if (!$sender instanceof Player) { + throw new AssumptionFailedError(InGameRequiredConstraint::class . ' should have prevented this'); + } + + if (count($args) === 0) { + FormManager::sendMainMenu($sender, function (Player $player, string $entityType) : void { + FormManager::sendCreateNPCForm($player, $entityType, [FormManager::class, 'handleCreateNPCResponse']); + }); + + return; + } + } + + public function prepare() : void { + $this->addConstraint(new InGameRequiredConstraint($this)); + + $this->setPermissions([ + Permissions::COMMAND_ABOUT, + Permissions::COMMAND_CREATE_SELF, + Permissions::COMMAND_CREATE_OTHERS, + Permissions::COMMAND_DELETE_SELF, + Permissions::COMMAND_DELETE_OTHERS, + Permissions::COMMAND_EDIT_SELF, + Permissions::COMMAND_EDIT_OTHERS, + Permissions::COMMAND_ID, + Permissions::COMMAND_LIST, + Permissions::COMMAND_MOVE_SELF, + Permissions::COMMAND_MOVE_OTHERS, + Permissions::COMMAND_RELOAD_CONFIG, + Permissions::COMMAND_RELOAD_EMOTES, + Permissions::COMMAND_TELEPORT_SELF, + Permissions::COMMAND_TELEPORT_OTHERS, + ]); + + $plugin = $this->getOwningPlugin(); + assert($plugin instanceof Smaccer); + + $this->registerSubCommand(new AboutSubCommand($plugin, 'about', 'Display plugin information', ['version', 'ver'])); + $this->registerSubCommand(new CreateSubCommand($plugin, 'create', 'Create an NPC', ['add', 'spawn'])); + $this->registerSubCommand(new DeleteSubCommand($plugin, 'delete', 'Delete an NPC', ['remove', 'despawn'])); + $this->registerSubCommand(new EditSubCommand($plugin, 'edit', 'Edit an NPC')); + $this->registerSubCommand(new IdSubCommand($plugin, 'id', 'Check an NPC id')); + $this->registerSubCommand(new ListSubCommand($plugin, 'list', 'Get a list of NPCs in the world')); + $this->registerSubCommand(new MoveSubCommand($plugin, 'move', 'Move an NPC to a player', ['mv'])); + $this->registerSubCommand(new ReloadSubCommand($plugin, 'reload', 'Reloads the configuration or emotes')); + $this->registerSubCommand(new TeleportSubCommand($plugin, 'teleport', 'Teleport to an NPC', ['tp'])); + } +} diff --git a/src/aiptu/smaccer/command/argument/EntityTypeArgument.php b/src/aiptu/smaccer/command/argument/EntityTypeArgument.php new file mode 100644 index 0000000..b572fee --- /dev/null +++ b/src/aiptu/smaccer/command/argument/EntityTypeArgument.php @@ -0,0 +1,39 @@ +getRegisteredNPC()); + return array_map('strtolower', $names); + } + + public function getTypeName() : string { + return 'entity'; + } + + public function getEnumName() : string { + return 'entityType'; + } + + public function parse(string $argument, CommandSender $sender) : string { + return $argument; + } +} diff --git a/src/aiptu/smaccer/command/argument/ReloadTypeArgument.php b/src/aiptu/smaccer/command/argument/ReloadTypeArgument.php new file mode 100644 index 0000000..d130139 --- /dev/null +++ b/src/aiptu/smaccer/command/argument/ReloadTypeArgument.php @@ -0,0 +1,39 @@ + self::CONFIG, + 'emotes' => self::EMOTES, + ]; + + public function getTypeName() : string { + return 'reload'; + } + + public function getEnumName() : string { + return 'reloadType'; + } + + public function parse(string $argument, CommandSender $sender) : string { + return (string) $this->getValue($argument); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/AboutSubCommand.php b/src/aiptu/smaccer/command/subcommand/AboutSubCommand.php new file mode 100644 index 0000000..161125c --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/AboutSubCommand.php @@ -0,0 +1,64 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + /** @var Smaccer $plugin */ + $plugin = $this->plugin; + + $info = [ + 'Name' => $plugin->getFullName(), + 'Plugin API Version(s)' => implode(', ', $plugin->getDescription()->getCompatibleApis()), + 'Author(s)' => implode(', ', $plugin->getDescription()->getAuthors()), + ]; + + $sender->sendMessage(TextFormat::GREEN . 'Plugin Information:'); + foreach ($info as $label => $value) { + $this->sendFormattedMessage($sender, $label, $value); + } + } + + private function sendFormattedMessage(CommandSender $sender, string $label, string $value) : void { + $sender->sendMessage(sprintf( + '%s| %s | %s |', + TextFormat::GREEN, + TextFormat::WHITE . $label, + TextFormat::GREEN . $value . TextFormat::RESET + )); + } + + public function prepare() : void { + $this->setPermission(Permissions::COMMAND_ABOUT); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/CreateSubCommand.php b/src/aiptu/smaccer/command/subcommand/CreateSubCommand.php new file mode 100644 index 0000000..108a8e4 --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/CreateSubCommand.php @@ -0,0 +1,120 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + if (!$sender instanceof Player) { + throw new AssumptionFailedError(InGameRequiredConstraint::class . ' should have prevented this'); + } + + /** @var Smaccer $plugin */ + $plugin = $this->plugin; + + $target = $sender; + if (isset($args['target'])) { + if (!$sender->hasPermission(Permissions::COMMAND_CREATE_OTHERS)) { + $sender->sendMessage(TextFormat::RED . "You don't have permission to create NPCs for other players."); + return; + } + + $targetPlayer = $plugin->getServer()->getPlayerByPrefix($args['target']); + if ($targetPlayer === null) { + $sender->sendMessage(TextFormat::RED . 'Player ' . $args['target'] . ' is not online.'); + return; + } + + $target = $targetPlayer; + } + + $entityType = $args['entity']; + $nameTag = $args['nametag'] ?? null; + + $scale = $args['scale'] ?? 1.0; + if ($scale < 0.1 || $scale > 10.0) { + $sender->sendMessage(TextFormat::RED . 'Invalid scale value. Please enter a number between 0.1 and 10.0.'); + return; + } + + $isBaby = $args['isBaby'] ?? false; + + $npcData = NPCData::create() + ->setNameTag($nameTag) + ->setScale($scale) + ->setBaby($isBaby); + + SmaccerHandler::getInstance()->spawnNPC($entityType, $target, $npcData)->onCompletion( + function (Entity $entity) use ($sender, $target) : void { + if (($entity instanceof HumanSmaccer) || ($entity instanceof EntitySmaccer)) { + $npcName = $entity->getName(); + $npcId = $entity->getId(); + + if ($sender !== $target) { + $sender->sendMessage(TextFormat::GREEN . 'NPC ' . $npcName . ' created successfully! ID: ' . $npcId . ' for: ' . $target->getName()); + } else { + $sender->sendMessage(TextFormat::GREEN . 'NPC ' . $npcName . ' created successfully! ID: ' . $npcId); + } + } + }, + function (\Throwable $e) use ($sender) : void { + $sender->sendMessage(TextFormat::RED . 'Failed to spawn npc: ' . $e->getMessage()); + } + ); + } + + public function prepare() : void { + $this->addConstraint(new InGameRequiredConstraint($this)); + + $this->setPermissions([ + Permissions::COMMAND_CREATE_SELF, + Permissions::COMMAND_CREATE_OTHERS, + ]); + + $this->registerArgument(0, new EntityTypeArgument('entity')); + $this->registerArgument(1, new RawStringArgument('nametag', true)); + $this->registerArgument(2, new FloatArgument('scale', true)); + $this->registerArgument(3, new BooleanArgument('isBaby', true)); + $this->registerArgument(4, new TargetPlayerArgument(true, 'target')); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/DeleteSubCommand.php b/src/aiptu/smaccer/command/subcommand/DeleteSubCommand.php new file mode 100644 index 0000000..e693142 --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/DeleteSubCommand.php @@ -0,0 +1,91 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + if (!$sender instanceof Player) { + throw new AssumptionFailedError(InGameRequiredConstraint::class . ' should have prevented this'); + } + + $npcId = $args['npcId'] ?? null; + $playerName = $sender->getName(); + + if ($npcId === null) { + try { + if (Queue::addToQueue($playerName, Queue::ACTION_DELETE)) { + $sender->sendMessage(TextFormat::GREEN . 'You are in a queue, hit the entity to delete it. Type "cancel" to quit the queue.'); + } else { + $sender->sendMessage(TextFormat::RED . "You've been in a queue!"); + } + } catch (\InvalidArgumentException $e) { + $sender->sendMessage(TextFormat::RED . $e->getMessage()); + } + + return; + } + + /** @var Smaccer $plugin */ + $plugin = $this->plugin; + $entity = $plugin->getServer()->getWorldManager()->findEntity($npcId); + + if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { + $sender->sendMessage(TextFormat::RED . 'NPC with ID ' . $npcId . ' not found!'); + return; + } + + if (!$entity->isOwnedBy($sender) && !$sender->hasPermission(Permissions::COMMAND_DELETE_OTHERS)) { + $sender->sendMessage(TextFormat::RED . "You don't have permission to delete this entity!"); + return; + } + + FormManager::confirmDeleteNPC($sender, $entity); + } + + public function prepare() : void { + $this->addConstraint(new InGameRequiredConstraint($this)); + + $this->setPermissions([ + Permissions::COMMAND_DELETE_SELF, + Permissions::COMMAND_DELETE_OTHERS, + ]); + + $this->registerArgument(0, new IntegerArgument('npcId', true)); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/EditSubCommand.php b/src/aiptu/smaccer/command/subcommand/EditSubCommand.php new file mode 100644 index 0000000..0a0fd2b --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/EditSubCommand.php @@ -0,0 +1,91 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + if (!$sender instanceof Player) { + throw new AssumptionFailedError(InGameRequiredConstraint::class . ' should have prevented this'); + } + + $npcId = $args['npcId'] ?? null; + $playerName = $sender->getName(); + + if ($npcId === null) { + try { + if (Queue::addToQueue($playerName, Queue::ACTION_EDIT)) { + $sender->sendMessage(TextFormat::GREEN . 'You are in a queue, hit the entity to edit it. Type "cancel" to quit the queue.'); + } else { + $sender->sendMessage(TextFormat::RED . 'You are already in the queue!'); + } + } catch (\InvalidArgumentException $e) { + $sender->sendMessage(TextFormat::RED . $e->getMessage()); + } + + return; + } + + /** @var Smaccer $plugin */ + $plugin = $this->plugin; + $entity = $plugin->getServer()->getWorldManager()->findEntity($npcId); + + if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { + $sender->sendMessage(TextFormat::RED . 'NPC with ID ' . $npcId . ' not found!'); + return; + } + + if (!$entity->isOwnedBy($sender) && !$sender->hasPermission(Permissions::COMMAND_EDIT_OTHERS)) { + $sender->sendMessage(TextFormat::RED . "You don't have permission to edit this entity!"); + return; + } + + FormManager::sendEditMenuForm($sender, $entity); + } + + public function prepare() : void { + $this->addConstraint(new InGameRequiredConstraint($this)); + + $this->setPermissions([ + Permissions::COMMAND_EDIT_SELF, + Permissions::COMMAND_EDIT_OTHERS, + ]); + + $this->registerArgument(0, new IntegerArgument('npcId', true)); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/IdSubCommand.php b/src/aiptu/smaccer/command/subcommand/IdSubCommand.php new file mode 100644 index 0000000..3b9fd38 --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/IdSubCommand.php @@ -0,0 +1,60 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + if (!$sender instanceof Player) { + throw new AssumptionFailedError(InGameRequiredConstraint::class . ' should have prevented this'); + } + + $playerName = $sender->getName(); + + try { + if (Queue::addToQueue($playerName, Queue::ACTION_RETRIEVE)) { + $sender->sendMessage(TextFormat::GREEN . 'You are in the queue, hit the entity to get the id'); + } else { + $sender->sendMessage(TextFormat::RED . "You've been in a queue!"); + } + } catch (\InvalidArgumentException $e) { + $sender->sendMessage(TextFormat::RED . $e->getMessage()); + } + } + + public function prepare() : void { + $this->addConstraint(new InGameRequiredConstraint($this)); + + $this->setPermission(Permissions::COMMAND_ID); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/ListSubCommand.php b/src/aiptu/smaccer/command/subcommand/ListSubCommand.php new file mode 100644 index 0000000..a499b43 --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/ListSubCommand.php @@ -0,0 +1,66 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + if (!$sender instanceof Player) { + throw new AssumptionFailedError(InGameRequiredConstraint::class . ' should have prevented this'); + } + + /** @var Smaccer $plugin */ + $plugin = $this->plugin; + + $entityData = SmaccerHandler::getInstance()->getEntitiesInfo(null, true); + $totalEntityCount = $entityData['count']; + $entities = $entityData['infoList']; + + if ($totalEntityCount > 0) { + $message = TextFormat::RED . 'NPC List and Locations: (' . $totalEntityCount . ')'; + $message .= "\n" . TextFormat::WHITE . '- ' . implode("\n - ", $entities); + } else { + $message = TextFormat::RED . 'No NPCs found in any world.'; + } + + $sender->sendMessage($message); + } + + public function prepare() : void { + $this->addConstraint(new InGameRequiredConstraint($this)); + + $this->setPermission(Permissions::COMMAND_LIST); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/MoveSubCommand.php b/src/aiptu/smaccer/command/subcommand/MoveSubCommand.php new file mode 100644 index 0000000..5666728 --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/MoveSubCommand.php @@ -0,0 +1,95 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + if (!$sender instanceof Player) { + throw new AssumptionFailedError(InGameRequiredConstraint::class . ' should have prevented this'); + } + + /** @var Smaccer $plugin */ + $plugin = $this->plugin; + $target = $sender; + if (isset($args['target'])) { + if (!$sender->hasPermission(Permissions::COMMAND_MOVE_OTHERS)) { + $sender->sendMessage(TextFormat::RED . "You don't have permission to move the entity for other players."); + return; + } + + $targetPlayer = $plugin->getServer()->getPlayerByPrefix($args['target']); + if ($targetPlayer === null) { + $sender->sendMessage(TextFormat::RED . 'Player ' . $args['target'] . ' is not online.'); + return; + } + + $target = $targetPlayer; + } + + $npcId = $args['npcId'] ?? null; + + if ($npcId === null) { + $sender->sendMessage(TextFormat::RED . 'Usage: /' . $aliasUsed . ' '); + return; + } + + $entity = $plugin->getServer()->getWorldManager()->findEntity($npcId); + + if ($entity === null) { + $sender->sendMessage(TextFormat::RED . 'NPC with ID ' . $npcId . ' not found!'); + return; + } + + $entity->teleport($target->getLocation()); + + $target->sendMessage(TextFormat::GREEN . 'Successfully moved NPC with ID ' . $npcId . TextFormat::AQUA . $entity->getWorld()->getFolderName() . ': ' . $entity->getLocation()->getFloorX() . '/' . $entity->getLocation()->getFloorY() . '/' . $entity->getLocation()->getFloorZ()); + if ($sender !== $target) { + $sender->sendMessage(TextFormat::GREEN . 'Successfully moved NPC with ID ' . $npcId . ' to ' . $target->getName() . TextFormat::AQUA . $entity->getWorld()->getFolderName() . ': ' . $entity->getLocation()->getFloorX() . '/' . $entity->getLocation()->getFloorY() . '/' . $entity->getLocation()->getFloorZ()); + } + } + + public function prepare() : void { + $this->addConstraint(new InGameRequiredConstraint($this)); + + $this->setPermissions([ + Permissions::COMMAND_MOVE_SELF, + Permissions::COMMAND_MOVE_OTHERS, + ]); + + $this->registerArgument(0, new IntegerArgument('npcId')); + $this->registerArgument(1, new TargetPlayerArgument(true, 'target')); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/ReloadSubCommand.php b/src/aiptu/smaccer/command/subcommand/ReloadSubCommand.php new file mode 100644 index 0000000..a1da0ef --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/ReloadSubCommand.php @@ -0,0 +1,66 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + /** @var Smaccer $plugin */ + $plugin = $this->plugin; + + $reloadType = $args['reloadType']; + + switch ($reloadType) { + case ReloadTypeArgument::CONFIG: + $plugin->reloadConfig(); + $sender->sendMessage(TextFormat::GREEN . 'Configuration reloaded successfully.'); + break; + case ReloadTypeArgument::EMOTES: + $plugin->getServer()->getAsyncPool()->submitTask(new LoadEmotesTask(EmoteUtils::getEmoteCachePath())); + $sender->sendMessage(TextFormat::GREEN . 'Emotes reloaded successfully.'); + break; + default: + $sender->sendMessage(TextFormat::RED . 'Invalid reload type specified.'); + break; + } + } + + public function prepare() : void { + $this->setPermissions([ + Permissions::COMMAND_RELOAD_CONFIG, + Permissions::COMMAND_RELOAD_EMOTES, + ]); + + $this->registerArgument(0, new ReloadTypeArgument('reloadType')); + } +} diff --git a/src/aiptu/smaccer/command/subcommand/TeleportSubCommand.php b/src/aiptu/smaccer/command/subcommand/TeleportSubCommand.php new file mode 100644 index 0000000..17a4928 --- /dev/null +++ b/src/aiptu/smaccer/command/subcommand/TeleportSubCommand.php @@ -0,0 +1,95 @@ + $aliases */ + public function __construct( + PluginBase $plugin, + string $name, + string $description = '', + array $aliases = [] + ) { + parent::__construct($plugin, $name, $description, $aliases); + } + + public function onRun(CommandSender $sender, string $aliasUsed, array $args) : void { + if (!$sender instanceof Player) { + throw new AssumptionFailedError(InGameRequiredConstraint::class . ' should have prevented this'); + } + + /** @var Smaccer $plugin */ + $plugin = $this->plugin; + $target = $sender; + if (isset($args['target'])) { + if (!$sender->hasPermission(Permissions::COMMAND_TELEPORT_OTHERS)) { + $sender->sendMessage(TextFormat::RED . "You don't have permission to teleport the entity for other players."); + return; + } + + $targetPlayer = $plugin->getServer()->getPlayerByPrefix($args['target']); + if ($targetPlayer === null) { + $sender->sendMessage(TextFormat::RED . 'Player ' . $args['target'] . ' is not online.'); + return; + } + + $target = $targetPlayer; + } + + $npcId = $args['npcId'] ?? null; + + if ($npcId === null) { + $sender->sendMessage(TextFormat::RED . 'Usage: /' . $aliasUsed . ' '); + return; + } + + $entity = $plugin->getServer()->getWorldManager()->findEntity($npcId); + + if ($entity === null) { + $sender->sendMessage(TextFormat::RED . 'NPC with ID ' . $npcId . ' not found!'); + return; + } + + $target->teleport($entity->getLocation()); + + $target->sendMessage(TextFormat::GREEN . 'Successfully teleported NPC with ID ' . $npcId . TextFormat::AQUA . $entity->getWorld()->getFolderName() . ': ' . $entity->getLocation()->getFloorX() . '/' . $entity->getLocation()->getFloorY() . '/' . $entity->getLocation()->getFloorZ()); + if ($sender !== $target) { + $sender->sendMessage(TextFormat::GREEN . 'Successfully teleported NPC with ID ' . $npcId . ' to ' . $target->getName() . TextFormat::AQUA . $entity->getWorld()->getFolderName() . ': ' . $entity->getLocation()->getFloorX() . '/' . $entity->getLocation()->getFloorY() . '/' . $entity->getLocation()->getFloorZ()); + } + } + + public function prepare() : void { + $this->addConstraint(new InGameRequiredConstraint($this)); + + $this->setPermissions([ + Permissions::COMMAND_TELEPORT_SELF, + Permissions::COMMAND_TELEPORT_OTHERS, + ]); + + $this->registerArgument(0, new IntegerArgument('npcId')); + $this->registerArgument(1, new TargetPlayerArgument(true, 'target')); + } +} diff --git a/src/aiptu/smaccer/entity/EntityAgeable.php b/src/aiptu/smaccer/entity/EntityAgeable.php new file mode 100644 index 0000000..ab5d1c0 --- /dev/null +++ b/src/aiptu/smaccer/entity/EntityAgeable.php @@ -0,0 +1,58 @@ +setBaby((bool) $nbt->getByte(EntityTag::BABY, 0)); + } + + public function saveNBT() : CompoundTag { + $nbt = parent::saveNBT(); + + $nbt->setByte(EntityTag::BABY, (int) $this->baby); + return $nbt; + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void { + parent::syncNetworkData($properties); + $properties->setGenericFlag(EntityMetadataFlags::BABY, $this->isBaby()); + } + + public function isBaby() : bool { + return $this->baby; + } + + public function setBaby(bool $value = true) : void { + $this->baby = $value; + + $this->setScale($value ? $this->getBabyScale() : 1.0); + + $this->networkPropertiesDirty = true; + } + + public function getBabyScale() : float { + return 0.5; + } +} diff --git a/src/aiptu/smaccer/entity/EntitySmaccer.php b/src/aiptu/smaccer/entity/EntitySmaccer.php new file mode 100644 index 0000000..ebb35ba --- /dev/null +++ b/src/aiptu/smaccer/entity/EntitySmaccer.php @@ -0,0 +1,93 @@ +initializeCreator($nbt); + $this->initializeCommand($nbt); + } + + parent::__construct($location, $nbt); + } + + protected function initEntity(CompoundTag $nbt) : void { + parent::initEntity($nbt); + + $this->setScale($nbt->getFloat(EntityTag::SCALE, 1.0)); + $this->initializeRotation($nbt); + $this->setNameTagAlwaysVisible((bool) $nbt->getByte(EntityTag::NAMETAG_VISIBLE, 1)); + $this->setNameTagVisible((bool) $nbt->getByte(EntityTag::NAMETAG_VISIBLE, 1)); + $this->initializeVisibility($nbt); + $this->setHasGravity((bool) $nbt->getByte(EntityTag::GRAVITY, 1)); + $this->initializeQuery($nbt); + } + + public function saveNBT() : CompoundTag { + $nbt = parent::saveNBT(); + + $this->saveCreator($nbt); + $this->saveCommand($nbt); + $nbt->setFloat(EntityTag::SCALE, $this->scale); + $this->saveRotation($nbt); + $nbt->setByte(EntityTag::NAMETAG_VISIBLE, (int) $this->isNameTagVisible()); + $this->saveVisibility($nbt); + $nbt->setByte(EntityTag::GRAVITY, (int) $this->hasGravity()); + $this->saveQuery($nbt); + + return $nbt; + } + + abstract protected function getInitialSizeInfo() : EntitySizeInfo; + + abstract public static function getNetworkTypeId() : string; + + abstract public function getName() : string; + + protected function getInitialDragMultiplier() : float { + return 0.02; + } + + protected function getInitialGravity() : float { + return 0.08; + } + + public function setHasGravity(bool $v = true) : void { + parent::setHasGravity($v); + + $this->networkPropertiesDirty = true; + + $this->setForceMovementUpdate(); + } +} diff --git a/src/aiptu/smaccer/entity/HumanSmaccer.php b/src/aiptu/smaccer/entity/HumanSmaccer.php new file mode 100644 index 0000000..4350ba5 --- /dev/null +++ b/src/aiptu/smaccer/entity/HumanSmaccer.php @@ -0,0 +1,95 @@ +initializeCreator($nbt); + $this->initializeCommand($nbt); + } + + parent::__construct($location, $skin, $nbt); + } + + protected function initEntity(CompoundTag $nbt) : void { + parent::initEntity($nbt); + + $this->setScale($nbt->getFloat(EntityTag::SCALE, 1.0)); + $this->initializeRotation($nbt); + $this->setNameTagAlwaysVisible((bool) $nbt->getByte(EntityTag::NAMETAG_VISIBLE, 1)); + $this->setNameTagVisible((bool) $nbt->getByte(EntityTag::NAMETAG_VISIBLE, 1)); + $this->initializeVisibility($nbt); + $this->initializeSlapBack($nbt); + $this->initializeEmote($nbt); + $this->setHasGravity((bool) $nbt->getByte(EntityTag::GRAVITY, 1)); + $this->initializeQuery($nbt); + } + + public function saveNBT() : CompoundTag { + $nbt = parent::saveNBT(); + + $this->saveCreator($nbt); + $this->saveCommand($nbt); + $nbt->setFloat(EntityTag::SCALE, $this->scale); + $this->saveRotation($nbt); + $nbt->setByte(EntityTag::NAMETAG_VISIBLE, (int) $this->isNameTagVisible()); + $this->saveVisibility($nbt); + $this->saveEmote($nbt); + $this->saveSlapBack($nbt); + $nbt->setByte(EntityTag::GRAVITY, (int) $this->hasGravity()); + $this->saveQuery($nbt); + + return $nbt; + } + + public function getName() : string { + return 'Human'; + } + + public function setHasGravity(bool $v = true) : void { + parent::setHasGravity($v); + + $this->networkPropertiesDirty = true; + + $this->setForceMovementUpdate(); + } +} diff --git a/src/aiptu/smaccer/entity/NPCData.php b/src/aiptu/smaccer/entity/NPCData.php new file mode 100644 index 0000000..702b49c --- /dev/null +++ b/src/aiptu/smaccer/entity/NPCData.php @@ -0,0 +1,124 @@ +nameTag; + } + + public function setNameTag(?string $nameTag) : self { + $this->nameTag = $nameTag; + return $this; + } + + public function getScale() : float { + return $this->scale; + } + + public function setScale(float $scale) : self { + $this->scale = $scale; + return $this; + } + + public function isBaby() : bool { + return $this->baby; + } + + public function setBaby(bool $baby) : self { + $this->baby = $baby; + return $this; + } + + public function isRotationEnabled() : bool { + return $this->rotationEnabled; + } + + public function setRotationEnabled(bool $rotationEnabled) : self { + $this->rotationEnabled = $rotationEnabled; + return $this; + } + + public function isNametagVisible() : bool { + return $this->nametagVisible; + } + + public function setNametagVisible(bool $nametagVisible) : self { + $this->nametagVisible = $nametagVisible; + return $this; + } + + public function getVisibility() : EntityVisibility { + return $this->visibility; + } + + public function setVisibility(EntityVisibility $visibility) : self { + $this->visibility = $visibility; + return $this; + } + + public function getSlapBack() : bool { + return $this->slapBack; + } + + public function setSlapBack(bool $slapBack) : self { + $this->slapBack = $slapBack; + return $this; + } + + public function getActionEmote() : ?EmoteType { + return $this->actionEmote; + } + + public function setActionEmote(?EmoteType $actionEmote) : self { + $this->actionEmote = $actionEmote; + return $this; + } + + public function getEmote() : ?EmoteType { + return $this->emote; + } + + public function setEmote(?EmoteType $emote) : self { + $this->emote = $emote; + return $this; + } + + public function hasGravity() : bool { + return $this->gravityEnabled; + } + + public function setHasGravity(bool $gravityEnabled) : self { + $this->gravityEnabled = $gravityEnabled; + return $this; + } +} diff --git a/src/aiptu/smaccer/entity/SmaccerHandler.php b/src/aiptu/smaccer/entity/SmaccerHandler.php new file mode 100644 index 0000000..10b81b5 --- /dev/null +++ b/src/aiptu/smaccer/entity/SmaccerHandler.php @@ -0,0 +1,528 @@ + AllaySmaccer::class, + 'Armadillo' => ArmadilloSmaccer::class, + 'Axolotl' => AxolotlSmaccer::class, + 'Bat' => BatSmaccer::class, + 'Bee' => BeeSmaccer::class, + 'Blaze' => BlazeSmaccer::class, + 'Bogged' => BoggedSmaccer::class, + 'Breeze' => BreezeSmaccer::class, + 'Camel' => CamelSmaccer::class, + 'Cat' => CatSmaccer::class, + 'CaveSpider' => CaveSpiderSmaccer::class, + 'Chicken' => ChickenSmaccer::class, + 'Cod' => CodSmaccer::class, + 'Cow' => CowSmaccer::class, + 'Creeper' => CreeperSmaccer::class, + 'Dolphin' => DolphinSmaccer::class, + 'Donkey' => DonkeySmaccer::class, + 'Drowned' => DrownedSmaccer::class, + 'ElderGuardian' => ElderGuardianSmaccer::class, + 'EnderDragon' => EnderDragonSmaccer::class, + 'Enderman' => EndermanSmaccer::class, + 'Endermite' => EndermiteSmaccer::class, + 'EvocationIllager' => EvocationIllagerSmaccer::class, + 'Fox' => FoxSmaccer::class, + 'Frog' => FrogSmaccer::class, + 'Ghast' => GhastSmaccer::class, + 'GlowSquid' => GlowSquidSmaccer::class, + 'Goat' => GoatSmaccer::class, + 'Guardian' => GuardianSmaccer::class, + 'Hoglin' => HoglinSmaccer::class, + 'Horse' => HorseSmaccer::class, + 'Husk' => HuskSmaccer::class, + 'IronGolem' => IronGolemSmaccer::class, + 'Llama' => LlamaSmaccer::class, + 'MagmaCube' => MagmaCubeSmaccer::class, + 'Mooshroom' => MooshroomSmaccer::class, + 'Mule' => MuleSmaccer::class, + 'Ocelot' => OcelotSmaccer::class, + 'Panda' => PandaSmaccer::class, + 'Parrot' => ParrotSmaccer::class, + 'Phantom' => PhantomSmaccer::class, + 'Pig' => PigSmaccer::class, + 'PiglinBrute' => PiglinBruteSmaccer::class, + 'Piglin' => PiglinSmaccer::class, + 'Pillager' => PillagerSmaccer::class, + 'PolarBear' => PolarBearSmaccer::class, + 'Pufferfish' => PufferfishSmaccer::class, + 'Rabbit' => RabbitSmaccer::class, + 'Ravager' => RavagerSmaccer::class, + 'Salmon' => SalmonSmaccer::class, + 'Sheep' => SheepSmaccer::class, + 'Shulker' => ShulkerSmaccer::class, + 'Silverfish' => SilverfishSmaccer::class, + 'SkeletonHorse' => SkeletonHorseSmaccer::class, + 'Skeleton' => SkeletonSmaccer::class, + 'Slime' => SlimeSmaccer::class, + 'Sniffer' => SnifferSmaccer::class, + 'SnowGolem' => SnowGolemSmaccer::class, + 'Spider' => SpiderSmaccer::class, + 'Squid' => SquidSmaccer::class, + 'Stray' => StraySmaccer::class, + 'Strider' => StriderSmaccer::class, + 'Tadpole' => TadpoleSmaccer::class, + 'TraderLlama' => TraderLlamaSmaccer::class, + 'Tropicalfish' => TropicalfishSmaccer::class, + 'Turtle' => TurtleSmaccer::class, + 'Vex' => VexSmaccer::class, + 'Villager' => VillagerSmaccer::class, + 'VillagerV2' => VillagerV2Smaccer::class, + 'Vindicator' => VindicatorSmaccer::class, + 'WanderingTrader' => WanderingTraderSmaccer::class, + 'Warden' => WardenSmaccer::class, + 'Witch' => WitchSmaccer::class, + 'WitherSkeleton' => WitherSkeletonSmaccer::class, + 'Wither' => WitherSmaccer::class, + 'Wolf' => WolfSmaccer::class, + 'Zoglin' => ZoglinSmaccer::class, + 'ZombieHorse' => ZombieHorseSmaccer::class, + 'Zombie' => ZombieSmaccer::class, + 'ZombieVillager' => ZombieVillagerSmaccer::class, + 'ZombieVillagerV2' => ZombieVillagerV2Smaccer::class, + ]; + + private array $registered_npcs = []; + + /** @var array> */ + private array $playerNPCs = []; + + public function registerAll() : void { + $this->registerEntity('Human', HumanSmaccer::class); + + foreach ($this->npcs as $type => $class) { + $this->registerEntity($type, $class); + } + } + + private function registerEntity(string $type, string $entityClass) : void { + if (!is_subclass_of($entityClass, Entity::class)) { + throw new \InvalidArgumentException("Class {$entityClass} must be a subclass of " . Entity::class); + } + + $registerFunction = function (World $world, CompoundTag $nbt) use ($entityClass) : Entity { + if (is_a($entityClass, HumanSmaccer::class, true)) { + return new $entityClass(EntityDataHelper::parseLocation($nbt, $world), Human::parseSkinNBT($nbt), $nbt); + } + + return new $entityClass(EntityDataHelper::parseLocation($nbt, $world), $nbt); + }; + + EntityFactory::getInstance()->register($entityClass, $registerFunction, array_merge([$entityClass], Utils::getClassNamespace($entityClass))); + $this->registered_npcs[$type] = $entityClass; + } + + public function getRegisteredNPC() : array { + return $this->registered_npcs; + } + + public function getNPC(string $entityName) : ?string { + foreach ($this->registered_npcs as $type => $class) { + if (strtolower($type) === strtolower($entityName)) { + return $class; + } + } + + return null; + } + + public function createEntity(string $type, Location $location, CompoundTag $nbt) : ?Entity { + $entityClass = $this->getNPC($type); + if ($entityClass === null) { + return null; + } + + if (!is_subclass_of($entityClass, Entity::class)) { + throw new \InvalidArgumentException("Class {$entityClass} must be a subclass of " . Entity::class); + } + + $createFunction = function (Location $location, CompoundTag $nbt) use ($entityClass) { + if (is_a($entityClass, HumanSmaccer::class, true)) { + return new $entityClass($location, Human::parseSkinNBT($nbt), $nbt); + } + + return new $entityClass($location, $nbt); + }; + + return $createFunction($location, $nbt); + } + + private function createBaseNBT(Vector3 $pos, ?Vector3 $motion = null, float $yaw = 0.0, float $pitch = 0.0) : CompoundTag { + return CompoundTag::create() + ->setTag('Pos', new ListTag([ + new DoubleTag($pos->x), + new DoubleTag($pos->y), + new DoubleTag($pos->z), + ])) + ->setTag('Motion', new ListTag([ + new DoubleTag($motion !== null ? $motion->x : 0.0), + new DoubleTag($motion !== null ? $motion->y : 0.0), + new DoubleTag($motion !== null ? $motion->z : 0.0), + ])) + ->setTag('Rotation', new ListTag([ + new FloatTag($yaw), + new FloatTag($pitch), + ])); + } + + /** + * @return Promise + */ + public function spawnNPC( + string $type, + Player $player, + NPCData $npcData, + ?Location $customPos = null, + ?Vector3 $motion = null + ) : Promise { + $resolver = new PromiseResolver(); + $promise = $resolver->getPromise(); + + $pos = $customPos ?? $player->getLocation(); + $yaw = $pos->getYaw(); + $pitch = $pos->getPitch(); + $motion ??= $player->getMotion(); + + $playerId = $player->getUniqueId()->getBytes(); + $nameTag = $npcData->getNameTag(); + $scale = $npcData->getScale(); + $rotationEnabled = $npcData->isRotationEnabled(); + $nametagVisible = $npcData->isNametagVisible(); + $visibility = $npcData->getVisibility(); + $isBaby = $npcData->isBaby(); + $slapBack = $npcData->getSlapBack(); + $actionEmote = $npcData->getActionEmote(); + $emote = $npcData->getEmote(); + $gravityEnabled = $npcData->hasGravity(); + + $entityClass = $this->getNPC($type); + if ($entityClass === null) { + $resolver->reject(new \InvalidArgumentException("Invalid NPC type: {$type}")); + return $promise; + } + + $nbt = $this->createBaseNBT($pos, $motion, $yaw, $pitch); + $nbt->setString(EntityTag::CREATOR, $playerId) + ->setFloat(EntityTag::SCALE, $scale) + ->setByte(EntityTag::ROTATE_TO_PLAYERS, (int) $rotationEnabled) + ->setByte(EntityTag::NAMETAG_VISIBLE, (int) $nametagVisible) + ->setInt(EntityTag::VISIBILITY, $visibility->value) + ->setByte(EntityTag::GRAVITY, (int) $gravityEnabled); + + if (is_a($entityClass, EntityAgeable::class, true)) { + $nbt->setByte(EntityTag::BABY, (int) $isBaby); + } + + if (is_a($entityClass, HumanSmaccer::class, true)) { + $skin = $player->getSkin(); + + $nbt->setTag( + 'Skin', + CompoundTag::create() + ->setString('Name', $skin->getSkinId()) + ->setByteArray('Data', $skin->getSkinData()) + ->setByteArray('CapeData', $skin->getCapeData()) + ->setString('GeometryName', $skin->getGeometryName()) + ->setByteArray('GeometryData', $skin->getGeometryData()) + ); + + $nbt->setByte(EntityTag::SLAP_BACK, (int) $slapBack); + + if ($actionEmote !== null) { + $nbt->setString(EntityTag::ACTION_EMOTE, $actionEmote->getUuid()); + } + + if ($emote !== null) { + $nbt->setString(EntityTag::EMOTE, $emote->getUuid()); + } + } + + $entity = $this->createEntity($type, $pos, $nbt); + if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { + $resolver->reject(new \RuntimeException("Failed to create NPC entity: {$type}")); + return $promise; + } + + if ($nameTag !== null) { + $entity->setNameTag($entity->applyNametag($nameTag, $player)); + } + + $entity->setScale($scale); + $entity->setRotateToPlayers($rotationEnabled); + $entity->setNameTagAlwaysVisible($nametagVisible); + $entity->setNameTagVisible($nametagVisible); + + if ($entity instanceof EntityAgeable) { + $entity->setBaby($isBaby); + } + + if ($entity instanceof HumanSmaccer) { + $entity->setSlapBack($slapBack); + + if ($actionEmote !== null) { + $entity->setActionEmote($actionEmote); + } + + if ($emote !== null) { + $entity->setEmote($emote); + } + } + + $entity->setVisibility($visibility); + $entity->setHasGravity($gravityEnabled); + $entity->sendData($entity->getViewers()); + + $ev = new NPCSpawnEvent($entity); + $ev->call(); + if ($ev->isCancelled()) { + $resolver->reject(new \RuntimeException('NPC spawn event was cancelled')); + return $promise; + } + + $entityId = $entity->getId(); + $this->playerNPCs[$playerId][$entityId] = $entity; + + $resolver->resolve($entity); + return $promise; + } + + /** + * @return Promise + */ + public function despawnNPC(string $creatorId, Entity $entity) : Promise { + $resolver = new PromiseResolver(); + $promise = $resolver->getPromise(); + + $entityId = $entity->getId(); + + if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { + $resolver->reject(new \InvalidArgumentException('Invalid entity type')); + return $promise; + } + + $ev = new NPCDespawnEvent($entity); + $ev->call(); + if ($ev->isCancelled()) { + $resolver->reject(new \RuntimeException('NPC despawn event was cancelled')); + return $promise; + } + + $entity->flagForDespawn(); + unset($this->playerNPCs[$creatorId][$entityId]); + + $resolver->resolve(true); + return $promise; + } + + /** + * @return Promise + */ + public function editNPC(Player $player, Entity $entity, NPCData $npcData) : Promise { + $resolver = new PromiseResolver(); + $promise = $resolver->getPromise(); + + if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { + $resolver->reject(new \InvalidArgumentException('Invalid entity type')); + return $promise; + } + + $ev = new NPCUpdateEvent($entity, $npcData); + $ev->call(); + if ($ev->isCancelled()) { + $resolver->reject(new \RuntimeException('NPC update event was cancelled')); + return $promise; + } + + $nameTag = $ev->getNPCData()->getNameTag(); + $scale = $ev->getNPCData()->getScale(); + $rotationEnabled = $ev->getNPCData()->isRotationEnabled(); + $nametagVisible = $ev->getNPCData()->isNametagVisible(); + $visibility = $ev->getNPCData()->getVisibility(); + $isBaby = $ev->getNPCData()->isBaby(); + $slapBack = $ev->getNPCData()->getSlapBack(); + $actionEmote = $ev->getNPCData()->getActionEmote(); + $emote = $ev->getNPCData()->getEmote(); + $gravityEnabled = $npcData->hasGravity(); + + if ($nameTag !== null) { + $entity->setNameTag($entity->applyNametag($nameTag, $player)); + } + + $entity->setScale($scale); + $entity->setRotateToPlayers($rotationEnabled); + $entity->setNameTagAlwaysVisible($nametagVisible); + $entity->setNameTagVisible($nametagVisible); + + if ($entity instanceof EntityAgeable) { + $entity->setBaby($isBaby); + } + + if ($entity instanceof HumanSmaccer) { + $entity->setSlapBack($slapBack); + + if ($actionEmote !== null) { + $entity->setActionEmote($actionEmote); + } + + if ($emote !== null) { + $entity->setEmote($emote); + } + } + + $entity->setVisibility($visibility); + $entity->setHasGravity($gravityEnabled); + $entity->sendData($entity->getViewers()); + + $resolver->resolve(true); + return $promise; + } + + public function getEntitiesInfo(?Player $player = null, bool $collectInfo = false) : array { + $entityCount = 0; + $entityInfoList = []; + + foreach (Smaccer::getInstance()->getServer()->getWorldManager()->getWorlds() as $world) { + foreach ($world->getEntities() as $entity) { + if ($entity instanceof EntitySmaccer || $entity instanceof HumanSmaccer) { + if ($player === null || $entity->isOwnedBy($player)) { + ++$entityCount; + if ($collectInfo) { + $entityInfoList[] = TextFormat::YELLOW . 'ID: (' . $entity->getId() . ') ' . TextFormat::GREEN . $entity->getNameTag() . TextFormat::GRAY . ' -- ' . TextFormat::AQUA . $entity->getWorld()->getFolderName() . ': ' . $entity->getLocation()->getFloorX() . '/' . $entity->getLocation()->getFloorY() . '/' . $entity->getLocation()->getFloorZ(); + } + } + } + } + } + + return [ + 'count' => $entityCount, + 'infoList' => $entityInfoList, + ]; + } +} diff --git a/src/aiptu/smaccer/entity/command/CommandHandler.php b/src/aiptu/smaccer/entity/command/CommandHandler.php new file mode 100644 index 0000000..7935c79 --- /dev/null +++ b/src/aiptu/smaccer/entity/command/CommandHandler.php @@ -0,0 +1,147 @@ + */ + private array $commands = []; + private int $nextId = 1; + + public function __construct(CompoundTag $nbt) { + $commandsTag = $nbt->getTag(EntityTag::COMMANDS); + if ($commandsTag instanceof ListTag) { + foreach ($commandsTag as $tag) { + if ($tag instanceof CompoundTag) { + $command = $tag->getString(self::KEY_COMMAND); + $type = $tag->getString(self::KEY_TYPE); + $this->add($command, $type); + } + } + } + } + + /** + * Adds a command and returns its ID. If the command already exists or the type is invalid, returns null. + */ + public function add(string $command, string $type) : ?int { + if (!$this->isValidType($type)) { + return null; + } + + $existingId = $this->getIdByCommandAndType($command, $type); + if ($existingId !== null) { + return null; + } + + if (str_starts_with($command, '/')) { + $command = substr($command, 1); + } + + $id = $this->nextId++; + $this->commands[$id] = [self::KEY_COMMAND => $command, self::KEY_TYPE => $type]; + return $id; + } + + /** + * Edits an existing command identified by its ID. + * Returns true if successful, false if the command ID does not exist. + */ + public function edit(int $id, string $newCommand, string $newType) : bool { + if (!$this->exists($id)) { + return false; + } + + if (!$this->isValidType($newType)) { + return false; + } + + if (str_starts_with($newCommand, '/')) { + $newCommand = substr($newCommand, 1); + } + + $existingId = $this->getIdByCommandAndType($newCommand, $newType); + if ($existingId !== null && $existingId !== $id) { + return false; + } + + $this->commands[$id] = [self::KEY_COMMAND => $newCommand, self::KEY_TYPE => $newType]; + return true; + } + + /** + * Checks if the given type is valid. + */ + public function isValidType(string $type) : bool { + return in_array($type, [EntityTag::COMMAND_TYPE_PLAYER, EntityTag::COMMAND_TYPE_SERVER], true); + } + + /** + * Checks if a command with the given ID exists. + */ + public function exists(int $id) : bool { + return isset($this->commands[$id]); + } + + /** + * Retrieves the ID associated with the given command and type. + */ + public function getIdByCommandAndType(string $command, string $type) : ?int { + foreach ($this->commands as $id => $data) { + if ($data[self::KEY_COMMAND] === $command && $data[self::KEY_TYPE] === $type) { + return $id; + } + } + + return null; + } + + /** + * Retrieves all commands. + * + * @return array + */ + public function getAll() : array { + return $this->commands; + } + + /** + * Removes the command with the specified ID. + */ + public function removeById(int $id) : bool { + if ($this->exists($id)) { + unset($this->commands[$id]); + return true; + } + + return false; + } + + /** + * Removes or clears all commands. + */ + public function clearAll() : void { + $this->commands = []; + $this->nextId = 1; + } +} diff --git a/src/aiptu/smaccer/entity/emote/EmoteManager.php b/src/aiptu/smaccer/entity/emote/EmoteManager.php new file mode 100644 index 0000000..00a6221 --- /dev/null +++ b/src/aiptu/smaccer/entity/emote/EmoteManager.php @@ -0,0 +1,107 @@ + */ + private array $emotes = []; + + /** + * @param array{ + * array{ + * uuid: string, + * title: string, + * image: string + * } + * } $emotes the array of emotes list + */ + public function __construct(array $emotes) { + $this->loadEmotes($emotes); + } + + /** + * Load emote from the given array. + * + * @param array{ + * array{ + * uuid: string, + * title: string, + * image: string + * } + * } $emotes the array of emotes list + */ + public function loadEmotes(array $emotes) : void { + // TODO: if your want to force load from github using a single command. so its should be empty first + $this->emotes = []; + + foreach ($emotes as $emote) { + extract($emote); + + $originalTitle = $title; + $counter = 2; + + while ($this->ensureUniqueTitle($title)) { + $title = $originalTitle . ' ' . $counter; + ++$counter; + } + + $this->emotes[] = new EmoteType($uuid, $title, $image); + } + } + + /** + * Ensure none of the title are the same. + * + * @param string $title The title that will be checked + * + * @return bool Returns `true` when the title is the same as the one listed and `false` when the title is Unique + */ + public function ensureUniqueTitle(string $title) { + foreach ($this->emotes as $emote) { + if ($emote->getTitle() === $title) { + return true; + } + } + + return false; + } + + /** + * Get an emote by its uuid. + * + * @param string $uuid the UUID of the emote + * + * @return EmoteType|null returns `EmoteType` class when the uuid exists and `null` if the UUID doesn`t exists + */ + public function getEmote(string $uuid) : ?EmoteType { + foreach ($this->emotes as $emote) { + if ($emote->getUuid() === $uuid) { + return $emote; + } + } + + return null; + } + + /** + * Return all emotes. + * + * @return array Returns all of the `EmoteType` + */ + public function getAll() : array { + return $this->emotes; + } +} diff --git a/src/aiptu/smaccer/entity/emote/EmoteType.php b/src/aiptu/smaccer/entity/emote/EmoteType.php new file mode 100644 index 0000000..c72fb26 --- /dev/null +++ b/src/aiptu/smaccer/entity/emote/EmoteType.php @@ -0,0 +1,43 @@ +uuid; + } + + /** + * The Title of the emote. + */ + public function getTitle() : string { + return $this->title; + } + + /** + * The Image Url of the emote. + */ + public function getImage() : string { + return $this->image; + } +} diff --git a/src/aiptu/smaccer/entity/npc/AllaySmaccer.php b/src/aiptu/smaccer/entity/npc/AllaySmaccer.php new file mode 100644 index 0000000..0f331b6 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/AllaySmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.6; + } + + public function getWidth() : float { + return 0.35; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ALLAY; + } + + public function getName() : string { + return 'Allay'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ArmadilloSmaccer.php b/src/aiptu/smaccer/entity/npc/ArmadilloSmaccer.php new file mode 100644 index 0000000..e102c47 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ArmadilloSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.39 : 0.65; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.42 : 0.7; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ARMADILLO; + } + + public function getName() : string { + return 'Armadillo'; + } + + public function getBabyScale() : float { + return 0.6; + } +} diff --git a/src/aiptu/smaccer/entity/npc/AxolotlSmaccer.php b/src/aiptu/smaccer/entity/npc/AxolotlSmaccer.php new file mode 100644 index 0000000..3e6b9d3 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/AxolotlSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.21 : 0.42; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.375 : 0.75; + } + + public static function getNetworkTypeId() : string { + return EntityIds::AXOLOTL; + } + + public function getName() : string { + return 'Axolotl'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/BatSmaccer.php b/src/aiptu/smaccer/entity/npc/BatSmaccer.php new file mode 100644 index 0000000..b6d08be --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/BatSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.9; + } + + public function getWidth() : float { + return 0.5; + } + + public static function getNetworkTypeId() : string { + return EntityIds::BAT; + } + + public function getName() : string { + return 'Bat'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/BeeSmaccer.php b/src/aiptu/smaccer/entity/npc/BeeSmaccer.php new file mode 100644 index 0000000..792fcaa --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/BeeSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.25 : 0.5; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.275 : 0.55; + } + + public static function getNetworkTypeId() : string { + return EntityIds::BEE; + } + + public function getName() : string { + return 'Bee'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/BlazeSmaccer.php b/src/aiptu/smaccer/entity/npc/BlazeSmaccer.php new file mode 100644 index 0000000..79fd99d --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/BlazeSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.8; + } + + public function getWidth() : float { + return 0.5; + } + + public static function getNetworkTypeId() : string { + return EntityIds::BLAZE; + } + + public function getName() : string { + return 'Blaze'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/BoggedSmaccer.php b/src/aiptu/smaccer/entity/npc/BoggedSmaccer.php new file mode 100644 index 0000000..daf030f --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/BoggedSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::BOGGED; + } + + public function getName() : string { + return 'Bogged'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/BreezeSmaccer.php b/src/aiptu/smaccer/entity/npc/BreezeSmaccer.php new file mode 100644 index 0000000..b289202 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/BreezeSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.77; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::BREEZE; + } + + public function getName() : string { + return 'Breeze'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/CamelSmaccer.php b/src/aiptu/smaccer/entity/npc/CamelSmaccer.php new file mode 100644 index 0000000..3fa0a99 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/CamelSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 1.06875 : 2.375; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.765 : 1.7; + } + + public static function getNetworkTypeId() : string { + return EntityIds::CAMEL; + } + + public function getName() : string { + return 'Camel'; + } + + public function getBabyScale() : float { + return 0.45; + } +} diff --git a/src/aiptu/smaccer/entity/npc/CatSmaccer.php b/src/aiptu/smaccer/entity/npc/CatSmaccer.php new file mode 100644 index 0000000..b020665 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/CatSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.56 : 0.7; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.48 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::CAT; + } + + public function getName() : string { + return 'Cat'; + } + + public function getBabyScale() : float { + return 0.8; + } +} diff --git a/src/aiptu/smaccer/entity/npc/CaveSpiderSmaccer.php b/src/aiptu/smaccer/entity/npc/CaveSpiderSmaccer.php new file mode 100644 index 0000000..e8c86dd --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/CaveSpiderSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.5; + } + + public function getWidth() : float { + return 0.7; + } + + public static function getNetworkTypeId() : string { + return EntityIds::CAVE_SPIDER; + } + + public function getName() : string { + return 'Cave Spider'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ChickenSmaccer.php b/src/aiptu/smaccer/entity/npc/ChickenSmaccer.php new file mode 100644 index 0000000..87c453d --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ChickenSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.4 : 0.8; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::CHICKEN; + } + + public function getName() : string { + return 'Chicken'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/CodSmaccer.php b/src/aiptu/smaccer/entity/npc/CodSmaccer.php new file mode 100644 index 0000000..733f9f6 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/CodSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.3; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::COD; + } + + public function getName() : string { + return 'Cod'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/CowSmaccer.php b/src/aiptu/smaccer/entity/npc/CowSmaccer.php new file mode 100644 index 0000000..75e36b5 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/CowSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.65 : 1.3; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::COW; + } + + public function getName() : string { + return 'Cow'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/CreeperSmaccer.php b/src/aiptu/smaccer/entity/npc/CreeperSmaccer.php new file mode 100644 index 0000000..2e1b8af --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/CreeperSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.8; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::CREEPER; + } + + public function getName() : string { + return 'Creeper'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/DolphinSmaccer.php b/src/aiptu/smaccer/entity/npc/DolphinSmaccer.php new file mode 100644 index 0000000..6808f75 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/DolphinSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.39 : 0.6; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.585 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::DOLPHIN; + } + + public function getName() : string { + return 'Dolphin'; + } + + public function getBabyScale() : float { + return 0.65; + } +} diff --git a/src/aiptu/smaccer/entity/npc/DonkeySmaccer.php b/src/aiptu/smaccer/entity/npc/DonkeySmaccer.php new file mode 100644 index 0000000..84ac0d6 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/DonkeySmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.8 : 1.6; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.7 : 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::DONKEY; + } + + public function getName() : string { + return 'Donkey'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/DrownedSmaccer.php b/src/aiptu/smaccer/entity/npc/DrownedSmaccer.php new file mode 100644 index 0000000..8f75f3e --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/DrownedSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::DROWNED; + } + + public function getName() : string { + return 'Drowned'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ElderGuardianSmaccer.php b/src/aiptu/smaccer/entity/npc/ElderGuardianSmaccer.php new file mode 100644 index 0000000..b9a1250 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ElderGuardianSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.99; + } + + public function getWidth() : float { + return 1.99; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ELDER_GUARDIAN; + } + + public function getName() : string { + return 'Elder Guardian'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/EnderDragonSmaccer.php b/src/aiptu/smaccer/entity/npc/EnderDragonSmaccer.php new file mode 100644 index 0000000..2d56832 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/EnderDragonSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 4; + } + + public function getWidth() : float { + return 13; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ENDER_DRAGON; + } + + public function getName() : string { + return 'Ender Dragon'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/EndermanSmaccer.php b/src/aiptu/smaccer/entity/npc/EndermanSmaccer.php new file mode 100644 index 0000000..b2d0004 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/EndermanSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ENDERMAN; + } + + public function getName() : string { + return 'Enderman'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/EndermiteSmaccer.php b/src/aiptu/smaccer/entity/npc/EndermiteSmaccer.php new file mode 100644 index 0000000..836016c --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/EndermiteSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.3; + } + + public function getWidth() : float { + return 0.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ENDERMITE; + } + + public function getName() : string { + return 'Endermite'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/EvocationIllagerSmaccer.php b/src/aiptu/smaccer/entity/npc/EvocationIllagerSmaccer.php new file mode 100644 index 0000000..ca9e4d6 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/EvocationIllagerSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::EVOCATION_ILLAGER; + } + + public function getName() : string { + return 'Evocation Illager'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/FoxSmaccer.php b/src/aiptu/smaccer/entity/npc/FoxSmaccer.php new file mode 100644 index 0000000..f0a3d9a --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/FoxSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.35 : 0.7; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::FOX; + } + + public function getName() : string { + return 'Fox'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/FrogSmaccer.php b/src/aiptu/smaccer/entity/npc/FrogSmaccer.php new file mode 100644 index 0000000..08f84a7 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/FrogSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.55; + } + + public function getWidth() : float { + return 0.5; + } + + public static function getNetworkTypeId() : string { + return EntityIds::FROG; + } + + public function getName() : string { + return 'Frog'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/GhastSmaccer.php b/src/aiptu/smaccer/entity/npc/GhastSmaccer.php new file mode 100644 index 0000000..4508c12 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/GhastSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 4; + } + + public function getWidth() : float { + return 4.02; + } + + public static function getNetworkTypeId() : string { + return EntityIds::GHAST; + } + + public function getName() : string { + return 'Ghast'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/GlowSquidSmaccer.php b/src/aiptu/smaccer/entity/npc/GlowSquidSmaccer.php new file mode 100644 index 0000000..187d772 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/GlowSquidSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.475 : 0.95; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.475 : 0.95; + } + + public static function getNetworkTypeId() : string { + return EntityIds::GLOW_SQUID; + } + + public function getName() : string { + return 'Glow Squid'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/GoatSmaccer.php b/src/aiptu/smaccer/entity/npc/GoatSmaccer.php new file mode 100644 index 0000000..e02a79b --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/GoatSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.65 : 1.3; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::GOAT; + } + + public function getName() : string { + return 'Goat'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/GuardianSmaccer.php b/src/aiptu/smaccer/entity/npc/GuardianSmaccer.php new file mode 100644 index 0000000..c0e3b76 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/GuardianSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.85; + } + + public function getWidth() : float { + return 0.85; + } + + public static function getNetworkTypeId() : string { + return EntityIds::GUARDIAN; + } + + public function getName() : string { + return 'Guardian'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/HoglinSmaccer.php b/src/aiptu/smaccer/entity/npc/HoglinSmaccer.php new file mode 100644 index 0000000..b6efe25 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/HoglinSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.425 : 0.85; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.425 : 0.85; + } + + public static function getNetworkTypeId() : string { + return EntityIds::HOGLIN; + } + + public function getName() : string { + return 'Hoglin'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/HorseSmaccer.php b/src/aiptu/smaccer/entity/npc/HorseSmaccer.php new file mode 100644 index 0000000..57622df --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/HorseSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.8 : 1.6; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.7 : 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::HORSE; + } + + public function getName() : string { + return 'Horse'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/HuskSmaccer.php b/src/aiptu/smaccer/entity/npc/HuskSmaccer.php new file mode 100644 index 0000000..097db4a --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/HuskSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::HUSK; + } + + public function getName() : string { + return 'Husk'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/IronGolemSmaccer.php b/src/aiptu/smaccer/entity/npc/IronGolemSmaccer.php new file mode 100644 index 0000000..57cb290 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/IronGolemSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2.9; + } + + public function getWidth() : float { + return 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::IRON_GOLEM; + } + + public function getName() : string { + return 'Iron Golem'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/LlamaSmaccer.php b/src/aiptu/smaccer/entity/npc/LlamaSmaccer.php new file mode 100644 index 0000000..45a9a9f --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/LlamaSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.935 : 1.87; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::LLAMA; + } + + public function getName() : string { + return 'Llama'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/MagmaCubeSmaccer.php b/src/aiptu/smaccer/entity/npc/MagmaCubeSmaccer.php new file mode 100644 index 0000000..9f16749 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/MagmaCubeSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2.08; + } + + public function getWidth() : float { + return 2.08; + } + + public static function getNetworkTypeId() : string { + return EntityIds::MAGMA_CUBE; + } + + public function getName() : string { + return 'Magma Cube'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/MooshroomSmaccer.php b/src/aiptu/smaccer/entity/npc/MooshroomSmaccer.php new file mode 100644 index 0000000..2e57593 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/MooshroomSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.65 : 1.3; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::MOOSHROOM; + } + + public function getName() : string { + return 'Mooshroom'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/MuleSmaccer.php b/src/aiptu/smaccer/entity/npc/MuleSmaccer.php new file mode 100644 index 0000000..a104937 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/MuleSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.8 : 1.6; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.7 : 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::MULE; + } + + public function getName() : string { + return 'Mule'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/OcelotSmaccer.php b/src/aiptu/smaccer/entity/npc/OcelotSmaccer.php new file mode 100644 index 0000000..81201b7 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/OcelotSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.7 : 0.7; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.6 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::OCELOT; + } + + public function getName() : string { + return 'Ocelot'; + } + + public function getBabyScale() : float { + return 1; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PandaSmaccer.php b/src/aiptu/smaccer/entity/npc/PandaSmaccer.php new file mode 100644 index 0000000..681d212 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/PandaSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.5 : 1.25; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.52 : 1.3; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PANDA; + } + + public function getName() : string { + return 'Panda'; + } + + public function getBabyScale() : float { + return 0.4; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ParrotSmaccer.php b/src/aiptu/smaccer/entity/npc/ParrotSmaccer.php new file mode 100644 index 0000000..2d1bef8 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ParrotSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1; + } + + public function getWidth() : float { + return 0.5; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PARROT; + } + + public function getName() : string { + return 'Parrot'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PhantomSmaccer.php b/src/aiptu/smaccer/entity/npc/PhantomSmaccer.php new file mode 100644 index 0000000..87e2df8 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/PhantomSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.5; + } + + public function getWidth() : float { + return 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PHANTOM; + } + + public function getName() : string { + return 'Phantom'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PigSmaccer.php b/src/aiptu/smaccer/entity/npc/PigSmaccer.php new file mode 100644 index 0000000..83fd817 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/PigSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PIG; + } + + public function getName() : string { + return 'Pig'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PiglinBruteSmaccer.php b/src/aiptu/smaccer/entity/npc/PiglinBruteSmaccer.php new file mode 100644 index 0000000..ff752af --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/PiglinBruteSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PIGLIN_BRUTE; + } + + public function getName() : string { + return 'Piglin Brute'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PiglinSmaccer.php b/src/aiptu/smaccer/entity/npc/PiglinSmaccer.php new file mode 100644 index 0000000..85b7272 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/PiglinSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PIGLIN; + } + + public function getName() : string { + return 'Piglin'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PillagerSmaccer.php b/src/aiptu/smaccer/entity/npc/PillagerSmaccer.php new file mode 100644 index 0000000..1e11577 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/PillagerSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PILLAGER; + } + + public function getName() : string { + return 'Pillager'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PolarBearSmaccer.php b/src/aiptu/smaccer/entity/npc/PolarBearSmaccer.php new file mode 100644 index 0000000..12754dd --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/PolarBearSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.7 : 1.4; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.7 : 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::POLAR_BEAR; + } + + public function getName() : string { + return 'Polar Bear'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PufferfishSmaccer.php b/src/aiptu/smaccer/entity/npc/PufferfishSmaccer.php new file mode 100644 index 0000000..7ff2d90 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/PufferfishSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.8; + } + + public function getWidth() : float { + return 0.8; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PUFFERFISH; + } + + public function getName() : string { + return 'Pufferfish'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/RabbitSmaccer.php b/src/aiptu/smaccer/entity/npc/RabbitSmaccer.php new file mode 100644 index 0000000..7508c95 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/RabbitSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.402 : 0.67; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.402 : 0.67; + } + + public static function getNetworkTypeId() : string { + return EntityIds::RABBIT; + } + + public function getName() : string { + return 'Rabbit'; + } + + public function getBabyScale() : float { + return 0.6; + } +} diff --git a/src/aiptu/smaccer/entity/npc/RavagerSmaccer.php b/src/aiptu/smaccer/entity/npc/RavagerSmaccer.php new file mode 100644 index 0000000..e1d389f --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/RavagerSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2.2; + } + + public function getWidth() : float { + return 1.95; + } + + public static function getNetworkTypeId() : string { + return EntityIds::RAVAGER; + } + + public function getName() : string { + return 'Ravager'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SalmonSmaccer.php b/src/aiptu/smaccer/entity/npc/SalmonSmaccer.php new file mode 100644 index 0000000..a005264 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SalmonSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.5; + } + + public function getWidth() : float { + return 0.5; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SALMON; + } + + public function getName() : string { + return 'Salmon'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SheepSmaccer.php b/src/aiptu/smaccer/entity/npc/SheepSmaccer.php new file mode 100644 index 0000000..df75e8d --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SheepSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.65 : 1.3; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SHEEP; + } + + public function getName() : string { + return 'Sheep'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ShulkerSmaccer.php b/src/aiptu/smaccer/entity/npc/ShulkerSmaccer.php new file mode 100644 index 0000000..c4542e6 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ShulkerSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1; + } + + public function getWidth() : float { + return 1; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SHULKER; + } + + public function getName() : string { + return 'Shulker'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SilverfishSmaccer.php b/src/aiptu/smaccer/entity/npc/SilverfishSmaccer.php new file mode 100644 index 0000000..92880f9 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SilverfishSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.3; + } + + public function getWidth() : float { + return 0.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SILVERFISH; + } + + public function getName() : string { + return 'Silverfish'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SkeletonHorseSmaccer.php b/src/aiptu/smaccer/entity/npc/SkeletonHorseSmaccer.php new file mode 100644 index 0000000..716cccd --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SkeletonHorseSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.8 : 1.6; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.7 : 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SKELETON_HORSE; + } + + public function getName() : string { + return 'Skeleton Horse'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SkeletonSmaccer.php b/src/aiptu/smaccer/entity/npc/SkeletonSmaccer.php new file mode 100644 index 0000000..daec956 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SkeletonSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SKELETON; + } + + public function getName() : string { + return 'Skeleton'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SlimeSmaccer.php b/src/aiptu/smaccer/entity/npc/SlimeSmaccer.php new file mode 100644 index 0000000..e887342 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SlimeSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2.08; + } + + public function getWidth() : float { + return 2.08; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SLIME; + } + + public function getName() : string { + return 'Slime'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SnifferSmaccer.php b/src/aiptu/smaccer/entity/npc/SnifferSmaccer.php new file mode 100644 index 0000000..f99aa23 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SnifferSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.7875 : 1.75; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.855 : 1.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SNIFFER; + } + + public function getName() : string { + return 'Sniffer'; + } + + public function getBabyScale() : float { + return 0.45; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SnowGolemSmaccer.php b/src/aiptu/smaccer/entity/npc/SnowGolemSmaccer.php new file mode 100644 index 0000000..a6f8aa0 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SnowGolemSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.8; + } + + public function getWidth() : float { + return 0.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SNOW_GOLEM; + } + + public function getName() : string { + return 'Snow Golem'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SpiderSmaccer.php b/src/aiptu/smaccer/entity/npc/SpiderSmaccer.php new file mode 100644 index 0000000..bfb1397 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SpiderSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.9; + } + + public function getWidth() : float { + return 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SPIDER; + } + + public function getName() : string { + return 'Spider'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/SquidSmaccer.php b/src/aiptu/smaccer/entity/npc/SquidSmaccer.php new file mode 100644 index 0000000..122130f --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/SquidSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.475 : 0.95; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.475 : 0.95; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SQUID; + } + + public function getName() : string { + return 'Squid'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/StraySmaccer.php b/src/aiptu/smaccer/entity/npc/StraySmaccer.php new file mode 100644 index 0000000..fd309f3 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/StraySmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::STRAY; + } + + public function getName() : string { + return 'Stray'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/StriderSmaccer.php b/src/aiptu/smaccer/entity/npc/StriderSmaccer.php new file mode 100644 index 0000000..2111f0d --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/StriderSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.85 : 1.7; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::STRIDER; + } + + public function getName() : string { + return 'Strider'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/TadpoleSmaccer.php b/src/aiptu/smaccer/entity/npc/TadpoleSmaccer.php new file mode 100644 index 0000000..0267fd5 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/TadpoleSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.6; + } + + public function getWidth() : float { + return 0.8; + } + + public static function getNetworkTypeId() : string { + return EntityIds::TADPOLE; + } + + public function getName() : string { + return 'Tadpole'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/TraderLlamaSmaccer.php b/src/aiptu/smaccer/entity/npc/TraderLlamaSmaccer.php new file mode 100644 index 0000000..98f2dcc --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/TraderLlamaSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.935 : 1.87; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.45 : 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::TRADER_LLAMA; + } + + public function getName() : string { + return 'Trader Llama'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/TropicalfishSmaccer.php b/src/aiptu/smaccer/entity/npc/TropicalfishSmaccer.php new file mode 100644 index 0000000..6e77574 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/TropicalfishSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.4; + } + + public function getWidth() : float { + return 0.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::TROPICALFISH; + } + + public function getName() : string { + return 'Tropicalfish'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/TurtleSmaccer.php b/src/aiptu/smaccer/entity/npc/TurtleSmaccer.php new file mode 100644 index 0000000..173eed2 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/TurtleSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.032 : 0.2; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.096 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::TURTLE; + } + + public function getName() : string { + return 'Turtle'; + } + + public function getBabyScale() : float { + return 0.16; + } +} diff --git a/src/aiptu/smaccer/entity/npc/VexSmaccer.php b/src/aiptu/smaccer/entity/npc/VexSmaccer.php new file mode 100644 index 0000000..c531be7 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/VexSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.8; + } + + public function getWidth() : float { + return 0.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::VEX; + } + + public function getName() : string { + return 'Vex'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/VillagerSmaccer.php b/src/aiptu/smaccer/entity/npc/VillagerSmaccer.php new file mode 100644 index 0000000..352b213 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/VillagerSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::VILLAGER; + } + + public function getName() : string { + return 'Villager'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/VillagerV2Smaccer.php b/src/aiptu/smaccer/entity/npc/VillagerV2Smaccer.php new file mode 100644 index 0000000..a0d9231 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/VillagerV2Smaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::VILLAGER_V2; + } + + public function getName() : string { + return 'Villager V2'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/VindicatorSmaccer.php b/src/aiptu/smaccer/entity/npc/VindicatorSmaccer.php new file mode 100644 index 0000000..f25d67f --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/VindicatorSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::VINDICATOR; + } + + public function getName() : string { + return 'Vindicator'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/WanderingTraderSmaccer.php b/src/aiptu/smaccer/entity/npc/WanderingTraderSmaccer.php new file mode 100644 index 0000000..5c5ca13 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/WanderingTraderSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WANDERING_TRADER; + } + + public function getName() : string { + return 'Wandering Trader'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/WardenSmaccer.php b/src/aiptu/smaccer/entity/npc/WardenSmaccer.php new file mode 100644 index 0000000..9567de5 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/WardenSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2.9; + } + + public function getWidth() : float { + return 0.9; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WARDEN; + } + + public function getName() : string { + return 'Warden'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/WitchSmaccer.php b/src/aiptu/smaccer/entity/npc/WitchSmaccer.php new file mode 100644 index 0000000..9139fa2 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/WitchSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WITCH; + } + + public function getName() : string { + return 'Witch'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/WitherSkeletonSmaccer.php b/src/aiptu/smaccer/entity/npc/WitherSkeletonSmaccer.php new file mode 100644 index 0000000..b3dbffe --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/WitherSkeletonSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2.01; + } + + public function getWidth() : float { + return 0.72; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WITHER_SKELETON; + } + + public function getName() : string { + return 'Wither Skeleton'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/WitherSmaccer.php b/src/aiptu/smaccer/entity/npc/WitherSmaccer.php new file mode 100644 index 0000000..743f2cd --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/WitherSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 3; + } + + public function getWidth() : float { + return 1; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WITHER; + } + + public function getName() : string { + return 'Wither'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/WolfSmaccer.php b/src/aiptu/smaccer/entity/npc/WolfSmaccer.php new file mode 100644 index 0000000..dbb4bd9 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/WolfSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.4 : 0.8; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WOLF; + } + + public function getName() : string { + return 'Wolf'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ZoglinSmaccer.php b/src/aiptu/smaccer/entity/npc/ZoglinSmaccer.php new file mode 100644 index 0000000..72733d2 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ZoglinSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.425 : 0.85; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.425 : 0.85; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ZOGLIN; + } + + public function getName() : string { + return 'Zoglin'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ZombieHorseSmaccer.php b/src/aiptu/smaccer/entity/npc/ZombieHorseSmaccer.php new file mode 100644 index 0000000..3845626 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ZombieHorseSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.8 : 1.6; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.7 : 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ZOMBIE_HORSE; + } + + public function getName() : string { + return 'Zombie Horse'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ZombieSmaccer.php b/src/aiptu/smaccer/entity/npc/ZombieSmaccer.php new file mode 100644 index 0000000..c693ee2 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ZombieSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ZOMBIE; + } + + public function getName() : string { + return 'Zombie'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ZombieVillagerSmaccer.php b/src/aiptu/smaccer/entity/npc/ZombieVillagerSmaccer.php new file mode 100644 index 0000000..6210304 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ZombieVillagerSmaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ZOMBIE_VILLAGER; + } + + public function getName() : string { + return 'Zombie Villager'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ZombieVillagerV2Smaccer.php b/src/aiptu/smaccer/entity/npc/ZombieVillagerV2Smaccer.php new file mode 100644 index 0000000..f2e69a4 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/ZombieVillagerV2Smaccer.php @@ -0,0 +1,40 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ZOMBIE_VILLAGER_V2; + } + + public function getName() : string { + return 'Zombie Villager V2'; + } +} diff --git a/src/aiptu/smaccer/entity/query/QueryHandler.php b/src/aiptu/smaccer/entity/query/QueryHandler.php new file mode 100644 index 0000000..66bf661 --- /dev/null +++ b/src/aiptu/smaccer/entity/query/QueryHandler.php @@ -0,0 +1,187 @@ +}> */ + private array $queries = []; + private int $nextId = 1; + + public function __construct(CompoundTag $nbt) { + $queriesTag = $nbt->getTag(self::NBT_QUERIES_KEY); + if ($queriesTag instanceof ListTag) { + foreach ($queriesTag as $tag) { + if ($tag instanceof CompoundTag) { + $type = $tag->getString(self::NBT_TYPE_KEY); + if ($type === self::TYPE_SERVER) { + $ip = $tag->getString(self::NBT_IP_KEY); + $port = $tag->getInt(self::NBT_PORT_KEY); + $this->addServerQuery($ip, $port); + } elseif ($type === self::TYPE_WORLD) { + $worldName = $tag->getString(self::NBT_WORLD_NAME_KEY); + $this->addWorldQuery($worldName); + } + } + } + } + } + + /** + * Adds a server query (IP or domain and port) and returns its ID. If invalid, returns null. + */ + public function addServerQuery(string $ipOrDomain, int $port) : ?int { + $ipOrDomain = trim($ipOrDomain); + + if (!Utils::isValidIpOrDomain($ipOrDomain) || !Utils::isValidPort($port)) { + return null; + } + + $id = $this->nextId++; + $this->queries[$id] = [ + 'type' => self::TYPE_SERVER, + 'value' => [self::NBT_IP_KEY => $ipOrDomain, self::NBT_PORT_KEY => $port], + ]; + return $id; + } + + /** + * Adds a world query (world name) and returns its ID. If invalid, returns null. + */ + public function addWorldQuery(string $worldName) : ?int { + $worldName = trim($worldName); + + if ($worldName === '') { + return null; + } + + $id = $this->nextId++; + $this->queries[$id] = [ + 'type' => self::TYPE_WORLD, + 'value' => [self::NBT_WORLD_NAME_KEY => $worldName], + ]; + return $id; + } + + /** + * Edits an existing server query identified by its ID. + */ + public function editServerQuery(int $id, string $newIpOrDomain, int $newPort) : bool { + $newIpOrDomain = trim($newIpOrDomain); + + if (!$this->exists($id) || !Utils::isValidIpOrDomain($newIpOrDomain) || !Utils::isValidPort($newPort)) { + return false; + } + + $this->queries[$id] = [ + 'type' => self::TYPE_SERVER, + 'value' => [self::NBT_IP_KEY => $newIpOrDomain, self::NBT_PORT_KEY => $newPort], + ]; + return true; + } + + public function editWorldQuery(int $id, string $newWorldName) : bool { + $newWorldName = trim($newWorldName); + + if (!$this->exists($id) || $newWorldName === '') { + return false; + } + + $this->queries[$id] = [ + 'type' => self::TYPE_WORLD, + 'value' => [self::NBT_WORLD_NAME_KEY => $newWorldName], + ]; + return true; + } + + /** + * Checks if a specific query exists in the NBT data. + */ + public function queryExistsInNBT(CompoundTag $nbt, string $type, string $queryValue, ?int $port = null) : bool { + $queryValue = trim($queryValue); + + $queriesTag = $nbt->getTag(self::NBT_QUERIES_KEY); + if ($queriesTag instanceof ListTag) { + foreach ($queriesTag as $tag) { + if ($tag instanceof CompoundTag && $tag->getString(self::NBT_TYPE_KEY) === $type) { + switch ($type) { + case self::TYPE_SERVER: + if ($tag->getString(self::NBT_IP_KEY) === $queryValue && $tag->getInt(self::NBT_PORT_KEY) === $port) { + return true; + } + + break; + case self::TYPE_WORLD: + if ($tag->getString(self::NBT_WORLD_NAME_KEY) === $queryValue) { + return true; + } + + break; + } + } + } + } + + return false; + } + + /** + * Checks if a query with the given ID exists. + */ + public function exists(int $id) : bool { + return isset($this->queries[$id]); + } + + /** + * Retrieves all queries. + * + * @return array}> + */ + public function getAll() : array { + return $this->queries; + } + + /** + * Removes the query with the specified ID. + */ + public function removeById(int $id) : bool { + if ($this->exists($id)) { + unset($this->queries[$id]); + return true; + } + + return false; + } + + /** + * Clears all queries. + */ + public function clearAll() : void { + $this->queries = []; + $this->nextId = 1; + } +} diff --git a/src/aiptu/smaccer/entity/query/QueryInfo.php b/src/aiptu/smaccer/entity/query/QueryInfo.php new file mode 100644 index 0000000..5d5b932 --- /dev/null +++ b/src/aiptu/smaccer/entity/query/QueryInfo.php @@ -0,0 +1,104 @@ +query['type']) { + case QueryHandler::TYPE_SERVER: + return $this->fetchServerQueryMessage(); + case QueryHandler::TYPE_WORLD: + return $this->generateWorldMessage(); + default: + return null; + } + } + + private function fetchServerQueryMessage() : string { + $key = $this->getCacheKey(); + + $this->scheduleServerQuery(); + + return self::$latestResults[$key] ?? 'Querying server...'; + } + + private function scheduleServerQuery() : void { + $server = Server::getInstance(); + $taskData = [ + 'ip' => $this->query['value'][QueryHandler::NBT_IP_KEY], + 'port' => (int) $this->query['value'][QueryHandler::NBT_PORT_KEY], + 'messages' => [ + 'online' => $this->query['onlineMessage'], + 'offline' => $this->query['offlineMessage'], + ], + 'cacheKey' => $this->getCacheKey(), + ]; + + $task = new QueryServerTask([$taskData]); + $server->getAsyncPool()->submitTask($task); + } + + private function getCacheKey() : string { + return "{$this->query['value'][QueryHandler::NBT_IP_KEY]}:{$this->query['value'][QueryHandler::NBT_PORT_KEY]}"; + } + + private function generateWorldMessage() : string { + $worldNames = explode('&', $this->query['value'][QueryHandler::NBT_WORLD_NAME_KEY]); + $totalPlayerCount = 0; + $loadedWorlds = []; + $notLoadedWorlds = []; + + foreach ($worldNames as $worldName) { + $world = Server::getInstance()->getWorldManager()->getWorldByName($worldName); + if ($world !== null) { + $totalPlayerCount += count($world->getPlayers()); + $loadedWorlds[] = $worldName; + } else { + $notLoadedWorlds[] = $worldName; + } + } + + $loadedWorldsString = implode(', ', $loadedWorlds); + $notLoadedWorldsString = implode(', ', $notLoadedWorlds); + + if (count($notLoadedWorlds) > 0) { + return str_replace( + ['{world_names}', '{count}', '{not_loaded_worlds}'], + [$loadedWorldsString, (string) $totalPlayerCount, $notLoadedWorldsString], + $this->query['worldNotLoadedFormat'] + ); + } + + return str_replace( + ['{world_names}', '{count}'], + [$loadedWorldsString, (string) $totalPlayerCount], + $this->query['worldMessageFormat'] + ); + } + + public static function updateCache(string $key, string $result) : void { + self::$latestResults[$key] = $result; + } +} diff --git a/src/aiptu/smaccer/entity/trait/CommandTrait.php b/src/aiptu/smaccer/entity/trait/CommandTrait.php new file mode 100644 index 0000000..9b98bf0 --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/CommandTrait.php @@ -0,0 +1,126 @@ +commandHandler = new CommandHandler($nbt); + } + + public function saveCommand(CompoundTag $nbt) : void { + $commands = array_map(function ($commandData) { + $commandTag = CompoundTag::create(); + $commandTag->setString(CommandHandler::KEY_COMMAND, $commandData[CommandHandler::KEY_COMMAND]); + $commandTag->setString(CommandHandler::KEY_TYPE, $commandData[CommandHandler::KEY_TYPE]); + return $commandTag; + }, $this->commandHandler->getAll()); + + $listTag = new ListTag($commands, NBT::TAG_Compound); + $nbt->setTag(EntityTag::COMMANDS, $listTag); + } + + public function canExecuteCommands(Player $player) : bool { + $plugin = Smaccer::getInstance(); + $settings = $plugin->getDefaultSettings(); + $cooldownEnabled = $settings->isCommandCooldownEnabled(); + $cooldown = $settings->getCommandCooldownValue(); + + if ($player->hasPermission(Permissions::BYPASS_COOLDOWN)) { + return true; + } + + if ($cooldownEnabled && $cooldown > 0) { + $playerName = strtolower($player->getName()); + $npcId = $this->getId(); + $lastHitTime = $this->commandCooldowns[$playerName][$npcId] ?? 0.0; + $currentTime = microtime(true); + $remainingCooldown = ($cooldown + $lastHitTime) - $currentTime; + + if ($remainingCooldown > 0) { + $player->sendMessage(TextFormat::RED . 'Please wait ' . round($remainingCooldown, 1) . ' seconds before interacting again.'); + return false; + } + + $this->commandCooldowns[$playerName][$npcId] = $currentTime; + } + + return true; + } + + public function executeCommands(Player $player) : void { + $commands = $this->commandHandler->getAll(); + $playerName = $player->getName(); + + foreach ($commands as $commandData) { + $command = str_replace('{player}', '"' . $playerName . '"', $commandData[CommandHandler::KEY_COMMAND]); + $this->dispatchCommand($player, $command, $commandData[CommandHandler::KEY_TYPE]); + } + } + + public function dispatchCommand(Player $player, string $command, string $type) : void { + $plugin = Smaccer::getInstance(); + $server = $plugin->getServer(); + $commandMap = $server->getCommandMap(); + + match ($type) { + EntityTag::COMMAND_TYPE_SERVER => $commandMap->dispatch(new ConsoleCommandSender($server, $server->getLanguage()), $command), + EntityTag::COMMAND_TYPE_PLAYER => $commandMap->dispatch($player, $command), + default => throw new \InvalidArgumentException("Invalid command type: {$type}") + }; + } + + public function getCommandHandler() : CommandHandler { + return $this->commandHandler; + } + + public function addCommand(string $command, string $type) : ?int { + return $this->commandHandler->add($command, $type); + } + + public function editCommand(int $id, string $newCommand, string $newType) : bool { + return $this->commandHandler->edit($id, $newCommand, $newType); + } + + public function getCommands() : array { + return $this->commandHandler->getAll(); + } + + public function removeCommandById(int $id) : bool { + return $this->commandHandler->removeById($id); + } + + public function clearCommands() : void { + $this->commandHandler->clearAll(); + } +} diff --git a/src/aiptu/smaccer/entity/trait/CreatorTrait.php b/src/aiptu/smaccer/entity/trait/CreatorTrait.php new file mode 100644 index 0000000..d50c347 --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/CreatorTrait.php @@ -0,0 +1,43 @@ +creatorId = $nbt->getString(EntityTag::CREATOR); + } + + public function saveCreator(CompoundTag $nbt) : void { + $nbt->setString(EntityTag::CREATOR, $this->creatorId); + } + + public function getCreatorId() : string { + return $this->creatorId; + } + + public function getCreator() : ?Player { + return Server::getInstance()->getPlayerByRawUUID($this->creatorId); + } + + public function isOwnedBy(Player $player) : bool { + return $player->getUniqueId()->getBytes() === $this->creatorId; + } +} diff --git a/src/aiptu/smaccer/entity/trait/EmoteTrait.php b/src/aiptu/smaccer/entity/trait/EmoteTrait.php new file mode 100644 index 0000000..2333077 --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/EmoteTrait.php @@ -0,0 +1,147 @@ +getTag($tag) instanceof StringTag ? Smaccer::getInstance()->getEmoteManager()->getEmote($nbt->getString($tag)) : null; + } + + private function saveEmoteToNBT(CompoundTag $nbt, ?EmoteType $emote, string $tag) : void { + if ($emote !== null) { + $nbt->setString($tag, $emote->getUuid()); + } + } + + public function initializeEmote(CompoundTag $nbt) : void { + $this->actionEmote = $this->initializeEmoteFromNBT($nbt, EntityTag::ACTION_EMOTE); + $this->emote = $this->initializeEmoteFromNBT($nbt, EntityTag::EMOTE); + } + + public function saveEmote(CompoundTag $nbt) : void { + $this->saveEmoteToNBT($nbt, $this->actionEmote, EntityTag::ACTION_EMOTE); + $this->saveEmoteToNBT($nbt, $this->emote, EntityTag::EMOTE); + } + + public function setActionEmote(?EmoteType $actionEmote) : void { + $this->actionEmote = $actionEmote; + $this->saveNBT(); + } + + public function getActionEmote() : ?EmoteType { + return $this->actionEmote; + } + + public function setEmote(?EmoteType $emote) : void { + $this->emote = $emote; + $this->saveNBT(); + } + + public function getEmote() : ?EmoteType { + return $this->emote; + } + + public function canPerformEmote(string $emote) : bool { + $currentTime = microtime(true); + if (isset($this->emoteCooldowns[$emote]) && ($currentTime - $this->emoteCooldowns[$emote]) < Smaccer::getInstance()->getDefaultSettings()->getEmoteCooldownValue()) { + return false; + } + + $this->emoteCooldowns[$emote] = $currentTime; + return true; + } + + public function canPerformActionEmote(string $actionEmote) : bool { + $currentTime = microtime(true); + if (isset($this->actionEmoteCooldowns[$actionEmote]) && ($currentTime - $this->actionEmoteCooldowns[$actionEmote]) < Smaccer::getInstance()->getDefaultSettings()->getActionEmoteCooldownValue()) { + return false; + } + + $this->actionEmoteCooldowns[$actionEmote] = $currentTime; + return true; + } + + public function performEmote(string $emote, ?array $targets = null) : void { + $emoteType = Smaccer::getInstance()->getEmoteManager()->getEmote($emote); + if ($emoteType === null) { + return; + } + + $event = new NPCPerformEmoteEvent($this, $emoteType); + $event->call(); + if ($event->isCancelled()) { + return; + } + + $this->broadcastEmote($event->getEmote()->getUuid(), $targets); + } + + public function performActionEmote(string $actionEmote, ?array $targets = null) : void { + $actionEmoteType = Smaccer::getInstance()->getEmoteManager()->getEmote($actionEmote); + if ($actionEmoteType === null) { + return; + } + + $event = new NPCPerformActionEmoteEvent($this, $actionEmoteType); + $event->call(); + if ($event->isCancelled()) { + return; + } + + $this->broadcastEmote($event->getActionEmote()->getUuid(), $targets); + } + + private function broadcastEmote(string $emote, ?array $targets = null) : void { + NetworkBroadcastUtils::broadcastPackets($targets ?? $this->getViewers(), [ + EmotePacket::create($this->getId(), $emote, '', '', EmotePacket::FLAG_MUTE_ANNOUNCEMENT), + ]); + } + + protected function entityBaseTick(int $tickDiff = 1) : bool { + $hasUpdate = parent::entityBaseTick($tickDiff); + + if ($this->getEmote() !== null) { + $emoteUuid = $this->getEmote()->getUuid(); + + if (Smaccer::getInstance()->getDefaultSettings()->isEmoteCooldownEnabled()) { + if ($this->canPerformEmote($emoteUuid)) { + $this->performEmote($emoteUuid); + $hasUpdate = true; + } + } else { + $this->performEmote($emoteUuid); + $hasUpdate = true; + } + } + + return $hasUpdate; + } +} diff --git a/src/aiptu/smaccer/entity/trait/InventoryTrait.php b/src/aiptu/smaccer/entity/trait/InventoryTrait.php new file mode 100644 index 0000000..98f9a40 --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/InventoryTrait.php @@ -0,0 +1,82 @@ +getArmorInventory()->getContents(); + $this->getArmorInventory()->setContents($armorContents); + } + + public function setHelmet(Item|Player $source) : void { + $item = $source instanceof Player ? $source->getArmorInventory()->getHelmet() : $source; + $this->getArmorInventory()->setHelmet($item); + } + + public function setChestplate(Item|Player $source) : void { + $item = $source instanceof Player ? $source->getArmorInventory()->getChestplate() : $source; + $this->getArmorInventory()->setChestplate($item); + } + + public function setLeggings(Item|Player $source) : void { + $item = $source instanceof Player ? $source->getArmorInventory()->getLeggings() : $source; + $this->getArmorInventory()->setLeggings($item); + } + + public function setBoots(Item|Player $source) : void { + $item = $source instanceof Player ? $source->getArmorInventory()->getBoots() : $source; + $this->getArmorInventory()->setBoots($item); + } + + public function setOffHandItem(Item|Player $source) : void { + $offHandItem = $source instanceof Player ? $source->getOffHandInventory()->getItem(0) : $source; + $this->getOffHandInventory()->setItem(0, $offHandItem); + } + + public function setItemInHand(Item|Player $source) : void { + $itemInHand = $source instanceof Player ? $source->getInventory()->getItemInHand() : $source; + $this->getInventory()->setItemInHand($itemInHand); + } + + public function getArmor() : array { + return $this->getArmorInventory()->getContents(); + } + + public function getHelmet() : Item { + return $this->getArmorInventory()->getHelmet(); + } + + public function getChestplate() : Item { + return $this->getArmorInventory()->getChestplate(); + } + + public function getLeggings() : Item { + return $this->getArmorInventory()->getLeggings(); + } + + public function getBoots() : Item { + return $this->getArmorInventory()->getBoots(); + } + + public function getOffHandItem() : Item { + return $this->getOffHandInventory()->getItem(0); + } + + public function getItemInHand() : Item { + return $this->getInventory()->getItemInHand(); + } +} diff --git a/src/aiptu/smaccer/entity/trait/NametagTrait.php b/src/aiptu/smaccer/entity/trait/NametagTrait.php new file mode 100644 index 0000000..5bef210 --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/NametagTrait.php @@ -0,0 +1,65 @@ +getNameTag(); + $event = new NPCNameTagChangeEvent($this, $oldNameTag, $nameTag); + $event->call(); + + if (!$event->isCancelled()) { + parent::setNameTag($event->getNewNameTag()); + } + } + + /** + * @param array|null $targets + * @param array $data Properly formatted entity data, defaults to everything + * + * @phpstan-param array $data + */ + public function sendData(?array $targets, ?array $data = null) : void { + parent::sendData($targets, $data); + + foreach ($this->hasSpawned as $player) { + $nametag = $this->applyNametag(null, $player); + $data[EntityMetadataProperties::NAMETAG] = new StringMetadataProperty($nametag); + $networkSession = $player->getNetworkSession(); + $networkSession->getEntityEventBroadcaster()->syncActorData([$networkSession], $this, $data); + } + } + + public function applyNametag(?string $nametag, Player $player) : string { + $nametag ??= $this->getNameTag(); + + $vars = [ + '{player}' => $player->getName(), + '{display_name}' => $player->getDisplayName(), + '{line}' => "\n", + ]; + + return TextFormat::colorize(str_replace(array_keys($vars), array_values($vars), $nametag)); + } +} diff --git a/src/aiptu/smaccer/entity/trait/QueryTrait.php b/src/aiptu/smaccer/entity/trait/QueryTrait.php new file mode 100644 index 0000000..dac794e --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/QueryTrait.php @@ -0,0 +1,138 @@ +queryHandler = new QueryHandler($nbt); + } + + public function saveQuery(CompoundTag $nbt) : void { + $queriesTag = new ListTag(); + + foreach ($this->queryHandler->getAll() as $query) { + $queryTag = new CompoundTag(); + + $queryTag->setString(QueryHandler::NBT_TYPE_KEY, $query['type']); + + if ($query['type'] === QueryHandler::TYPE_SERVER) { + $queryTag->setString(QueryHandler::NBT_IP_KEY, (string) $query['value'][QueryHandler::NBT_IP_KEY]); + $queryTag->setInt(QueryHandler::NBT_PORT_KEY, (int) $query['value'][QueryHandler::NBT_PORT_KEY]); + } elseif ($query['type'] === QueryHandler::TYPE_WORLD) { + $queryTag->setString(QueryHandler::NBT_WORLD_NAME_KEY, (string) $query['value'][QueryHandler::NBT_WORLD_NAME_KEY]); + } + + $queriesTag->push($queryTag); + } + + $nbt->setTag(QueryHandler::NBT_QUERIES_KEY, $queriesTag); + } + + public function onUpdate(int $currentTick) : bool { + $result = parent::onUpdate($currentTick); + + $this->updateNameTag(); + + return $result; + } + + public function updateNameTag() : void { + $queries = $this->queryHandler->getAll(); + $currentNameTag = $this->getNameTag(); + $newNameTagParts = []; + + $nonQueryPart = strtok($currentNameTag, "\n"); + + if ($nonQueryPart !== false) { + $newNameTagParts[] = $nonQueryPart; + } + + usort($queries, function ($a, $b) { + if ($a['type'] === $b['type']) { + return 0; + } + + return $a['type'] === QueryHandler::TYPE_SERVER ? -1 : 1; + }); + + foreach ($queries as $query) { + $queryInfo = new QueryInfo([ + 'type' => $query['type'], + 'value' => $query['value'], + 'onlineMessage' => Smaccer::getInstance()->getServerOnlineFormat(), + 'offlineMessage' => Smaccer::getInstance()->getServerOfflineFormat(), + 'worldMessageFormat' => Smaccer::getInstance()->getWorldMessageFormat(), + 'worldNotLoadedFormat' => Smaccer::getInstance()->getWorldNotLoadedFormat(), + ]); + + $nameTagPart = $queryInfo->getNameTagPart(); + if ($nameTagPart !== null) { + $newNameTagParts[] = $nameTagPart; + } + } + + $newNameTag = implode("\n", $newNameTagParts); + if ($newNameTag !== $currentNameTag) { + $this->setNameTag($newNameTag); + } + } + + public function getQueryHandler() : QueryHandler { + return $this->queryHandler; + } + + public function addServerQuery(string $ipOrDomain, int $port) : ?int { + return $this->queryHandler->addServerQuery($ipOrDomain, $port); + } + + public function addWorldQuery(string $worldName) : ?int { + return $this->queryHandler->addWorldQuery($worldName); + } + + public function editServerQuery(int $id, string $newIpOrDomain, int $newPort) : bool { + return $this->queryHandler->editServerQuery($id, $newIpOrDomain, $newPort); + } + + public function editWorldQuery(int $id, string $newWorldName) : bool { + return $this->queryHandler->editWorldQuery($id, $newWorldName); + } + + public function queryExistsInNBT(CompoundTag $nbt, string $type, string $queryValue, ?int $port = null) : bool { + return $this->queryHandler->queryExistsInNBT($nbt, $type, $queryValue, $port); + } + + public function getQueries() : array { + return $this->queryHandler->getAll(); + } + + public function removeQueryById(int $id) : bool { + return $this->queryHandler->removeById($id); + } + + public function clearQueries() : void { + $this->queryHandler->clearAll(); + } +} diff --git a/src/aiptu/smaccer/entity/trait/RotationTrait.php b/src/aiptu/smaccer/entity/trait/RotationTrait.php new file mode 100644 index 0000000..b9e4e7b --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/RotationTrait.php @@ -0,0 +1,37 @@ +setRotateToPlayers((bool) $nbt->getByte(EntityTag::ROTATE_TO_PLAYERS, 1)); + } + + public function saveRotation(CompoundTag $nbt) : void { + $nbt->setByte(EntityTag::ROTATE_TO_PLAYERS, (int) $this->rotateToPlayers); + } + + public function setRotateToPlayers(bool $value = true) : void { + $this->rotateToPlayers = $value; + } + + public function canRotateToPlayers() : bool { + return $this->rotateToPlayers; + } +} diff --git a/src/aiptu/smaccer/entity/trait/SkinTrait.php b/src/aiptu/smaccer/entity/trait/SkinTrait.php new file mode 100644 index 0000000..c0ed6ad --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/SkinTrait.php @@ -0,0 +1,40 @@ +setSkin(new Skin( + $this->getSkin()->getSkinId(), + $skinData, + $this->getSkin()->getCapeData(), + $this->getSkin()->getGeometryName(), + $this->getSkin()->getGeometryData() + )); + $this->sendSkin(); + } + + public function changeCape(string $capeData) : void { + $this->setSkin(new Skin( + $this->getSkin()->getSkinId(), + $this->getSkin()->getSkinData(), + $capeData, + $this->getSkin()->getGeometryName(), + $this->getSkin()->getGeometryData() + )); + $this->sendSkin(); + } +} diff --git a/src/aiptu/smaccer/entity/trait/SlapBackTrait.php b/src/aiptu/smaccer/entity/trait/SlapBackTrait.php new file mode 100644 index 0000000..9f772ca --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/SlapBackTrait.php @@ -0,0 +1,52 @@ +setSlapBack((bool) $nbt->getByte(EntityTag::SLAP_BACK, 1)); + } + + public function saveSlapBack(CompoundTag $nbt) : void { + $nbt->setByte(EntityTag::SLAP_BACK, (int) $this->slapBack); + } + + public function setSlapBack(bool $value = true) : void { + $this->slapBack = $value; + } + + public function canSlapBack() : bool { + return $this->slapBack; + } + + public function slapBack() : void { + $event = new NPCSlapBackActionEvent($this, $this->slapBack); + $event->call(); + if ($event->isCancelled()) { + return; + } + + $slapBack = $event->canSlapBack(); + if ($slapBack) { + $this->broadcastAnimation(new ArmSwingAnimation($this)); + } + } +} diff --git a/src/aiptu/smaccer/entity/trait/VisibilityTrait.php b/src/aiptu/smaccer/entity/trait/VisibilityTrait.php new file mode 100644 index 0000000..8553515 --- /dev/null +++ b/src/aiptu/smaccer/entity/trait/VisibilityTrait.php @@ -0,0 +1,64 @@ +setVisibility(EntityVisibility::fromInt($nbt->getInt(EntityTag::VISIBILITY, EntityVisibility::VISIBLE_TO_EVERYONE->value))); + } + + public function saveVisibility(CompoundTag $nbt) : void { + $nbt->setInt(EntityTag::VISIBILITY, $this->visibility->value); + } + + public function getVisibility() : EntityVisibility { + return $this->visibility; + } + + public function setVisibility(EntityVisibility $visibility) : void { + if ($this->visibility !== $visibility) { + $event = new NPCVisibilityChangeEvent($this, $this->visibility, $visibility); + $event->call(); + if ($event->isCancelled()) { + return; + } + + $this->visibility = $event->getNewVisibility(); + } + + switch ($this->visibility) { + case EntityVisibility::VISIBLE_TO_EVERYONE: + $this->spawnToAll(); + break; + case EntityVisibility::VISIBLE_TO_CREATOR: + $creator = $this->getCreator(); + if ($creator !== null) { + $this->despawnFromAll(); + $this->spawnTo($creator); + } + + break; + case EntityVisibility::INVISIBLE_TO_EVERYONE: + $this->despawnFromAll(); + break; + } + } +} diff --git a/src/aiptu/smaccer/entity/utils/EntityTag.php b/src/aiptu/smaccer/entity/utils/EntityTag.php new file mode 100644 index 0000000..46a5eb9 --- /dev/null +++ b/src/aiptu/smaccer/entity/utils/EntityTag.php @@ -0,0 +1,30 @@ +name) === $lowercasedValue) { + return $visibility; + } + } + + throw new \InvalidArgumentException("Invalid visibility string: {$value}"); + } + + public static function getAll() : array { + return array_column( + array_map(fn ($visibility) => ['value' => $visibility->value, 'name' => $visibility->name], self::cases()), + 'name', + 'value' + ); + } +} diff --git a/src/aiptu/smaccer/event/NPCAttackEvent.php b/src/aiptu/smaccer/event/NPCAttackEvent.php new file mode 100644 index 0000000..e4889e1 --- /dev/null +++ b/src/aiptu/smaccer/event/NPCAttackEvent.php @@ -0,0 +1,33 @@ +entity; + } +} diff --git a/src/aiptu/smaccer/event/NPCDespawnEvent.php b/src/aiptu/smaccer/event/NPCDespawnEvent.php new file mode 100644 index 0000000..6e46c96 --- /dev/null +++ b/src/aiptu/smaccer/event/NPCDespawnEvent.php @@ -0,0 +1,28 @@ + + */ +class NPCDespawnEvent extends EntityEvent implements Cancellable { + use CancellableTrait; + + public function __construct(protected Entity $entity) {} +} diff --git a/src/aiptu/smaccer/event/NPCInteractEvent.php b/src/aiptu/smaccer/event/NPCInteractEvent.php new file mode 100644 index 0000000..fe8bf14 --- /dev/null +++ b/src/aiptu/smaccer/event/NPCInteractEvent.php @@ -0,0 +1,33 @@ +entity; + } +} diff --git a/src/aiptu/smaccer/event/NPCNameTagChangeEvent.php b/src/aiptu/smaccer/event/NPCNameTagChangeEvent.php new file mode 100644 index 0000000..efece58 --- /dev/null +++ b/src/aiptu/smaccer/event/NPCNameTagChangeEvent.php @@ -0,0 +1,44 @@ + + */ +class NPCNameTagChangeEvent extends EntityEvent implements Cancellable { + use CancellableTrait; + + public function __construct( + protected Entity $entity, + private string $oldNameTag, + private string $newNameTag + ) {} + + public function getOldNameTag() : string { + return $this->oldNameTag; + } + + public function getNewNameTag() : string { + return $this->newNameTag; + } + + public function setNewNameTag(string $newNameTag) : void { + $this->newNameTag = $newNameTag; + } +} diff --git a/src/aiptu/smaccer/event/NPCPerformActionEmoteEvent.php b/src/aiptu/smaccer/event/NPCPerformActionEmoteEvent.php new file mode 100644 index 0000000..2f2acfd --- /dev/null +++ b/src/aiptu/smaccer/event/NPCPerformActionEmoteEvent.php @@ -0,0 +1,40 @@ + + */ +class NPCPerformActionEmoteEvent extends EntityEvent implements Cancellable { + use CancellableTrait; + + public function __construct( + protected Entity $entity, + private EmoteType $actionEmote + ) {} + + public function getActionEmote() : EmoteType { + return $this->actionEmote; + } + + public function setActionEmote(EmoteType $actionEmote) : void { + $this->actionEmote = $actionEmote; + } +} diff --git a/src/aiptu/smaccer/event/NPCPerformEmoteEvent.php b/src/aiptu/smaccer/event/NPCPerformEmoteEvent.php new file mode 100644 index 0000000..64fa6f7 --- /dev/null +++ b/src/aiptu/smaccer/event/NPCPerformEmoteEvent.php @@ -0,0 +1,40 @@ + + */ +class NPCPerformEmoteEvent extends EntityEvent implements Cancellable { + use CancellableTrait; + + public function __construct( + protected Entity $entity, + private EmoteType $emote + ) {} + + public function getEmote() : EmoteType { + return $this->emote; + } + + public function setEmote(EmoteType $emote) : void { + $this->emote = $emote; + } +} diff --git a/src/aiptu/smaccer/event/NPCSlapBackActionEvent.php b/src/aiptu/smaccer/event/NPCSlapBackActionEvent.php new file mode 100644 index 0000000..57096c5 --- /dev/null +++ b/src/aiptu/smaccer/event/NPCSlapBackActionEvent.php @@ -0,0 +1,39 @@ + + */ +class NPCSlapBackActionEvent extends EntityEvent implements Cancellable { + use CancellableTrait; + + public function __construct( + protected Entity $entity, + private bool $slapBack + ) {} + + public function canSlapBack() : bool { + return $this->slapBack; + } + + public function setSlapBack(bool $value = true) : void { + $this->slapBack = $value; + } +} diff --git a/src/aiptu/smaccer/event/NPCSpawnEvent.php b/src/aiptu/smaccer/event/NPCSpawnEvent.php new file mode 100644 index 0000000..4b118ce --- /dev/null +++ b/src/aiptu/smaccer/event/NPCSpawnEvent.php @@ -0,0 +1,28 @@ + + */ +class NPCSpawnEvent extends EntityEvent implements Cancellable { + use CancellableTrait; + + public function __construct(protected Entity $entity) {} +} diff --git a/src/aiptu/smaccer/event/NPCUpdateEvent.php b/src/aiptu/smaccer/event/NPCUpdateEvent.php new file mode 100644 index 0000000..8b53f88 --- /dev/null +++ b/src/aiptu/smaccer/event/NPCUpdateEvent.php @@ -0,0 +1,40 @@ + + */ +class NPCUpdateEvent extends EntityEvent implements Cancellable { + use CancellableTrait; + + public function __construct( + protected Entity $entity, + protected NPCData $npcData + ) {} + + public function getNPCData() : NPCData { + return $this->npcData; + } + + public function setNPCData(NPCData $npcData) : void { + $this->npcData = $npcData; + } +} diff --git a/src/aiptu/smaccer/event/NPCVisibilityChangeEvent.php b/src/aiptu/smaccer/event/NPCVisibilityChangeEvent.php new file mode 100644 index 0000000..76707c0 --- /dev/null +++ b/src/aiptu/smaccer/event/NPCVisibilityChangeEvent.php @@ -0,0 +1,45 @@ + + */ +class NPCVisibilityChangeEvent extends EntityEvent implements Cancellable { + use CancellableTrait; + + public function __construct( + protected Entity $entity, + private EntityVisibility $oldVisibility, + private EntityVisibility $newVisibility + ) {} + + public function getOldVisibility() : EntityVisibility { + return $this->oldVisibility; + } + + public function getNewVisibility() : EntityVisibility { + return $this->newVisibility; + } + + public function setNewVisibility(EntityVisibility $newVisibility) : void { + $this->newVisibility = $newVisibility; + } +} diff --git a/src/aiptu/smaccer/tasks/LoadEmotesTask.php b/src/aiptu/smaccer/tasks/LoadEmotesTask.php new file mode 100644 index 0000000..ddb3fd2 --- /dev/null +++ b/src/aiptu/smaccer/tasks/LoadEmotesTask.php @@ -0,0 +1,60 @@ +cachedFilePath); + + if ($currentCommitId === null) { + throw new RuntimeException('Failed to fetch current commit ID'); + } + + if ($cachedFile === null || $cachedFile['commit_id'] !== $currentCommitId) { + $emotes = EmoteUtils::getEmotes(); + if ($emotes === null) { + throw new RuntimeException('Failed to fetch emote list'); + } + + EmoteUtils::saveEmoteToCache($this->cachedFilePath, $currentCommitId, $emotes); + + $this->setResult($emotes); + return; + } + + $this->setResult($cachedFile['emotes']); + } + + public function onCompletion() : void { + /** @var array{array{uuid: string, title: string, image: string}} $result */ + $result = $this->getResult(); + if (!is_array($result)) { + throw new RuntimeException('Emotes result is not an array'); + } + + Smaccer::getInstance()->setEmoteManager(new EmoteManager($result)); + } +} diff --git a/src/aiptu/smaccer/tasks/QueryServerTask.php b/src/aiptu/smaccer/tasks/QueryServerTask.php new file mode 100644 index 0000000..0d003af --- /dev/null +++ b/src/aiptu/smaccer/tasks/QueryServerTask.php @@ -0,0 +1,81 @@ + */ + private ThreadSafeArray $taskData; + + /** + * @param array $taskData + */ + public function __construct(array $taskData) { + $this->taskData = ThreadSafeArray::fromArray($taskData); + } + + public function onRun() : void { + $resultData = []; + foreach ($this->taskData as $data) { + /** @var TaskData $data */ + try { + $queryData = PMQuery::query($data['ip'], $data['port']); + $onlinePlayers = $queryData['Players']; + $maxOnlinePlayers = $queryData['MaxPlayers']; + $resultMessage = str_replace( + ['{online}', '{max_online}'], + [$onlinePlayers, $maxOnlinePlayers], + $data['messages']['online'] + ); + } catch (PmQueryException $e) { + $resultMessage = $data['messages']['offline']; + } + + $resultData[] = [ + 'cacheKey' => $data['cacheKey'], + 'resultMessage' => $resultMessage, + ]; + } + + $this->setResult($resultData); + } + + public function onCompletion() : void { + $result = $this->getResult(); + if (is_array($result)) { + foreach ($result as $data) { + /** @var array{cacheKey: string, resultMessage: string} $data */ + QueryInfo::updateCache($data['cacheKey'], $data['resultMessage']); + } + } + } +} diff --git a/src/aiptu/smaccer/utils/EmoteUtils.php b/src/aiptu/smaccer/utils/EmoteUtils.php new file mode 100644 index 0000000..1e75da4 --- /dev/null +++ b/src/aiptu/smaccer/utils/EmoteUtils.php @@ -0,0 +1,150 @@ +getBody(), true, flags: JSON_THROW_ON_ERROR); + if (!is_array($data) || !isset($data['sha']) || !is_string($data['sha'])) { + return null; + } + + return $data['sha']; + } + + /** + * Retrieve a list of emotes in emotes.json from this github repository https://github.com/TwistedAsylumMC/Bedrock-Emotes. + * + * @return array{ + * array{ + * uuid: string, + * title: string, + * image: string + * } + * }|null An array of associative arrays, each containing: + * - 'uuid' (string): The unique identifier of the emote. + * - 'title' (string): The title of the emote. + * - 'image' (string): The URL to the thumbnail image of the emote. + * + * Or `null` if there is an issue with fetching the emotes. + */ + public static function getEmotes() : ?array { + $response = Internet::getURL(self::EMOTES_URL); + if ($response === null) { + return null; + } + + /** @var array{array{uuid: string, title: string, image: string}} $data */ + $data = json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR); + if (!is_array($data)) { + return null; + } + + foreach ($data as $emote) { + if (!isset($emote['uuid'], $emote['title'], $emote['image']) || !is_string($emote['uuid']) || !is_string($emote['title']) || !is_string($emote['image'])) { + return null; + } + } + + return $data; + } + + /** + * Retrieve emotes from a cache file. + * + * @param string $cacheFilePath the path to the cache file + * + * @return array{ + * commit_id: string, + * emotes: array + * }|null Returns an associative array with `commit_id` and `emotes` if the cache file exists, + * or `null` if the file does not exist + */ + public static function getEmotesFromCache(string $cacheFilePath) : ?array { + if (file_exists($cacheFilePath)) { + $data = json_decode(Filesystem::fileGetContents($cacheFilePath), true, flags: JSON_THROW_ON_ERROR); + if (!is_array($data) || !isset($data['commit_id'], $data['emotes']) || !is_string($data['commit_id']) || !is_array($data['emotes'])) { + return null; + } + + foreach ($data['emotes'] as $emote) { + if (!isset($emote['uuid'], $emote['title'], $emote['image']) || !is_string($emote['uuid']) || !is_string($emote['title']) || !is_string($emote['image'])) { + return null; + } + } + + return $data; + } + + return null; + } + + /** + * Save emotes to a cache file. + * + * @param string $cacheFilePath the path to the cache file will be saved + * @param string $commitId the Current Commit ID + * @param array{ + * array{ + * uuid: string, + * title: string, + * image: string + * } + * } $emotes the array of emotes list + */ + public static function saveEmoteToCache(string $cacheFilePath, string $commitId, array $emotes) : void { + $jsonData = json_encode([ + 'commit_id' => $commitId, + 'emotes' => $emotes, + ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); + if ($jsonData === false) { + throw new \RuntimeException('Failed to encode emotes to JSON.'); + } + + Filesystem::safeFilePutContents($cacheFilePath, $jsonData); + } + + /** + * Get the emote file path. + * + * @return string the emote file path + */ + public static function getEmoteCachePath() : string { + return Smaccer::getInstance()->getDataFolder() . 'emotes_cache.json'; + } +} diff --git a/src/aiptu/smaccer/utils/FormManager.php b/src/aiptu/smaccer/utils/FormManager.php new file mode 100644 index 0000000..4713cd2 --- /dev/null +++ b/src/aiptu/smaccer/utils/FormManager.php @@ -0,0 +1,1228 @@ + match ($selected->getValue()) { + 0 => self::sendEntitySelectionForm($player, 0, $onSubmit), + 1 => self::sendNPCIdSelectionForm($player, self::ACTION_DELETE), + 2 => self::sendNPCIdSelectionForm($player, self::ACTION_EDIT), + 3 => self::sendNPCListForm($player), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + $entityData = SmaccerHandler::getInstance()->getEntitiesInfo($player); + $ownedEntityCount = $entityData['count']; + + if ($ownedEntityCount > 0) { + $form->appendOptions( + 'Create NPC', + 'Delete NPC', + 'Edit NPC', + 'List NPCs' + ); + } else { + $form->appendOptions('Create NPC'); + } + + $player->sendForm($form); + } + + public static function sendEntitySelectionForm(Player $player, int $page, callable $onEntitySelected) : void { + $entityTypes = array_keys(SmaccerHandler::getInstance()->getRegisteredNPC()); + + $start = $page * self::ITEMS_PER_PAGE; + $end = min($start + self::ITEMS_PER_PAGE, count($entityTypes)); + + $buttons = array_map( + fn ($type) => new Button($type, Image::url("https://raw.githubusercontent.com/AIPTU/Smaccer/assets/faces/{$type}.png")), + array_slice($entityTypes, $start, self::ITEMS_PER_PAGE) + ); + + if ($page > 0) { + $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); + } + + if ($end < count($entityTypes)) { + $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); + } + + $player->sendForm( + new MenuForm( + 'Select Entity', + 'Choose an entity to create:', + $buttons, + function (Player $player, Button $selected) use ($entityTypes, $page, $onEntitySelected) : void { + $selectedText = $selected->text; + if ($selectedText === self::PREVIOUS_PAGE) { + self::sendEntitySelectionForm($player, $page - 1, $onEntitySelected); + } elseif ($selectedText === self::NEXT_PAGE) { + self::sendEntitySelectionForm($player, $page + 1, $onEntitySelected); + } else { + $selectedEntityType = $entityTypes[array_search($selectedText, $entityTypes, true)]; + $onEntitySelected($player, $selectedEntityType); + } + } + ) + ); + } + + public static function sendCreateNPCForm(Player $player, string $entityType, callable $onNPCFormSubmit) : void { + $entityClass = SmaccerHandler::getInstance()->getNPC($entityType); + if ($entityClass === null) { + return; + } + + $settings = Smaccer::getInstance()->getDefaultSettings(); + $rotationEnabled = $settings->isRotationEnabled(); + $nametagVisible = $settings->isNametagVisible(); + $defaultVisibility = $settings->getEntityVisibility()->value; + $gravityEnabled = $settings->isGravityEnabled(); + + $formElements = [ + new Input('Enter NPC name tag', 'NPC Name', ''), + new Input('Set NPC scale (0.1 - 10.0)', '1.0', '1.0'), + new Toggle('Enable rotation?', $rotationEnabled), + new Toggle('Set name tag visible', $nametagVisible), + new StepSlider('Select visibility', array_values(EntityVisibility::getAll()), $defaultVisibility), + new Toggle('Enable gravity?', $gravityEnabled), + ]; + + if (is_a($entityClass, EntityAgeable::class, true)) { + $formElements[] = new Toggle('Is baby?', false); + } + + if (is_a($entityClass, HumanSmaccer::class, true)) { + $formElements[] = new Toggle('Enable slapback?', $settings->isSlapEnabled()); + } + + $player->sendForm( + new CustomForm( + 'Spawn NPC', + $formElements, + function (Player $player, CustomFormResponse $response) use ($entityType, $onNPCFormSubmit) : void { + $onNPCFormSubmit($player, $response, $entityType); + } + ) + ); + } + + public static function handleCreateNPCResponse(Player $player, CustomFormResponse $response, string $entityType) : void { + $entityClass = SmaccerHandler::getInstance()->getNPC($entityType); + if ($entityClass === null) { + return; + } + + $values = $response->getValues(); + + $nameTag = $values[0]; + $scaleStr = $values[1]; + $rotationEnabled = $values[2]; + $nameTagVisible = $values[3]; + $visibility = $values[4]; + $gravityEnabled = $values[5]; + + if (!is_string($nameTag) || !is_numeric($scaleStr) || !is_bool($rotationEnabled) || !is_bool($nameTagVisible) || !is_string($visibility) || !is_bool($gravityEnabled)) { + $player->sendMessage(TextFormat::RED . 'Invalid form values.'); + return; + } + + $scale = (float) $scaleStr; + if ($scale < 0.1 || $scale > 10.0) { + $player->sendMessage(TextFormat::RED . 'Invalid scale value. Please enter a number between 0.1 and 10.0.'); + return; + } + + $visibilityEnum = EntityVisibility::fromString($visibility); + + $npcData = NPCData::create() + ->setNameTag($nameTag) + ->setScale($scale) + ->setRotationEnabled($rotationEnabled) + ->setNametagVisible($nameTagVisible) + ->setVisibility($visibilityEnum) + ->setHasGravity($gravityEnabled); + + $index = 6; + + if (is_a($entityClass, EntityAgeable::class, true) && isset($values[$index])) { + $isBaby = (bool) $values[$index]; + $npcData->setBaby($isBaby); + ++$index; + } + + if (is_a($entityClass, HumanSmaccer::class, true)) { + if (isset($values[$index])) { + $enableSlapback = (bool) $values[$index]; + $npcData->setSlapBack($enableSlapback); + ++$index; + } + } + + SmaccerHandler::getInstance()->spawnNPC($entityType, $player, $npcData)->onCompletion( + function (Entity $entity) use ($player) : void { + if (($entity instanceof HumanSmaccer) || ($entity instanceof EntitySmaccer)) { + $player->sendMessage(TextFormat::GREEN . 'NPC ' . $entity->getName() . ' created successfully! ID: ' . $entity->getId()); + } + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to spawn npc: ' . $e->getMessage()); + } + ); + } + + public static function sendNPCIdSelectionForm(Player $player, string $action) : void { + $player->sendForm( + new CustomForm( + 'Select NPC', + [ + new Input("Enter the ID of the NPC to {$action}", 'NPC ID', ''), + ], + function (Player $player, CustomFormResponse $response) use ($action) : void { + $npcId = (int) $response->getInput()->getValue(); + $npc = Smaccer::getInstance()->getServer()->getWorldManager()->findEntity($npcId); + + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + $player->sendMessage(TextFormat::RED . 'NPC with ID ' . $npcId . ' not found!'); + return; + } + + $hasPermission = match ($action) { + self::ACTION_DELETE => $player->hasPermission(Permissions::COMMAND_DELETE_OTHERS), + self::ACTION_EDIT => $player->hasPermission(Permissions::COMMAND_EDIT_OTHERS), + default => false, + }; + + if (!$npc->isOwnedBy($player) && !$hasPermission) { + $player->sendMessage(TextFormat::RED . "You don't have permission to {$action} this entity!"); + return; + } + + match ($action) { + self::ACTION_DELETE => self::confirmDeleteNPC($player, $npc), + self::ACTION_EDIT => self::sendEditMenuForm($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid action.'), + }; + } + ) + ); + } + + public static function confirmDeleteNPC(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + ModalForm::confirm( + 'Confirm Deletion', + "Are you sure you want to delete NPC: {$npc->getName()}?", + function (Player $player) use ($npc) : void { + SmaccerHandler::getInstance()->despawnNPC($npc->getCreatorId(), $npc)->onCompletion( + function (bool $success) use ($player, $npc) : void { + $player->sendMessage(TextFormat::GREEN . 'NPC ' . $npc->getName() . ' with ID ' . $npc->getId() . ' despawned successfully.'); + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to despawn npc: ' . $e->getMessage()); + } + ); + } + ) + ); + } + + public static function sendEditMenuForm(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $form = MenuForm::withOptions( + 'Edit NPC', + 'Choose an edit option:', + [ + 'General Settings', + 'Commands', + 'Teleport NPC to Player', + 'Teleport Player to NPC', + 'Query Settings', + ], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendEditNPCForm($player, $npc), + 1 => self::sendEditCommandsForm($player, $npc), + 2 => self::sendTeleportOptionsForm($player, $npc, self::TELEPORT_NPC_TO_PLAYER), + 3 => self::sendTeleportOptionsForm($player, $npc, self::TELEPORT_PLAYER_TO_NPC), + 4 => self::sendQueryManagementForm($player, $npc), + 5 => self::handleEmoteSelection($player, $npc), + 6 => self::sendEditSkinSettingsForm($player, $npc), + 7 => self::sendArmorSettingsForm($player, $npc), + 8 => self::equipHeldItem($player, $npc), + 9 => self::equipOffHandItem($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + if ($npc instanceof HumanSmaccer) { + $form->appendOptions( + 'Emote Settings', + 'Skin Settings', + 'Armor Settings', + 'Equip Held Item', + 'Equip Off-Hand Item' + ); + } + + $player->sendForm($form); + } + + public static function sendEditNPCForm(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $visibilityValues = array_values(EntityVisibility::getAll()); + $currentVisibility = $npc->getVisibility()->name; + $defaultVisibilityIndex = array_search($currentVisibility, $visibilityValues, true); + $defaultVisibility = Smaccer::getInstance()->getDefaultSettings()->getEntityVisibility()->value; + + $formElements = [ + new Input('Edit NPC name tag', 'NPC Name', $npc->getNameTag()), + new Input('Set NPC scale (0.1 - 10.0)', '1.0', (string) $npc->getScale()), + new Toggle('Enable rotation?', $npc->canRotateToPlayers()), + new Toggle('Set name tag visible', $npc->isNameTagVisible()), + new StepSlider('Select visibility', $visibilityValues, $defaultVisibilityIndex !== false ? (int) $defaultVisibilityIndex : $defaultVisibility), + new Toggle('Enable gravity?', $npc->hasGravity()), + ]; + + if ($npc instanceof EntityAgeable) { + $formElements[] = new Toggle('Is baby?', $npc->isBaby()); + } + + if ($npc instanceof HumanSmaccer) { + $formElements[] = new Toggle('Enable slapback?', $npc->canSlapBack()); + } + + $player->sendForm( + new CustomForm( + 'Edit NPC', + $formElements, + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $values = $response->getValues(); + + $nameTag = $values[0]; + $scaleStr = $values[1]; + $rotationEnabled = $values[2]; + $nameTagVisible = $values[3]; + $visibility = $values[4]; + $gravityEnabled = $values[5]; + + if (!is_string($nameTag) || !is_numeric($scaleStr) || !is_bool($rotationEnabled) || !is_bool($nameTagVisible) || !is_string($visibility) || !is_bool($gravityEnabled)) { + $player->sendMessage(TextFormat::RED . 'Invalid form values.'); + return; + } + + $scale = (float) $scaleStr; + if ($scale < 0.1 || $scale > 10.0) { + $player->sendMessage(TextFormat::RED . 'Invalid scale value. Please enter a number between 0.1 and 10.0.'); + return; + } + + $visibilityEnum = EntityVisibility::fromString($visibility); + + $npcData = NPCData::create() + ->setNameTag($nameTag) + ->setScale($scale) + ->setRotationEnabled($rotationEnabled) + ->setNametagVisible($nameTagVisible) + ->setVisibility($visibilityEnum) + ->setHasGravity($gravityEnabled); + + $index = 6; + + if ($npc instanceof EntityAgeable && isset($values[$index])) { + $isBaby = (bool) $values[$index]; + $npcData->setBaby($isBaby); + ++$index; + } + + if ($npc instanceof HumanSmaccer) { + if (isset($values[$index])) { + $enableSlapback = (bool) $values[$index]; + $npcData->setSlapBack($enableSlapback); + ++$index; + } + } + + SmaccerHandler::getInstance()->editNPC($player, $npc, $npcData)->onCompletion( + function (bool $success) use ($player, $npc) : void { + $player->sendMessage(TextFormat::GREEN . 'NPC ' . $npc->getName() . ' updated successfully!'); + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to edit NPC: ' . $e->getMessage()); + } + ); + } + ) + ); + } + + public static function sendEditCommandsForm(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $commandHandler = $npc->getCommandHandler(); + + $form = new MenuForm( + 'Edit Commands', + 'Choose a command operation:', + onSubmit: fn (Player $player, Button $selected) => match ($selected->text) { + 'Add' => self::sendAddCommandForm($player, $npc), + 'List' => self::sendListCommandsForm($player, $npc), + 'Clear' => self::confirmClearCommands($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + if (count($commandHandler->getAll()) > 0) { + $form->appendOptions( + 'Add', + 'List', + 'Clear' + ); + } else { + $form->appendOptions('Add'); + } + + $player->sendForm($form); + } + + public static function sendAddCommandForm(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + new CustomForm( + 'Add Command', + [ + new Input('Enter command', 'command', ''), + new Dropdown('Select command type', [ + EntityTag::COMMAND_TYPE_PLAYER, + EntityTag::COMMAND_TYPE_SERVER, + ]), + ], + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $command = $response->getInput()->getValue(); + $commandType = $response->getDropdown()->getSelectedOption(); + + if ($npc->addCommand($command, $commandType) !== null) { + $player->sendMessage(TextFormat::GREEN . "Command added to NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to add command for NPC {$npc->getName()}."); + } + } + ) + ); + } + + public static function sendListCommandsForm(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $commands = $npc->getCommands(); + + $buttons = array_map( + fn ($id, $data) => new Button("Command: {$data['command']} (Type: {$data['type']})"), + array_keys($commands), + $commands + ); + + $player->sendForm( + new MenuForm( + 'List Commands', + 'Commands for NPC:', + $buttons, + function (Player $player, Button $selected) use ($npc, $commands) : void { + $selectedText = $selected->text; + foreach ($commands as $id => $data) { + if ("Command: {$data['command']} (Type: {$data['type']})" === $selectedText) { + self::handleCommandSelection($player, $npc, $id, $data['command'], $data['type']); + break; + } + } + } + ) + ); + } + + public static function handleCommandSelection(Player $player, Entity $npc, int $commandId, string $command, string $type) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + MenuForm::withOptions( + 'Edit or Remove Command', + "Command: {$command} (Type: {$type})", + [ + 'Edit', + 'Remove', + ], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendEditCommandForm($player, $npc, $commandId, $command, $type), + 1 => self::confirmRemoveCommand($player, $npc, $commandId), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ) + ); + } + + public static function sendEditCommandForm(Player $player, Entity $npc, int $commandId, string $command, string $type) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $defaultTypeIndex = array_search($type, [EntityTag::COMMAND_TYPE_PLAYER, EntityTag::COMMAND_TYPE_SERVER], true); + + $player->sendForm( + new CustomForm( + 'Edit Command', + [ + new Input('Edit command', 'command', $command), + new Dropdown('Select command type', [ + EntityTag::COMMAND_TYPE_PLAYER, + EntityTag::COMMAND_TYPE_SERVER, + ], $defaultTypeIndex !== false ? $defaultTypeIndex : 0), + ], + function (Player $player, CustomFormResponse $response) use ($npc, $commandId) : void { + $newCommand = $response->getInput()->getValue(); + $newType = $response->getDropdown()->getSelectedOption(); + + if ($npc->editCommand($commandId, $newCommand, $newType)) { + $player->sendMessage(TextFormat::GREEN . "Command updated for NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to update command for NPC {$npc->getName()}."); + } + } + ) + ); + } + + public static function confirmRemoveCommand(Player $player, Entity $npc, int $commandId) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + ModalForm::confirm( + 'Confirm Remove Command', + "Are you sure you want to remove this command from NPC: {$npc->getName()}?", + function (Player $player) use ($npc, $commandId) : void { + $npc->removeCommandById($commandId); + $player->sendMessage(TextFormat::GREEN . "Command removed from NPC {$npc->getName()}."); + } + ) + ); + } + + public static function confirmClearCommands(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + ModalForm::confirm( + 'Confirm Clear Commands', + "Are you sure you want to clear all commands from NPC: {$npc->getName()}?", + function (Player $player) use ($npc) : void { + $npc->clearCommands(); + $player->sendMessage(TextFormat::GREEN . "All commands cleared from NPC {$npc->getName()}."); + } + ) + ); + } + + public static function sendTeleportOptionsForm(Player $player, Entity $npc, string $action) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $server = Smaccer::getInstance()->getServer(); + $playerNames = array_map(fn ($player) => $player->getName(), $server->getOnlinePlayers()); + + $player->sendForm( + MenuForm::withOptions( + 'Teleport Options', + 'Select a player:', + $playerNames, + function (Player $player, Button $selected) use ($server, $npc, $action) : void { + $selectedPlayerName = $selected->text; + $selectedPlayer = $server->getPlayerExact($selectedPlayerName); + + if ($selectedPlayer !== null) { + if ($action === self::TELEPORT_NPC_TO_PLAYER) { + $npc->teleport($selectedPlayer->getLocation()); + $player->sendMessage(TextFormat::GREEN . "NPC {$npc->getName()} has been teleported to {$selectedPlayerName}'s location."); + } elseif ($action === self::TELEPORT_PLAYER_TO_NPC) { + $player->teleport($npc->getLocation()); + $player->sendMessage(TextFormat::GREEN . "You have been teleported to NPC {$npc->getName()}'s location."); + } + } else { + $player->sendMessage(TextFormat::RED . 'Player not found.'); + } + } + ) + ); + } + + public static function handleEmoteSelection(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + MenuForm::withOptions( + 'Edit Emote', + 'Choose an emote option:', + [ + 'Action Emote', + 'Emote', + ], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendEditActionEmoteForm($player, $npc), + 1 => self::sendEditEmoteForm($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ) + ); + } + + public static function sendEditActionEmoteForm(Player $player, Entity $npc, int $page = 0) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $actionEmoteOptions = array_merge([new EmoteType('', 'None', '')], Smaccer::getInstance()->getEmoteManager()->getAll()); + $defaultActionEmote = $npc->getActionEmote(); + $currentActionEmote = $defaultActionEmote === null ? 'None' : $defaultActionEmote->getTitle(); + + $start = $page * self::ITEMS_PER_PAGE; + $end = min($start + self::ITEMS_PER_PAGE, count($actionEmoteOptions)); + + $buttons = array_map(function (EmoteType $emote) { + $image = $emote->getTitle() !== 'None' ? $emote->getImage() : null; + return $image !== null ? new Button($emote->getTitle(), Image::url($image)) : new Button($emote->getTitle()); + }, array_slice($actionEmoteOptions, $start, self::ITEMS_PER_PAGE)); + + if ($page > 0) { + $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); + } + + if ($end < count($actionEmoteOptions)) { + $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); + } + + $player->sendForm( + new MenuForm( + 'Action Emote', + 'Current action emote: ' . $currentActionEmote, + $buttons, + function (Player $player, Button $selected) use ($npc, $page, $actionEmoteOptions, $start) : void { + $buttonText = $selected->text; + $buttonValue = $selected->getValue(); + + if ($buttonText === self::PREVIOUS_PAGE) { + self::sendEditActionEmoteForm($player, $npc, $page - 1); + } elseif ($buttonText === self::NEXT_PAGE) { + self::sendEditActionEmoteForm($player, $npc, $page + 1); + } else { + if ($buttonText !== 'None') { + $actionEmote = $actionEmoteOptions[$start + $buttonValue]; + + $npc->setActionEmote($actionEmote); + } else { + $npc->setActionEmote(null); + } + + $player->sendMessage(TextFormat::GREEN . "Action emote updated for NPC {$npc->getName()}."); + } + } + ) + ); + } + + public static function sendEditEmoteForm(Player $player, Entity $npc, int $page = 0) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $emoteOptions = array_merge([new EmoteType('', 'None', '')], Smaccer::getInstance()->getEmoteManager()->getAll()); + $defaultEmote = $npc->getEmote(); + $currentEmote = $defaultEmote === null ? 'None' : $defaultEmote->getTitle(); + + $start = $page * self::ITEMS_PER_PAGE; + $end = min($start + self::ITEMS_PER_PAGE, count($emoteOptions)); + + $buttons = array_map(function (EmoteType $emote) { + $image = $emote->getTitle() !== 'None' ? $emote->getImage() : null; + return $image !== null ? new Button($emote->getTitle(), Image::url($image)) : new Button($emote->getTitle()); + }, array_slice($emoteOptions, $start, self::ITEMS_PER_PAGE)); + + if ($page > 0) { + $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); + } + + if ($end < count($emoteOptions)) { + $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); + } + + $player->sendForm( + new MenuForm( + 'Emote', + 'Current emote: ' . $currentEmote, + $buttons, + function (Player $player, Button $selected) use ($npc, $page, $emoteOptions, $start) : void { + $buttonText = $selected->text; + $buttonValue = $selected->getValue(); + + if ($buttonText === self::PREVIOUS_PAGE) { + self::sendEditEmoteForm($player, $npc, $page - 1); + } elseif ($buttonText === self::NEXT_PAGE) { + self::sendEditEmoteForm($player, $npc, $page + 1); + } else { + if ($buttonText !== 'None') { + $emote = $emoteOptions[$start + $buttonValue]; + + $npc->setEmote($emote); + } else { + $npc->setEmote(null); + } + + $player->sendMessage(TextFormat::GREEN . "Emote updated for NPC {$npc->getName()}."); + } + } + ) + ); + } + + public static function sendEditSkinSettingsForm(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + MenuForm::withOptions( + 'Skin Settings', + 'Select an option:', + [ + 'Edit Skin', + 'Edit Cape', + ], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendEditSkinForm($player, $npc), + 1 => self::sendEditCapeForm($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ) + ); + } + + public static function sendEditSkinForm(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + MenuForm::withOptions( + 'Edit Skin', + 'Select an option:', + [ + 'Change Skin from Player', + 'Change Skin from URL', + ], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendChangeSkinFromPlayerForm($player, $npc), + 1 => self::sendChangeSkinFromURLForm($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ) + ); + } + + public static function sendChangeSkinFromPlayerForm(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $server = Smaccer::getInstance()->getServer(); + $onlinePlayers = $server->getOnlinePlayers(); + $playerNames = array_map(fn ($player) => $player->getName(), $onlinePlayers); + + $player->sendForm( + MenuForm::withOptions( + 'Change Skin from Player', + 'Select a player:', + $playerNames, + function (Player $player, Button $selected) use ($server, $npc) : void { + $selectedPlayerName = $selected->text; + $selectedPlayer = $server->getPlayerExact($selectedPlayerName); + + if ($selectedPlayer !== null) { + $npc->setSkin($selectedPlayer->getSkin()); + $npc->sendSkin(); + $player->sendMessage(TextFormat::GREEN . "Skin updated for NPC {$npc->getName()} from player {$selectedPlayerName}."); + } else { + $player->sendMessage(TextFormat::RED . 'Player not found.'); + } + } + ) + ); + } + + public static function sendChangeSkinFromURLForm(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $formElements = [ + new Input('Enter skin URL', 'https://example.com/skin.png'), + ]; + + $player->sendForm( + new CustomForm( + 'Change Skin from URL', + $formElements, + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $url = $response->getInput()->getValue(); + + SkinUtils::skinFromURL($url)->onCompletion( + function (string $skinBytes) use ($player, $npc) : void { + $npc->changeSkin($skinBytes); + $player->sendMessage(TextFormat::GREEN . "Skin updated for NPC {$npc->getName()} from URL."); + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to update skin from URL: ' . $e->getMessage()); + } + ); + } + ) + ); + } + + public static function sendEditCapeForm(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $formElements = [ + new Input('Enter cape URL', 'https://example.com/cape.png'), + ]; + + $player->sendForm( + new CustomForm( + 'Change Cape from URL', + $formElements, + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $url = $response->getInput()->getValue(); + + SkinUtils::capeFromURL($url)->onCompletion( + function (string $capeBytes) use ($player, $npc) : void { + $npc->changeCape($capeBytes); + $player->sendMessage(TextFormat::GREEN . "Cape updated for NPC {$npc->getName()} from URL."); + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to update cape from URL: ' . $e->getMessage()); + } + ); + } + ) + ); + } + + public static function sendArmorSettingsForm(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + $player->sendMessage(TextFormat::RED . 'This NPC cannot wear armor.'); + return; + } + + $form = MenuForm::withOptions( + 'Armor Settings', + 'Choose an armor option:', + [ + 'Equip All Armor', + 'Equip Helmet', + 'Equip Chestplate', + 'Equip Leggings', + 'Equip Boots', + ], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::equipArmorPiece($player, $npc, self::ARMOR_ALL), + 1 => self::equipArmorPiece($player, $npc, self::ARMOR_HELMET), + 2 => self::equipArmorPiece($player, $npc, self::ARMOR_CHESTPLATE), + 3 => self::equipArmorPiece($player, $npc, self::ARMOR_LEGGINGS), + 4 => self::equipArmorPiece($player, $npc, self::ARMOR_BOOTS), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + $player->sendForm($form); + } + + public static function equipArmorPiece(Player $player, HumanSmaccer $npc, string $piece) : void { + $armorInventory = $player->getArmorInventory(); + + switch ($piece) { + case self::ARMOR_HELMET: + $npc->setHelmet($player); + break; + case self::ARMOR_CHESTPLATE: + $npc->setChestplate($player); + break; + case self::ARMOR_LEGGINGS: + $npc->setLeggings($player); + break; + case self::ARMOR_BOOTS: + $npc->setBoots($player); + break; + case self::ARMOR_ALL: + $npc->setArmor($player); + + $player->sendMessage(TextFormat::GREEN . "All armor equipped to NPC {$npc->getName()}."); + return; + default: + $player->sendMessage(TextFormat::RED . 'Invalid armor piece specified.'); + return; + } + + $player->sendMessage(TextFormat::GREEN . ucfirst($piece) . " equipped to NPC {$npc->getName()}."); + } + + public static function equipHeldItem(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $item = $player->getInventory()->getItemInHand(); + $npc->setItemInHand($item); + + $player->sendMessage(TextFormat::GREEN . "Held item equipped to NPC {$npc->getName()}."); + } + + public static function equipOffHandItem(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $item = $player->getOffHandInventory()->getItem(0); + $npc->setOffHandItem($item); + + $player->sendMessage(TextFormat::GREEN . "Off-hand item equipped to NPC {$npc->getName()}."); + } + + public static function sendNPCListForm(Player $player) : void { + $entityData = SmaccerHandler::getInstance()->getEntitiesInfo(null, true); + $totalEntityCount = $entityData['count']; + $entities = $entityData['infoList']; + + if ($totalEntityCount > 0) { + $content = TextFormat::RED . 'NPC List and Locations: (' . $totalEntityCount . ')'; + $content .= "\n" . TextFormat::WHITE . '- ' . implode("\n - ", $entities); + } else { + $content = TextFormat::RED . 'No NPCs found in any world.'; + } + + $player->sendForm(new MenuForm('List NPCs', $content)); + } + + public static function sendQueryManagementForm(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $form = MenuForm::withOptions( + 'Manage Queries', + 'Select a query type:', + [ + 'Add Server Query', + 'Add World Query', + 'Edit/Remove Server Query', + 'Edit/Remove World Query', + ], + fn (Player $player, Button $selected) => match ($selected->text) { + 'Add Server Query' => self::sendAddServerQueryForm($player, $npc), + 'Add World Query' => self::sendAddWorldQueryForm($player, $npc), + 'Edit/Remove Server Query' => self::sendEditRemoveQueryForm($player, $npc, QueryHandler::TYPE_SERVER), + 'Edit/Remove World Query' => self::sendEditRemoveQueryForm($player, $npc, QueryHandler::TYPE_WORLD), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + $player->sendForm($form); + } + + public static function sendAddServerQueryForm(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + new CustomForm( + 'Add Server Query', + [ + new Input('Enter IP/Domain', 'ip_or_domain'), + new Input('Enter Port', 'port'), + ], + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $values = $response->getValues(); + + $ipOrDomain = $values[0]; + $port = $values[1]; + + if (!is_string($ipOrDomain) || !is_numeric($port)) { + $player->sendMessage(TextFormat::RED . 'Invalid form values.'); + return; + } + + if ($npc->getQueryHandler()->addServerQuery($ipOrDomain, (int) $port) !== null) { + $player->sendMessage(TextFormat::GREEN . "Server query added to NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to add server query for NPC {$npc->getName()}."); + } + } + ) + ); + } + + public static function sendAddWorldQueryForm(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + new CustomForm( + 'Add World Query', + [ + new Input('Enter world name', 'world_name'), + ], + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $worldName = $response->getInput()->getValue(); + + if ($npc->getQueryHandler()->addWorldQuery($worldName) !== null) { + $player->sendMessage(TextFormat::GREEN . "World query added to NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to add world query for NPC {$npc->getName()}."); + } + } + ) + ); + } + + public static function sendEditRemoveQueryForm(Player $player, Entity $npc, string $queryType) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $queries = $npc->getQueryHandler()->getAll(); + $buttons = array_values(array_filter(array_map( + fn ($id, $data) => $queryType === $data['type'] + ? new Button( + $data['type'] === QueryHandler::TYPE_SERVER + ? "IP: {$data['value']['ip']} Port: {$data['value']['port']}" + : "World: {$data['value']['world_name']}" + ) + : null, + array_keys($queries), + $queries + ))); + + if (count($buttons) === 0) { + $player->sendMessage(TextFormat::RED . 'No queries found for the selected type.'); + return; + } + + $player->sendForm( + new MenuForm( + 'Edit/Remove Query', + 'Select a query to edit/remove:', + $buttons, + function (Player $player, Button $selected) use ($npc, $queries) : void { + $selectedText = $selected->text; + foreach ($queries as $id => $data) { + $expectedText = $data['type'] === QueryHandler::TYPE_SERVER + ? "IP: {$data['value']['ip']} Port: {$data['value']['port']}" + : "World: {$data['value']['world_name']}"; + if ($expectedText === $selectedText) { + self::handleQuerySelection($player, $npc, $id, $data['type'], $data['value']); + return; + } + } + + $player->sendMessage(TextFormat::RED . 'Failed to match the selected query.'); + } + ) + ); + } + + public static function handleQuerySelection(Player $player, Entity $npc, int $queryId, string $queryType, array $queryValue) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + MenuForm::withOptions( + 'Edit or Remove Query', + $queryType === QueryHandler::TYPE_SERVER + ? "IP: {$queryValue['ip']} Port: {$queryValue['port']}" + : "World: {$queryValue['world_name']}", + [ + 'Edit', + 'Remove', + ], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendEditQueryForm($player, $npc, $queryId, $queryType, $queryValue), + 1 => self::confirmRemoveQuery($player, $npc, $queryId), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ) + ); + } + + public static function sendEditQueryForm(Player $player, Entity $npc, int $queryId, string $queryType, array $queryValue) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + if ($queryType === QueryHandler::TYPE_SERVER) { + $player->sendForm( + new CustomForm( + 'Edit Server Query', + [ + new Input('Edit IP/Domain', 'ip_or_domain', $queryValue['ip']), + new Input('Edit Port', 'port', (string) $queryValue['port']), + ], + function (Player $player, CustomFormResponse $response) use ($npc, $queryId) : void { + $values = $response->getValues(); + + $newIpOrDomain = $values[0]; + $newPort = $values[1]; + + if (!is_string($newIpOrDomain) || !is_numeric($newPort)) { + $player->sendMessage(TextFormat::RED . 'Invalid form values.'); + return; + } + + if ($npc->getQueryHandler()->editServerQuery($queryId, $newIpOrDomain, (int) $newPort)) { + $player->sendMessage(TextFormat::GREEN . "Server query updated for NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to update server query for NPC {$npc->getName()}."); + } + } + ) + ); + } else { + $player->sendForm( + new CustomForm( + 'Edit World Query', + [ + new Input('Edit world name', 'world_name', $queryValue['world_name']), + ], + function (Player $player, CustomFormResponse $response) use ($npc, $queryId) : void { + $newWorldName = $response->getInput()->getValue(); + + if ($npc->getQueryHandler()->editWorldQuery($queryId, $newWorldName)) { + $player->sendMessage(TextFormat::GREEN . "World query updated for NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to update world query for NPC {$npc->getName()}."); + } + } + ) + ); + } + } + + public static function confirmRemoveQuery(Player $player, Entity $npc, int $queryId) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm( + ModalForm::confirm( + 'Confirm Remove Query', + "Are you sure you want to remove this query from NPC: {$npc->getName()}?", + function (Player $player) use ($npc, $queryId) : void { + $npc->getQueryHandler()->removeById($queryId); + $player->sendMessage(TextFormat::GREEN . "Query removed from NPC {$npc->getName()}."); + } + ) + ); + } +} diff --git a/src/aiptu/smaccer/utils/Permissions.php b/src/aiptu/smaccer/utils/Permissions.php new file mode 100644 index 0000000..36c914a --- /dev/null +++ b/src/aiptu/smaccer/utils/Permissions.php @@ -0,0 +1,33 @@ + a promise that resolves to the skin bytes + * + * @throws \InvalidArgumentException if the URL is invalid or not a PNG + */ + public static function skinFromURL(string $url) : Promise { + $resolver = new PromiseResolver(); + + try { + self::validateUrl($url); + self::validatePngUrl($url); + + Utils::fetchAsync($url, function ($result) use ($resolver) : void { + if ($result === null) { + $resolver->reject(new \RuntimeException('Failed to download skin.')); + return; + } + + $skinData = $result->getBody(); + $filePath = self::saveSkinToFile($skinData); + + $skinBytes = self::skinFromFile($filePath); + $resolver->resolve($skinBytes); + }); + } catch (\Throwable $e) { + $resolver->reject($e); + } + + return $resolver->getPromise(); + } + + /** + * Downloads a cape from a URL and returns the cape bytes in a promise. + * + * @param string $url the URL of the PNG cape + * + * @return Promise a promise that resolves to the cape bytes + * + * @throws \InvalidArgumentException if the URL is invalid or not a PNG + */ + public static function capeFromURL(string $url) : Promise { + $resolver = new PromiseResolver(); + + try { + self::validateUrl($url); + self::validatePngUrl($url); + + Utils::fetchAsync($url, function ($result) use ($resolver) : void { + if ($result === null) { + $resolver->reject(new \RuntimeException('Failed to download cape.')); + return; + } + + $capeData = $result->getBody(); + $filePath = self::saveSkinToFile($capeData); + + $capeBytes = self::capeFromFile($filePath); + $resolver->resolve($capeBytes); + }); + } catch (\Throwable $e) { + $resolver->reject($e); + } + + return $resolver->getPromise(); + } + + /** + * Processes a skin from a file path and returns the skin bytes. + * + * @param string $filePath the file path of the PNG skin + * + * @return string the skin bytes + * + * @throws \RuntimeException if the file is not a valid PNG skin + */ + public static function skinFromFile(string $filePath) : string { + return self::processPngFile($filePath, self::SKIN); + } + + /** + * Processes a cape from a file path and returns the cape bytes. + * + * @param string $filePath the file path of the PNG cape + * + * @return string the cape bytes + * + * @throws \RuntimeException if the file is not a valid PNG cape + */ + public static function capeFromFile(string $filePath) : string { + return self::processPngFile($filePath, self::CAPE); + } + + private static function processPngFile(string $filePath, string $type) : string { + $image = imagecreatefrompng($filePath); + if ($image === false) { + self::cleanupFile($filePath); + throw new \RuntimeException("The file is not a valid PNG {$type}."); + } + + if (!imageistruecolor($image)) { + imagepalettetotruecolor($image); + } + + $bytes = ($type === self::SKIN) ? self::extractSkinBytes($image) : self::extractCapeBytes($image); + imagedestroy($image); + self::cleanupFile($filePath); + + return $bytes; + } + + /** + * Validates that a URL is in the correct format. + * + * @param string $url the URL to validate + * + * @throws \InvalidArgumentException if the URL format is invalid + */ + private static function validateUrl(string $url) : void { + if (!Utils::isValidUrl($url)) { + throw new \InvalidArgumentException('Invalid URL format.'); + } + } + + /** + * Validates that a URL points to a PNG image. + * + * @param string $url the URL to validate + * + * @throws \InvalidArgumentException if the URL does not point to a PNG image + */ + private static function validatePngUrl(string $url) : void { + if (!Utils::isPngUrl($url)) { + throw new \InvalidArgumentException('URL does not point to a PNG image.'); + } + } + + /** + * Saves image data to a temporary file. + * + * @param string $data the image data to save + * + * @return string the file path where the image data was saved + * + * @throws \RuntimeException if there is an error saving the image data + */ + private static function saveSkinToFile(string $data) : string { + $filePath = Path::join(Smaccer::getInstance()->getDataFolder(), uniqid('skin_', true) . '.png'); + try { + Filesystem::safeFilePutContents($filePath, $data); + return $filePath; + } catch (\RuntimeException $e) { + throw new \RuntimeException('An error occurred while saving the skin file: ' . $e->getMessage()); + } + } + + /** + * Extracts the bytes from a GD image resource for skin. + * + * @param \GdImage $image the GD image resource + * + * @return string the extracted skin bytes + */ + private static function extractSkinBytes(\GdImage $image) : string { + $bytes = ''; + for ($y = 0; $y < imagesy($image); ++$y) { + for ($x = 0; $x < imagesx($image); ++$x) { + $rgba = imagecolorat($image, $x, $y); + $a = ((~($rgba >> 24)) << 1) & 0xFF; + $r = ($rgba >> 16) & 0xFF; + $g = ($rgba >> 8) & 0xFF; + $b = $rgba & 0xFF; + $bytes .= chr($r) . chr($g) . chr($b) . chr($a); + } + } + + return $bytes; + } + + /** + * Extracts the bytes from a GD image resource for cape. + * + * @param \GdImage $image the GD image resource + * + * @return string the extracted cape bytes + */ + private static function extractCapeBytes(\GdImage $image) : string { + $bytes = ''; + for ($y = 0; $y < imagesy($image); ++$y) { + for ($x = 0; $x < imagesx($image); ++$x) { + $argb = imagecolorat($image, $x, $y); + $bytes .= chr(($argb >> 16) & 0xFF) . chr(($argb >> 8) & 0xFF) . chr($argb & 0xFF) . chr(((~($argb >> 24)) << 1) & 0xFF); + } + } + + return $bytes; + } + + /** + * Deletes a file if it exists. + * + * @param string $filePath the path to the file to delete + */ + private static function cleanupFile(string $filePath) : void { + if (is_file($filePath)) { + unlink($filePath); + } + } +} diff --git a/src/aiptu/smaccer/utils/Utils.php b/src/aiptu/smaccer/utils/Utils.php new file mode 100644 index 0000000..6672cd0 --- /dev/null +++ b/src/aiptu/smaccer/utils/Utils.php @@ -0,0 +1,122 @@ + $results + */ + $bulkCurlTaskCallback = function (array $results) use ($callback) : void { + if (isset($results[0]) && !$results[0] instanceof InternetException) { + $callback($results[0]); + } else { + $callback(null); + } + }; + $task = new BulkCurlTask([ + new BulkCurlTaskOperation($url), + ], $bulkCurlTaskCallback); + Server::getInstance()->getAsyncPool()->submitTask($task); + } + + public static function isValidUrl(string $url) : bool { + return filter_var($url, FILTER_VALIDATE_URL) !== false; + } + + /** + * Validates the IP address or host name format. + */ + public static function isValidIpOrDomain(string $ipOrDomain) : bool { + return self::isValidIp($ipOrDomain) || self::isValidDomain($ipOrDomain); + } + + /** + * Validates the IP address format. + */ + public static function isValidIp(string $ip) : bool { + return filter_var($ip, FILTER_VALIDATE_IP) !== false; + } + + /** + * Validates the host name format. + */ + public static function isValidDomain(string $host) : bool { + return filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; + } + + /** + * Validates the port number. + */ + public static function isValidPort(int $port) : bool { + return $port > 0 && $port <= 65535; + } + + public static function isPngUrl(string $url) : bool { + return preg_match('/^https?:\/\/.+\.(png)$/i', $url) === 1; + } +} diff --git a/src/aiptu/smaccer/utils/promise/Promise.php b/src/aiptu/smaccer/utils/promise/Promise.php new file mode 100644 index 0000000..dbbf6af --- /dev/null +++ b/src/aiptu/smaccer/utils/promise/Promise.php @@ -0,0 +1,113 @@ + $shared + */ + public function __construct(private PromiseSharedData $shared) {} + + /** + * Provide callbacks to be called when the promise is resolved or rejected. + * + * @phpstan-param (Closure(TValue): void)|(Closure(): void) $onSuccess + * @phpstan-param (Closure(Throwable): void)|(Closure(): void) $onFailure + */ + public function onCompletion(Closure $onSuccess, Closure $onFailure) : void { + if ($this->shared->result !== null) { + $onSuccess($this->shared->result); + } elseif ($this->shared->error !== null) { + $onFailure($this->shared->error); + } else { + $this->shared->onSuccess[spl_object_id($onSuccess)] = $onSuccess; + $this->shared->onError[spl_object_id($onFailure)] = $onFailure; + } + } + + /** + * Returns true if the promise has been resolved or rejected. + */ + public function isResolved() : bool { + return $this->shared->result !== null || $this->shared->error !== null; + } + + /** + * Returns the result of the promise. + * + * @phpstan-return TValue|null + */ + public function getResult() : mixed { + return $this->shared->result; + } + + /** + * Returns the exception that was thrown when the promise was rejected. + */ + public function getError() : ?Throwable { + return $this->shared->error; + } + + /** + * Utility method to create a promise that resolves once all the given promises have resolved. + * + * @param Promise ...$promises All the promises to wait for. + * + * @phpstan-template UValue of mixed + * + * @phpstan-param Promise ...$promises + * + * Returns a {@see Promise} that resolves with an array of all the results of the given promises. The results in the + * array are in the same order in which the promises were given to the method. + * If any of the given promises is rejected, the returned promise is rejected with the same exception. + * + * @phpstan-return Promise> + */ + public static function all(self ...$promises) : self { + /** @phpstan-var PromiseResolver> $resolver */ + $resolver = new PromiseResolver(); + /** @phpstan-var non-empty-array $results */ + $results = []; + foreach ($promises as $key => $promise) { + $promise->onCompletion( + function (mixed $value) use ($promises, $key, &$results, $resolver) : void { + $results[$key] = $value; + if (count($results) === count($promises)) { + $resolver->resolveSilent($results); + } + }, + function (Throwable $error) use ($resolver) : void { + $resolver->rejectSilent($error); + } + ); + } + + return $resolver->getPromise(); + } +} diff --git a/src/aiptu/smaccer/utils/promise/PromiseResolver.php b/src/aiptu/smaccer/utils/promise/PromiseResolver.php new file mode 100644 index 0000000..d675829 --- /dev/null +++ b/src/aiptu/smaccer/utils/promise/PromiseResolver.php @@ -0,0 +1,119 @@ + */ + private PromiseSharedData $shared; + /** @phpstan-var Promise */ + private Promise $promise; + + public function __construct() { + $this->shared = new PromiseSharedData(); + $this->promise = new Promise($this->shared); + } + + /** + * Resolves the promise with the given value. + * + * @param mixed $value the value to resolve the promise with + * + * @phpstan-param TValue $value + * + * @throws LogicException when the promise has already been resolved or rejected + */ + public function resolve(mixed $value) : void { + if ($this->promise->isResolved()) { + throw new LogicException('Promise has already been ' . ($this->shared->result === null ? 'rejected' : 'resolved')); + } + + $this->shared->result = $value; + foreach ($this->shared->onSuccess as $closure) { + $closure($value); + } + + $this->shared->onSuccess = []; + $this->shared->onError = []; + } + + /** + * Resolves the promise with the given value. Unlike {@see PromiseResolver::resolve()}, this method does not throw + * an exception if the promise has already been resolved or rejected. + * + * @param mixed $value the value to resolve the promise with + * + * @phpstan-param TValue $value + * Returns true if the promise was successfully resolved, or false if it was already resolved or rejected. + */ + public function resolveSilent(mixed $value) : bool { + try { + $this->resolve($value); + } catch (LogicException) { + return false; + } + + return true; + } + + /** + * Rejects the promise with the given exception. + * + * @param Throwable $error the exception to reject the promise with + * + * @throws LogicException when the promise has already been resolved or rejected + */ + public function reject(Throwable $error) : void { + if ($this->promise->isResolved()) { + throw new LogicException('Promise has already been ' . ($this->shared->result === null ? 'rejected' : 'resolved')); + } + + $this->shared->error = $error; + foreach ($this->shared->onError as $closure) { + $closure($error); + } + + $this->shared->onSuccess = []; + $this->shared->onError = []; + } + + /** + * Rejects the promise with the given exception. Unlike {@see PromiseResolver::reject()}, this method does not throw + * an exception if the promise has already been resolved or rejected. + * + * @param Throwable $error The exception to reject the promise with. + * Returns true if the promise was successfully rejected, or false if it was already resolved or rejected. + */ + public function rejectSilent(Throwable $error) : bool { + try { + $this->reject($error); + } catch (LogicException) { + return false; + } + + return true; + } + + /** + * @phpstan-return Promise + */ + public function getPromise() : Promise { + return $this->promise; + } +} diff --git a/src/aiptu/smaccer/utils/promise/PromiseSharedData.php b/src/aiptu/smaccer/utils/promise/PromiseSharedData.php new file mode 100644 index 0000000..a686c5f --- /dev/null +++ b/src/aiptu/smaccer/utils/promise/PromiseSharedData.php @@ -0,0 +1,48 @@ + + */ + public array $onSuccess = []; + /** + * An array of {@see Closure}s to call when the promise is rejected. + * + * @phpstan-var array + */ + public array $onError = []; + + /** + * The result of the promise. + * + * @phpstan-var TValue|null + */ + public mixed $result = null; + /** The exception that was thrown when the promise was rejected. */ + public ?Throwable $error = null; +}