From e8d175c09448b5846584113d53e4c6c054e282d2 Mon Sep 17 00:00:00 2001 From: Futrime Date: Sun, 4 Feb 2024 18:13:33 +0800 Subject: [PATCH] docs: update tutorial --- docs/tutorials/create_your_first_plugin.md | 728 +----------------- docs/tutorials/create_your_first_plugin.zh.md | 300 ++++---- 2 files changed, 144 insertions(+), 884 deletions(-) diff --git a/docs/tutorials/create_your_first_plugin.md b/docs/tutorials/create_your_first_plugin.md index dcda8f37b7..ec2a2e4351 100644 --- a/docs/tutorials/create_your_first_plugin.md +++ b/docs/tutorials/create_your_first_plugin.md @@ -1,729 +1,3 @@ # Create Your First Plugin -## Introduction - -This tutorial aims to help you get started with plugin development in LeviLamina. It is by no means a comprehensive guide to all possibilities in LeviLamina, but rather an overall overview of foundational knowledge. First, make sure you understand C++, set up your workspace in the IDE, and then delve into the basic knowledge of most LeviLamina plugins. - -In this tutorial, we will create a simple plugin to implement the following features: - -- Players can use the `/suicide` command to commit suicide. -- Players receive a clock when they first log in to the server. -- When players use the clock, a confirmation window pops up asking if they want to commit suicide. If confirmed, they commit suicide. - -This tutorial covers the following key points: - -- Logging output -- Subscribing and unsubscribing events -- Registering commands -- Reading configuration files -- Database access -- Using forms -- Constructing Minecraft objects -- Invoking Minecraft functions - -!!! info - All source code for this tutorial can be found at [futrime/better-suicide](https://github.com/futrime/better-suicide). We recommend that you review the source code while following the tutorial. If you have already installed [lip](https://docs.lippkg.com), you can also directly run the following code to install the plugins implemented in this tutorial in the LeviLamina instance environment. - - ```shell - lip install github.com/futrime/better-suicide - ``` - -## Learn C++ - -These tutorials assume a basic knowledge of the C++ programming language. If you are just starting with C++ or need a review, here is a non-exhaustive list: - -- [C++ Developer Roadmap](https://roadmap.sh/cpp) -- [cppreference.com](https://en.cppreference.com/w/) -- [C++ Tutorial](https://www.w3schools.com/cpp/) -- [C++ Language Tutorial](https://cplusplus.com/doc/tutorial/) -- [hacking C++](https://hackingcpp.com/) -- [C++ Core Guidelines](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) - -## Set Up Your Workspace - -Before developing plugins (or learning C++), you need to set up a development environment. This includes but is not limited to the following: - -- [xmake](https://xmake.io) -- [Visual Studio Code](https://code.visualstudio.com) -- [Git](https://git-scm.com) -- [Visual Studio 2022](https://visualstudio.microsoft.com/) (When installing Visual Studio 2022, make sure to select C++ Desktop Development) - -!!! warning - If you are not using the latest versions of Visual Studio 2022, MSVC, and Windows SDK, you may encounter issues in the subsequent build, load, and run plugin steps. If you encounter problems like `xxx is not a member of std`, please consider this possibility. The tutorial was tested in an environment with Visual Studio Community 2022 version 17.8.1, MSVC v143 - VS 2022 C++ x64/x86 build tools (v14.38-17.8), and Windows 11 SDK (10.0.22000.0). - -!!! tip - Due to the size of the LeviLamina project, if you are using Visual Studio Code, its built-in Intellisense system may struggle. We recommend installing the clangd plugin and using clangd for code checking, etc. After installing clangd and the corresponding plugin, you need to run the following command to generate `compile_commands.json` and then restart VSCode to make clangd effective. - - ```shell - xmake project -k compile_commands - ``` - -Next, you need to install LeviLamina somewhere. This tutorial is targeted at LeviLamina 0.3.0, and for other versions, some modifications may be required. - -## Create a Plugin Repository - -Visit [levilamina-plugin-template](https://github.com/LiteLDev/levilamina-plugin-template), click on `Use this template` to initialize your plugin repository with this template. - -![Create from template](img/levilamina-plugin-template.png) - -Clone the plugin repository to your local machine using Git, then open it with VSCode. You need to modify some files to fill in your plugin information. - -First, you need to modify the LeviLamina version number and plugin name information in `xmake.lua`. Modifying the LeviLamina version number is to specify the LeviLamina version your plugin is compatible with, and you can find LeviLamina version numbers [here](https://github.com/LiteLDev/LeviLamina/releases). Modifying the plugin name is to specify the name of your plugin, which will be displayed in LeviLamina. The name allows English uppercase and lowercase letters, numbers, and hyphens, and should not include spaces or other special characters. We recommend using either `example-plugin` or `ExamplePlugin`. - -```lua --- ... - -add_requires("levilamina") -- or add_requires("levilamina x.x.x") to specify target LeviLamina version - -target("plugin") -- Change this to your plugin name. - --- ... -``` - -Next, you need to modify the copyright information in the `LICENSE` file. You can choose an open-source license suitable for your plugin [here](https://choosealicense.com/licenses/). Rest assured, your plugin does not need to be open source, as the plugin template uses the CC0 license, and you are free to modify or remove the `LICENSE` file. However, we recommend using an open-source license, as it makes it easier for others to use and contribute to your plugin. - -Then, you need to modify the content in the `README.md` file. This file will be displayed on your plugin repository's homepage, and you can introduce your plugin's features, usage, configuration files, commands, and more. - -## Build Your Plugin - -Before we begin, let's try building an empty plugin. - -First, update the repository: - -```shell -xmake repo -u -``` - -Configure the build: - -```shell -xmake f -m debug -``` - -!!! tip - If you want to build in other modes, you can also use `-m release` or `-m releasedbg`. These two modes will enable the `fastest` optimization level. Among them, `-m release` will disable debugging information, while `-m releasedbg` will enable debugging information, just like `-m debug`. For their specific differences, please refer to [Custom Rules - xmake](https://xmake.io/#/en/manual/custom_rule). - -Then build: - -```shell -xmake -``` - -!!! failure - Build failed? Try upgrading Visual Studio 2022, MSVC, and Windows SDK. Remember to upgrade to the latest versions. - -## Understand the Plugin Structure - -Open the `src/` directory, and you will see the following file structure: - -```text -src/ -├── DllMain.cpp -├── Plugin.cpp -└── Plugin.h -``` - -`DllMain.cpp` is the entry file of the plugin. Here, the plugin implements the C-style interface exports and the most basic code related to the loading, enabling, and disabling processes of the plugin. This part of the code does not need modification. - -`Plugin.h` and `Plugin.cpp` are the main code of the plugin. Here, you can implement the functionality of the plugin. In this tutorial, we will implement the functionality of the suicide plugin here. - -## Register the `/suicide` Command - -In BDS, commands cannot be registered right from the beginning; instead, they need to be registered after specific program execution. Therefore, you cannot register commands when the plugin is loaded, but only when the plugin is enabled. Generally, it is advisable to unregister commands when the plugin is disabled to prevent undefined behavior. - -!!! warning - Plugins call their constructor when loaded. However, please do not place any game-related operations such as event subscription, command registration, etc., in the constructor, as these operations need to be performed after the game is fully loaded. If you perform these operations in the constructor, your plugin is likely to crash during loading. - -!!! tip - In general, the constructor of a plugin should only perform non-game-related initialization operations, such as initializing the logging system, configuration files, databases, etc. - -```cpp -// ... - -#include - -#include -#include -#include -#include -#include -#include -#include - -bool Plugin::enable() { - auto& logger = mSelf.getLogger(); - - // ... - - // Register commands. - auto commandRegistry = ll::service::getCommandRegistry(); - if (!commandRegistry) { - throw std::runtime_error("failed to get command registry"); - } - - auto command = - DynamicCommand::createCommand(commandRegistry, "suicide", "Commits suicide.", CommandPermissionLevel::Any); - command->addOverload(); - command->setCallback( - [&logger](DynamicCommand const&, CommandOrigin const& origin, CommandOutput& output, std::unordered_map&) { - auto* entity = origin.getEntity(); - if (entity == nullptr || !entity->isType(ActorType::Player)) { - output.error("Only players can commit suicide"); - return; - } - - auto* player = static_cast(entity); - player->kill(); - - logger.info("{} killed themselves", player->getRealName()); - } - ); - DynamicCommand::setup(commandRegistry, std::move(command)); - - // ... - - return true; -} - -bool Plugin::disable() { - - // ... - - // Unregister commands. - auto commandRegistry = ll::service::getCommandRegistry(); - if (!commandRegistry) { - throw std::runtime_error("failed to get command registry"); - } - - commandRegistry->unregisterCommand("suicide"); - - // ... - - return true; -} -``` - -Let's break down this code. The following statement retrieves a logger from the LeviLamina plugin instance stored in the `Plugin` class. - -```cpp -auto& logger = mSelf.getLogger(); -``` - -Next, we obtain the command registry. The command registry only takes effect after a specific event, so its type is `optional_ref`. We need to determine whether the obtained command registry is valid. - -```cpp -auto commandRegistry = ll::service::getCommandRegistry(); -if (!commandRegistry) { - throw std::runtime_error("failed to get command registry"); -} -``` - -The dynamic command system supports registering commands directly using the `DynamicCommand::createCommand()` function. - -```cpp -auto command = DynamicCommand::createCommand( - event.registry(), - "suicide", - "Commits suicide.", - CommandPermissionLevel::Any); -``` - -Here, the second parameter is the command itself, i.e., the characters entered in the console or chat. Although various special characters have not been tested for their effectiveness, we still recommend using only lowercase English letters. The third parameter is the command description, which is displayed above the chat when part of the command is entered, showing candidate commands and their descriptions in semi-transparent gray. The fourth parameter is the permission level of the command, defined as follows. If we want ordinary players in survival mode to be able to execute it, we should choose `Any`. `GameDirectors` corresponds to players with at least creative mode, `Admin` corresponds to players with at least OP permissions, and `Host` corresponds to console permissions. - -```cpp -enum class CommandPermissionLevel : schar { - Any = 0x0, - GameDirectors = 0x1, - Admin = 0x2, - Host = 0x3, - Owner = 0x4, - Internal = 0x5, -}; -``` - -Then, we need to add an overload for the command and set the corresponding callback. - -```cpp -command->addOverload(); -command->setCallback([&logger](DynamicCommand const&, - CommandOrigin const& origin, - CommandOutput& output, - std::unordered_map&) { - // ... -}); -``` - -!!! note - Command overloads mean different patterns for a command, for example, `dyncmd ` is one overload, and `dyncmd list` is another overload. Here is an example from LeviLamina's test case: - - ```cpp - auto command = - DynamicCommand::createCommand(registry, "testcmd", "dynamic command", CommandPermissionLevel::GameDirectors); - - auto& optionsAdd = command->setEnum("TestOperation1", {"add", "remove"}); - auto& optionsList = command->setEnum("TestOperation2", {"list"}); - - command->mandatory("testEnum", ParamType::Enum, optionsAdd, CommandParameterOption::EnumAutocompleteExpansion); - command->mandatory("testEnum", ParamType::Enum, optionsList, CommandParameterOption::EnumAutocompleteExpansion); - command->mandatory("testString", ParamType::String); - - command->addOverload({optionsAdd, "testString"}); // dyncmd - command->addOverload({"TestOperation2"}); // dyncmd - ``` - -In the callback function, we first attempt to get the command's execution source. Here, we need to make a decision because the console, command blocks, and various entities can execute commands, but the suicide plugin should only respond to player requests. If the command is executed by an incorrect source, an error message should be displayed. - -```cpp -auto* entity - - = origin.getEntity(); -if (entity == nullptr || !entity->isType(ActorType::Player)) { - output.error("Only players can commit suicide"); - return; -} -``` - -Once we confirm that the source of execution is a player, we can convert the entity pointer to a player pointer and kill them. - -```cpp -auto* player = static_cast(entity); -player->kill(); - -logger.info("{} killed themselves", player->getRealName()); -``` - -!!! warning - Since BDS lacks RTTI information, `dynamic_cast()` cannot be used. - -!!! tip - You may notice another function `player->getName()`, but we did not use it. This is because the player's name can be modified through plugins or other means, while the result of `player->getRealName()` is (generally) fixed. - -At this point, the command object is configured, and we call `DynamicCommand::setup()` to load the command object into the game. Note that `std::move()` is needed here because it takes a right-value reference. - -```cpp -DynamicCommand::setup(event.registry(), std::move(command)); -``` - -At the end of the `enable()` function, return `true`, indicating that the plugin was enabled successfully. If `false` is returned in the `enable()` function, LeviLamina will consider the plugin to have failed to enable and display an error message on the console. - -In the `disable()` function, we need to unregister the command. - -```cpp -// Unregister commands. -auto commandRegistry = ll::service::getCommandRegistry(); -if (!commandRegistry) { - throw std::runtime_error("failed to get command registry"); -} - -commandRegistry->unregisterCommand("suicide"); -``` - -## Reading Configuration File - -The second functionality of our plugin is to give players a clock when they first enter the server. The third functionality is to display a confirmation prompt for suicide when using the clock, allowing players to confirm before proceeding. However, there is a small issue with these two functionalities: server administrators may have installed other plugins that implement similar features and may not want to use these specific features of the suicide plugin. We want to provide a way for administrators to toggle these two functionalities. - -We are pleased to announce that LeviLamina has implemented reflection for configuration files and configuration information structures in C++. This means that we can define a structure in C++ and then define an instance of this structure in the configuration file. LeviLamina will automatically read the contents of the configuration file into the structure instance. This way, we can directly use the structure instance in C++ without the need to manually parse the configuration file. - -First, let's create a separate `Config.h` file to define a structure named `Config` for storing configuration information. - -```cpp -struct Config { - int version = 1; - bool doGiveClockOnFirstJoin = true; - bool enableClockMenu = true; -}; -``` - -In the `Plugin` class, let's add a member variable to store the configuration information. - -```cpp -// ... - -#include "Config.h" - -class Plugin { - -// ... - -private: - Config mConfig; -}; -``` - -Next, in the constructor of the `Plugin` class, let's read the configuration file and save the configuration information to the member variable. - -```cpp -#include - -// ... - -Plugin::Plugin(ll::plugin::Plugin& self) : mSelf(self) { - auto& logger = mSelf.getLogger(); - - // Load or initialize configurations. - const auto& configFilePath = self.getConfigDir() / "config.json"; - if (!ll::config::loadConfig(mConfig, configFilePath)) { - logger.warn("Cannot load configurations from {}", configFilePath); - logger.info("Saving default configurations"); - - if (!ll::config::saveConfig(mConfig, configFilePath)) { - logger.error("Cannot save default configurations to {}", configFilePath); - } - } -} -``` - -In this code, we first obtain the path to the plugin's configuration file and then call the `ll::config::loadConfig()` function to read the configuration information from the file into the structure instance. If the reading fails, we output a warning message to the console and save the default configuration information to the file. - -!!! note - Since configuration file reading occurs in the constructor, we can ensure that the configuration file has been successfully read in subsequent operations. - -## Persistently Storing Player Join Information in the Database - -The second functionality of our plugin is to give players a clock when they first enter the server. However, if we store player join information in memory, it will be lost when the server restarts. Therefore, we need to persistently store player join information in the database. LeviLamina provides an encapsulation for key-value databases, allowing us to use databases directly in C++. - -Firstly, let's add a member variable to the `Plugin` class to store the database instance. - -```cpp -// ... - -#include - -class Plugin { - -// ... - -private: - std::unique_ptr mPlayerDb; -}; -``` - -!!! note - Why use `std::unique_ptr` instead of `ll::KeyValueDB` directly? This is because `ll::KeyValueDB` prohibits copying and only allows moving. Therefore, we need to use `std::unique_ptr` to manage the lifetime of the `ll::KeyValueDB` instance. - -!!! warning - Do not use regular pointers to store an instance of `ll::KeyValueDB`, as it can complicate lifecycle management, leading to memory leaks and other issues. Remember: you're writing C++, not C. - -Next, in the constructor of the `Plugin` class, let's initialize the database instance. - -```cpp -// ... - -#include - -#include - -// ... - -Plugin::Plugin(ll::plugin::Plugin& self) : mSelf(self) { - auto& logger = mSelf.getLogger(); - - // ... - - // Initialize database. - const auto& playerDbPath = self.getDataDir() / "players"; - mPlayerDb = std::make_unique(playerDbPath); -} -``` - -In this code, we first obtain the path to the plugin's database, and then we call `std::make_unique()` to create a database instance. If the database path does not exist, the `std::make_unique()` function will automatically create the necessary directories. - -!!! note - Since database initialization occurs in the constructor, we can ensure that the database has been successfully initialized in subsequent operations. - -## Giving a Clock to Players on Their First Join - -The second functionality of our plugin is to give players a clock when they first enter the server. We need to check if the player is joining for the first time and, if so, give them a clock. - -In BDS, when a player joins, the `PlayerJoinEvent` is triggered. In LeviLamina, we can subscribe to this event, and when it is triggered, the plugin can implement the logic for when a player joins. - -In the `Plugin.h` file, let's add an event listener pointer: - -```cpp -// ... - -class Plugin { - -// ... - -private: - // ... - - ll::event::ListenerPtr mPlayerJoinEventListener; -}; -``` - -In the `Plugin.cpp` file, let's register this event listener in the `enable()` function and unregister it in the `disable()` function. - -```cpp -bool Plugin::enable() { - // ... - - auto& eventBus = ll::event::EventBus::getInstance(); - - mPlayerJoinEventListener = eventBus.emplaceListener( - [doGiveClockOnFirstJoin = mConfig.doGiveClockOnFirstJoin, - &logger, - &playerDb = mPlayerDb](ll::event::player::PlayerJoinEvent& event) { - if (doGiveClockOnFirstJoin) { - auto& player = event.self(); - auto& uuid = player.getUuid(); - - // Check if the player has joined before. - if (!playerDb->get(uuid.asString())) { - - ItemStack itemStack("clock", 1); - player.add(itemStack); - - // Must refresh inventory to see the clock. - player.refreshInventory(); - - // Mark the player as joined. - if (!playerDb->set(uuid.asString(), "true")) { - logger.error("Cannot mark {} as joined in database", player.getRealName()); - } - - logger.info("First join of {}! Giving them a clock", player.getRealName()); - } - } - } - ); - - // ... -} - -bool Plugin::disable() { - // ... - - auto& eventBus = ll::event::EventBus::getInstance(); - - eventBus.removeListener(mPlayerJoinEventListener); - - // ... -} -``` - -Let's break down this code. In the callback lambda function, we capture the configuration `doGiveClockOnFirstJoin`, as well as the logger and database instance. We then check if `doGiveClockOnFirstJoin` is `true`, and if it is, we proceed with the logic. - -```cpp -[doGiveClockOnFirstJoin = mConfig.doGiveClockOnFirstJoin, - &logger, - &playerDb = mPlayerDb](ll::event:: - -player::PlayerJoinEvent& event) { - if (doGiveClockOnFirstJoin) { - // ... - } -} -``` - -Next, we obtain the player instance and the player's UUID from the event. - -```cpp -auto& player = event.self(); -auto& uuid = player.getUuid(); -``` - -!!! note - The type of UUID obtained here is `mce::UUID` rather than `std::string`. We recommend converting UUID to `std::string` only when necessary, as the implementation of `mce::UUID` is more efficient. - -!!! danger - Please do not use XUID as the unique identifier for players. While in the LiteLoaderBDS era, many plugins used XUID as the unique identifier for players, this is incorrect. XUID is the identifier for Xbox Live, not for players. If the server is not in online mode or has NPCs (Non-Player Characters), the behavior of XUID will be unpredictable. Therefore, we strongly recommend using UUID as the unique identifier for players. - -Then, we use the player's UUID as the key to check if the player has joined before in the database. - -```cpp -// Check if the player has joined before. -if (!playerDb->get(uuid.asString())) { - // ... -} -``` - -Next, we create a stack of clock items and add this item stack to the player's inventory. - -```cpp -ItemStack itemStack("clock", 1); -player.add(itemStack); -``` - -!!! note - Here, we use the `ItemStack` class instead of the `Item` class. The `ItemStack` class is a wrapper for the `Item` class, and it includes information such as the quantity, enchantments, durability, etc, while `Item` class just represents the item kind. Therefore, you should the `ItemStack` class instead of the `Item` class. - -Then, we need to refresh the player's inventory so that the player can see the clock. - -```cpp -player.refreshInventory(); -``` - -Finally, we use the player's UUID as the key to mark the player as joined in the database. - -```cpp -// Mark the player as joined. -if (!playerDb->set(uuid.asString(), "true")) { - logger.error("Cannot mark {} as joined in database", player.getRealName()); -} -``` - -In the `disable()` function, we need to remove the event listener from the event bus to unsubscribe from the event. - -```cpp -eventBus.removeListener(mPlayerJoinEventListener); -``` - -## Displaying a Confirmation Prompt for Suicide when Using the Clock - -The third functionality of our plugin is to display a confirmation prompt for suicide when using the clock. After the player confirms, they can proceed with suicide. We need to subscribe to the event of a player using an item, and when a clock is used, display a confirmation prompt. - -In the `Plugin.h` file, let's add an event listener pointer: - -```cpp -// ... - -class Plugin { - -// ... - -private: - // ... - - ll::event::ListenerPtr mPlayerUseItemEventListener; -}; -``` - -In the `Plugin.cpp` file, let's register this event listener in the `enable()` function and unregister it in the `disable()` function. - -```cpp -bool Plugin::enable() { - mPlayerUseItemEventListener = eventBus.emplaceListener( - [enableClockMenu = mConfig.enableClockMenu, &logger](ll::event::PlayerUseItemEvent& event) { - if (enableClockMenu) { - auto& player = event.self(); - auto& itemStack = event.item(); - - logger.info("{} used {}", player.getRealName(), itemStack.getRawNameId()); - - if (itemStack.getRawNameId() == "clock") { - ll::form::ModalForm form( - "Warning", - "Are you sure you want to kill yourself?", - "Yes", - "No", - [&logger](Player& player, bool yes) { - if (yes) { - player.kill(); - - logger.info("{} killed themselves", player.getRealName()); - } - } - ); - - form.sendTo(player); - - logger.info("{} opened better-suicide menu", player.getRealName()); - } - } - } - ); -} - -bool Plugin::disable() { - // ... - - eventBus.removeListener(mPlayerUseItemEventListener); - - // ... -} -``` - -Let's break down this code. In the callback lambda function, we capture the configuration `enableClockMenu` and logger. We then check if `enableClockMenu` is `true`, and if it is, we proceed with the logic. - -```cpp -mPlayerUseItemEventListener = eventBus.emplaceListener( - [enableClockMenu = mConfig.enableClockMenu, &logger](ll::event::PlayerUseItemEvent& event) { - if (enableClockMenu) { - // ... - } - } -); -``` - -In the logic, we first obtain the player instance and the item stack being used from the event. - -```cpp -auto& player = event.self(); -auto& itemStack = event.item(); -``` - -Then, we log the player's name and the item being used. - -```cpp -logger.info("{} used {}", player.getRealName(), itemStack.getRawNameId()); -``` - -Next, we check if the item being used is a clock and, if it is, display a confirmation prompt using a modal form. - -```cpp -if (itemStack.getRawNameId() == "clock") { - ll::form::ModalForm form( - "Warning", - "Are you sure you want to kill yourself?", - "No", - "Yes", - [&logger](Player& player, bool isCanceled) { - if (!isCanceled) { - player.kill(); - - logger.info("{} killed themselves", player.getRealName()); - } - } - ); - - form.sendTo(player); - - logger.info("{} opened better-suicide menu", player.getRealName()); -} -``` - -In this form, the first parameter is the title of the form, the second parameter is the prompt content, the third parameter is the content of the button in the bottom-left corner, and the fourth parameter is the content of the button in the bottom-right corner. The callback function receives two parameters: the player to whom the form is sent and a boolean indicating whether the form was canceled. The callback function is called when the player interacts with the form. - -Finally, we send the form to the player. - -```cpp -form.sendTo(player); -``` - -## Run Your Plugin - -If your plugin is built successfully, you should see a directory in the `bin/` folder named after your plugin. Copy this directory to the `plugins/` directory inside the LeviLamina directory (create one if it doesn't exist), resulting in the following file structure: - -```text -/path/to/levilamina/plugins/your-plugin-name -├── your-plugin-name.dll -└── manifest.json -``` - -Then run the LeviLamina server (`bedrock_server_mod.exe`). - -## What's Next? - -You can [publicly release your plugin](./publish_your_first_plugin.md) for more people to use. - -## Further Exercises - -We can add more features to this plugin to practice additional knowledge of LeviLamina plugin development. Here are some possible exercises: - -- Set a cooldown time for player suicides. -- Allow players to keep all items when committing suicide. -- Preserve experience when a player commits suicide. -- Enable players to respawn in the same location after suicide. -- Keep track of player suicide counts and display a leaderboard in the sidebar. -- Use advanced forms to let players choose the method of suicide. -- Display a custom death message when a player commits suicide. - -Here are some reference materials you might need: - -- [Event Guide](../guides/event_guide.md) -- [Export Interface Guide](../guides/export_interface_guide.md) -- [Form Guide](../guides/form_guide.md) -- [Hook Guide](../guides/hook_guide.md) -- [Find Function Guide](../guides/find_function_guide.md) +_Not translated yet_ diff --git a/docs/tutorials/create_your_first_plugin.zh.md b/docs/tutorials/create_your_first_plugin.zh.md index 8d43a5026f..b39290179d 100644 --- a/docs/tutorials/create_your_first_plugin.zh.md +++ b/docs/tutorials/create_your_first_plugin.zh.md @@ -58,32 +58,59 @@ xmake project -k compile_commands ``` -然后,你需要在某处安装LeviLamina。本教程针对的是LeviLamina 0.3.0,对于其它版本,可能需要做一些修改。 +然后,你需要在某处安装LeviLamina。本教程针对的是LeviLamina 0.6.3,对于其它版本,可能需要做一些修改。 ## 创建插件仓库 -访问[levilamina-plugin-template](https://github.com/LiteLDev/levilamina-plugin-template),点击`Use this template`以使用这个模板初始化你的插件仓库。 +访问[levilamina-plugin-template](https://github.com/futrime/levilamina-plugin-template),点击`Use this template`以使用这个模板初始化你的插件仓库。 ![Create from template](img/levilamina-plugin-template.png) 将插件仓库使用Git克隆到本地,然后使用VSCode打开。你需要修改其中的一些文件,填写你的插件信息。 -首先,你需要修改`xmake.lua`中LeviLamina版本号和插件名字信息。修改LeviLamina版本号是为了指定你的插件适用的LeviLamina版本,你可以在[这里](https://github.com/LiteLDev/LeviLamina/releases)找到LeviLamina的版本号。修改插件名字是为了指定你的插件的名字,这个名字将会在LeviLamina中显示。名字允许英文大小写、数字、中划线,不允许包括空格和其他特殊字符,建议采用`example-plugin`或`ExamplePlugin`这两种形式。 +首先,你需要修改`xmake.lua`中插件名字信息。修改插件名字是为了指定你的插件的名字,这个名字将会在LeviLamina中显示。名字允许英文大小写、数字、中划线,不允许包括空格和其他特殊字符,建议采用`example-plugin`或`ExamplePlugin`这两种形式。在这里,我们的插件命名为`better-suicide`。 ```lua --- ... - -add_requires("levilamina") -- or add_requires("levilamina x.x.x") to specify target LeviLamina version - -target("plugin") -- Change this to your plugin name. - --- ... +target("better-suicide") -- Change this to your plugin name. +``` + +接着,修改`tooth.json`的内容。`tooth.json`为lip安装插件包提供了相关信息,正确配置后,你的插件将会被[lip Index](https://lippkg.com)收录,并能被全世界的用户下载安装。将`tooth`字段的值改为这个插件的GitHub仓库地址,填写`info`中各个信息字段,然后根据仓库release地址填写`asset_url`字段,修改依赖的LeviLamina版本,并根据在`xmake.lua`中填写的插件名修改`place`的`src`和`dest`。对于本文的插件,以下是一个可行的参考: + +```json +{ + "format_version": 2, + "tooth": "github.com/futrime/better-suicide", + "version": "0.5.0", + "info": { + "name": "better-suicide", + "description": "Allow players to suicide in Minecraft.", + "author": "futrime", + "tags": [ + "levilamina", + "plugin" + ] + }, + "asset_url": "https://github.com/futrime/better-suicide/releases/download/v0.5.0/better-suicide-windows-x64.zip", + "prerequisites": { + "github.com/LiteLDev/LeviLamina": "0.6.x" + }, + "files": { + "place": [ + { + "src": "better-suicide/*", + "dest": "plugins/better-suicide/" + } + ] + } +} ``` 然后,你需要修改`LICENSE`文件中的版权信息。你可以在[这里](https://choosealicense.com/licenses/)选择一个适合你的插件的开源协议。请放心,你的插件不需要开源,因为插件模板使用了CC0协议,你可以随意修改或删除`LICENSE`文件。但是,我们建议你使用一个开源协议,因为这样可以让其他人更容易地使用你的插件和帮助你改进你的插件。 接下来,你需要修改`README.md`文件中的内容。这个文件将会在你的插件仓库主页显示,你可以在这里介绍你的插件的功能、使用方法、配置文件、指令等等。 +最后,你需要修改目录名和命名空间名。将`rename_this`目录改成你喜欢的名字,并将`Entry.cpp`和`Entry.h`中命名空间`rename_this`改成相同的名字。按照C++常见惯例,目录名和命名空间名应当使用小写字母和下划线,且应当保持一致。这里,我们统一改成`better_suicide`。 + ## 构建你的插件 在一切开始之前,先让我们尝试构建一下空的插件。 @@ -122,21 +149,38 @@ xmake !!! failure 构建失败了?尝试升级一下Visual Studio 2022、MSVC和Windows SDK吧。记住,一定要升级到最新版本。 +## 补充`#include` -## 理解插件结构 +在`Entry.cpp`中补充`#include`,最终效果看起来是这样的: -打开`src/`目录,你会看到以下的文件结构: - -```text -src/ -├── DllMain.cpp -├── Plugin.cpp -└── Plugin.h -``` +```cpp +#include "Entry.h" -`DllMain.cpp`是插件的入口文件。在这里,插件实现了C语言风格接口的导出,以及插件在加载、启用、禁用的过程中涉及的最基本的代码。这部分代码不需要你修改。 +#include "Config.h" -`Plugin.h`和`Plugin.cpp`是插件的主要代码。在这里,你可以实现插件的功能。在这个教程中,我们将会在这里实现自杀插件的功能。 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +``` ## 注册指令`/suicide` @@ -149,20 +193,7 @@ src/ 一般来说,插件的构造函数中只需要进行一些与游戏无关初始化操作即可,例如初始化日志系统、初始化配置文件、初始化数据库等等。 ```cpp -// ... - -#include - -#include -#include -#include -#include -#include -#include -#include - -bool Plugin::enable() { - auto& logger = mSelf.getLogger(); +auto enable(ll::plugin::NativePlugin& /*self*/) -> bool { // ... @@ -183,7 +214,7 @@ bool Plugin::enable() { return; } - auto* player = static_cast(entity); + auto* player = static_cast(entity); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) player->kill(); logger.info("{} killed themselves", player->getRealName()); @@ -196,7 +227,7 @@ bool Plugin::enable() { return true; } -bool Plugin::disable() { +auto disable(ll::plugin::NativePlugin& /*self*/) -> bool { // ... @@ -214,13 +245,7 @@ bool Plugin::disable() { } ``` -让我们将这些代码拆开来看。下列语句从`Plugin`类中保存的LeviLamina插件实例中获取一个logger,用于将文本内容输出到控制台上。 - -```cpp -auto& logger = mSelf.getLogger(); -``` - -接下来,我们获取指令注册表。指令注册表只有在特定时机之后才会生效,因此其类型为`optional_ref`。我们需要判定获取到的指令注册表是否有效。 +让我们将这些代码拆开来看。下列语句获取指令注册表。指令注册表只有在特定时机之后才会生效,因此其类型为`optional_ref`。我们需要判定获取到的指令注册表是否有效。 ```cpp auto commandRegistry = ll::service::getCommandRegistry(); @@ -232,11 +257,8 @@ if (!commandRegistry) { 动态指令系统支持使用`DynamicCommand::createCommand()`函数直接注册指令。 ```cpp -auto command = DynamicCommand::createCommand( - event.registry(), - "suicide", - "Commits suicide.", - CommandPermissionLevel::Any); +auto command = + DynamicCommand::createCommand(commandRegistry, "suicide", "Commits suicide.", CommandPermissionLevel::Any); ``` 其中,第二个参数是指令本身,即在控制台或聊天栏内输入的字符。虽然尚未测试各种特殊字符能否生效,但我们仍然建议只包含小写英文字母。第三个参数是指令简介,在聊天栏输入指令的一部分时,会在上方以半透明灰色的形式显示候选指令及其简介。第四个参数是指令的权限等级,其定义如下。其中,如果我们希望生存模式下的普通玩家也能执行,应当选择`Any`。而`GameDirectors`对应至少为创造模式的玩家的权限,`Admin`对应至少为OP的权限,`Host`对应控制台的权限。 @@ -268,18 +290,18 @@ command->setCallback([&logger](DynamicCommand const&, 指令的重载意味着指令的一个模式,例如`dyncmd ` 是一个重载,而`dyncmd list`是另一个重载。下面是一个例子,来自LeviLamina的测试样例: ```cpp - auto command = - DynamicCommand::createCommand(registry, "testcmd", "dynamic command", CommandPermissionLevel::GameDirectors); + auto command = + DynamicCommand::createCommand(registry, "testcmd", "dynamic command", CommandPermissionLevel::GameDirectors); - auto& optionsAdd = command->setEnum("TestOperation1", {"add", "remove"}); - auto& optionsList = command->setEnum("TestOperation2", {"list"}); + auto& optionsAdd = command->setEnum("TestOperation1", {"add", "remove"}); + auto& optionsList = command->setEnum("TestOperation2", {"list"}); - command->mandatory("testEnum", ParamType::Enum, optionsAdd, CommandParameterOption::EnumAutocompleteExpansion); - command->mandatory("testEnum", ParamType::Enum, optionsList, CommandParameterOption::EnumAutocompleteExpansion); - command->mandatory("testString", ParamType::String); + command->mandatory("testEnum", ParamType::Enum, optionsAdd, CommandParameterOption::EnumAutocompleteExpansion); + command->mandatory("testEnum", ParamType::Enum, optionsList, CommandParameterOption::EnumAutocompleteExpansion); + command->mandatory("testString", ParamType::String); - command->addOverload({optionsAdd, "testString"}); // dyncmd - command->addOverload({"TestOperation2"}); // dyncmd + command->addOverload({optionsAdd, "testString"}); // dyncmd + command->addOverload({"TestOperation2"}); // dyncmd ``` 在回调函数中,我们首先尝试获取指令的执行来源。在这里,我们需要进行一个判定,因为控制台、命令方块乃至各种实体都能够执行指令,但自杀插件应当只响应玩家的请求。如果错误的执行来源执行了自杀指令,那么应当提示一个错误信息。 @@ -343,42 +365,38 @@ struct Config { }; ``` -我们在`Plugin`类中增加一个成员变量,用于保存配置文件中的配置信息。 +我们在匿名命名空间中增加一个成员变量,用于保存配置文件中的配置信息。 ```cpp -// ... - -#include "Config.h" - -class Plugin { +namespace { // ... -private: - Config mConfig; -}; +Config config; + +} ``` -然后,我们在`Plugin`类的构造函数中,读取配置文件并将配置信息保存到成员变量中。 +然后,我们读取配置文件并将配置信息保存到成员变量中。 ```cpp -#include - -// ... - -Plugin::Plugin(ll::plugin::Plugin& self) : mSelf(self) { - auto& logger = mSelf.getLogger(); +auto load(ll::plugin::NativePlugin& self) -> bool { + + // ... // Load or initialize configurations. const auto& configFilePath = self.getConfigDir() / "config.json"; - if (!ll::config::loadConfig(mConfig, configFilePath)) { + if (!ll::config::loadConfig(config, configFilePath)) { logger.warn("Cannot load configurations from {}", configFilePath); logger.info("Saving default configurations"); - if (!ll::config::saveConfig(mConfig, configFilePath)) { + if (!ll::config::saveConfig(config, configFilePath)) { logger.error("Cannot save default configurations to {}", configFilePath); } } + + // ... + } ``` @@ -391,20 +409,10 @@ Plugin::Plugin(ll::plugin::Plugin& self) : mSelf(self) { 我们的插件的第二个功能是玩家首次进入服务器时,给予一个钟。但是,如果我们将进服信息保存在内存中,那么当服务器重启后,玩家的进服信息就会丢失。因此,我们需要将玩家的进服信息持久化保存在数据库中。LeviLamina提供了KV数据库的封装,可以让我们在C++中直接使用数据库。 -首先,我们在`Plugin`类中增加一个成员变量,用于保存数据库实例。 +首先,我们在匿名命名空间中增加一个成员变量,用于保存数据库实例。 ```cpp -// ... - -#include - -class Plugin { - -// ... - -private: - std::unique_ptr mPlayerDb; -}; +std::unique_ptr playerDb; ``` !!! note @@ -413,29 +421,22 @@ private: !!! warning 请不要使用普通的指针来保存`ll::KeyValueDB`的实例,因为这样很容易使得生命周期管理变得复杂,从而导致内存泄漏和其他问题。请记住:你在写C++,而不是C。 -然后,我们在`Plugin`类的构造函数中,初始化数据库实例。 +然后,我们在`load`函数中,初始化数据库实例。 ```cpp -// ... - -#include - -#include - -// ... - -Plugin::Plugin(ll::plugin::Plugin& self) : mSelf(self) { - auto& logger = mSelf.getLogger(); - +auto load(ll::plugin::NativePlugin& self) -> bool { + // ... - // Initialize database. + // Initialize databases; const auto& playerDbPath = self.getDataDir() / "players"; - mPlayerDb = std::make_unique(playerDbPath); + playerDb = std::make_unique(playerDbPath); + + // ... } ``` -在这段代码中,我们首先获取插件的数据库路径,然后调用`std::make_unique()`函数,创建一个数据库实例。如果数据库路径不存在,那么`std::make_unique()`函数会自动创建数据库路径。 +在这段代码中,我们首先获取插件的数据库路径,然后调用`std::make_unique()`函数,创建一个数据库实例。如果数据库路径不存在,那么`std::make_unique()`函数会自动创建数据库路径。 !!! note 由于数据库初始化是在构造函数内进行的,所以在后续操作中可以保证数据库已经初始化成功了。 @@ -446,41 +447,29 @@ Plugin::Plugin(ll::plugin::Plugin& self) : mSelf(self) { 在BDS中,玩家进服时,会触发事件`PlayerJoinEvent`。在LeviLamina中,我们可以订阅这个事件,当这个事件被触发时,插件可以在这里实现玩家进服时的逻辑。 -在`Plugin.h`中,我们增加一个事件监听器指针: +在匿名命名空间中,我们增加一个事件监听器指针: ```cpp -// ... - -#include - -// ... - -class Plugin { - -// ... - -private: - // ... - - ll::event::ListenerPtr mPlayerJoinEventListener; -}; +ll::event::ListenerPtr playerJoinEventListener; ``` -在`Plugin.cpp`中,我们在`enable()`函数中注册这个事件监听器,并在`disable()`函数中取消注册。 +在`enable()`函数中注册这个事件监听器,并在`disable()`函数中取消注册。 ```cpp -bool Plugin::enable() { +auto enable(ll::plugin::NativePlugin& /*self*/) -> bool { + // ... auto& eventBus = ll::event::EventBus::getInstance(); - mPlayerJoinEventListener = eventBus.emplaceListener( - [doGiveClockOnFirstJoin = mConfig.doGiveClockOnFirstJoin, + playerJoinEventListener = eventBus.emplaceListener( + [doGiveClockOnFirstJoin = config.doGiveClockOnFirstJoin, &logger, - &playerDb = mPlayerDb](ll::event::player::PlayerJoinEvent& event) { + &playerDb = playerDb](ll::event::player::PlayerJoinEvent& event) { if (doGiveClockOnFirstJoin) { auto& player = event.self(); - auto& uuid = player.getUuid(); + + const auto& uuid = player.getUuid(); // Check if the player has joined before. if (!playerDb->get(uuid.asString())) { @@ -503,25 +492,28 @@ bool Plugin::enable() { ); // ... + } -bool Plugin::disable() { +auto disable(ll::plugin::NativePlugin& /*self*/) -> bool { + // ... auto& eventBus = ll::event::EventBus::getInstance(); - eventBus.removeListener(mPlayerJoinEventListener); + eventBus.removeListener(playerJoinEventListener); // ... + } ``` 让我们将这些代码拆开来看。在回调lambda函数中,我们捕获了配置中的`doGiveClockOnFirstJoin`,以及插件的logger和数据库实例。然后,我们判断配置中的`doGiveClockOnFirstJoin`是否为`true`,如果是,则继续执行逻辑。 ```cpp -[doGiveClockOnFirstJoin = mConfig.doGiveClockOnFirstJoin, +[doGiveClockOnFirstJoin = config.doGiveClockOnFirstJoin, &logger, - &playerDb = mPlayerDb](ll::event::player::PlayerJoinEvent& event) { + &playerDb = playerDb](ll::event::player::PlayerJoinEvent& event) { if (doGiveClockOnFirstJoin) { // ... } @@ -578,41 +570,33 @@ if (!playerDb->set(uuid.asString(), "true")) { 在`disable()`函数中,我们需要在事件总线上移除事件监听器以取消对事件的订阅。 ```cpp -eventBus.removeListener(mPlayerJoinEventListener); +eventBus.removeListener(playerJoinEventListener); ``` ## 使用钟的时候,弹出确认自杀的提示 我们的插件的第三个功能是使用钟的时候,弹出确认自杀的提示,玩家确认后可以自杀。我们需要订阅玩家使用物品的事件,当玩家使用钟时,弹出确认自杀的提示。 -在`Plugin.h`中,我们增加一个事件监听器指针: +在匿名命名空间中,我们增加一个事件监听器指针: ```cpp -// ... +ll::event::ListenerPtr playerUseItemEventListener; +``` -class Plugin { +在`enable()`函数中注册这个事件监听器,并在`disable()`函数中取消注册。 -// ... +```cpp +auto enable(ll::plugin::NativePlugin& /*self*/) -> bool { -private: // ... - ll::event::ListenerPtr mPlayerUseItemEventListener; -}; -``` - -在`Plugin.cpp`中,我们在`enable()`函数中注册这个事件监听器,并在`disable()`函数中取消注册。 - -```cpp -bool Plugin::enable() { - mPlayerUseItemEventListener = eventBus.emplaceListener( - [enableClockMenu = mConfig.enableClockMenu, &logger](ll::event::PlayerUseItemEvent& event) { + playerUseItemEventListener = + eventBus.emplaceListener([enableClockMenu = config.enableClockMenu, + &logger](ll::event::PlayerUseItemEvent& event) { if (enableClockMenu) { auto& player = event.self(); auto& itemStack = event.item(); - logger.info("{} used {}", player.getRealName(), itemStack.getRawNameId()); - if (itemStack.getRawNameId() == "clock") { ll::form::ModalForm form( "Warning", @@ -629,28 +613,30 @@ bool Plugin::enable() { ); form.sendTo(player); - - logger.info("{} opened better-suicide menu", player.getRealName()); } } - } - ); + }); + + // ... + } -bool Plugin::disable() { +auto disable(ll::plugin::NativePlugin& /*self*/) -> bool { + // ... - eventBus.removeListener(mPlayerUseItemEventListener); + eventBus.removeListener(playerUseItemEventListener); // ... + } ``` 让我们将代码拆开来看。在回调lambda函数中,我们捕获了配置项`enableClockMenu`和logger,然后进行判断,只有配置项启用时,才执行逻辑。 ```cpp -mPlayerUseItemEventListener = eventBus.emplaceListener( - [enableClockMenu = mConfig.enableClockMenu, &logger](ll::event::PlayerUseItemEvent& event) { +playerUseItemEventListener = eventBus.emplaceListener( + [enableClockMenu = config.enableClockMenu, &logger](ll::event::PlayerUseItemEvent& event) { if (enableClockMenu) { // ... } @@ -701,8 +687,8 @@ form.sendTo(player); 如果你的插件正常构建完毕,你应该能看到`bin/`目录内有一个以你的插件名为名的目录。将这个目录拷贝到LeviLamina目录中的`plugins/`目录里面(如果没有,请创建),得到如下的文件结构: ```text -/path/to/levilamina/plugins/your-plugin-name -├── your-plugin-name.dll +/path/to/levilamina/plugins/better-suicide +├── better-suicide.dll └── manifest.json ```