diff options
Diffstat (limited to 'MCServer/Plugins/APIDump')
-rw-r--r-- | MCServer/Plugins/APIDump/APIDesc.lua | 25 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/Hooks/OnDisconnect.lua | 24 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/Hooks/OnProjectileHitBlock.lua | 5 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/InfoFile.html | 246 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/UsingChunkStays.html | 167 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/WebWorldThreads.html | 48 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/main.css | 3 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/main_APIDump.lua | 6 |
8 files changed, 482 insertions, 42 deletions
diff --git a/MCServer/Plugins/APIDump/APIDesc.lua b/MCServer/Plugins/APIDump/APIDesc.lua index 233bdbc46..19ca971e2 100644 --- a/MCServer/Plugins/APIDump/APIDesc.lua +++ b/MCServer/Plugins/APIDump/APIDesc.lua @@ -114,6 +114,7 @@ g_APIDesc = GetBlockSkyLight = { Params = "BlockX, BlockY, BlockZ", Return = "NIBBLETYPE", Notes = "Returns the skylight at the specified absolute coords" }, GetBlockType = { Params = "BlockX, BlockY, BlockZ", Return = "BLOCKTYPE", Notes = "Returns the block type at the specified absolute coords" }, GetBlockTypeMeta = { Params = "BlockX, BlockY, BlockZ", Return = "BLOCKTYPE, NIBBLETYPE", Notes = "Returns the block type and meta at the specified absolute coords" }, + GetCoordRange = {Params = "", Return = "MaxX, MaxY, MaxZ", Notes = "Returns the maximum relative coords in all 3 axes. See also GetSize()." }, GetDataTypes = { Params = "", Return = "number", Notes = "Returns the mask of datatypes that the object is currently holding" }, GetOrigin = { Params = "", Return = "OriginX, OriginY, OriginZ", Notes = "Returns the origin coords of where the area was read from." }, GetOriginX = { Params = "", Return = "number", Notes = "Returns the origin x-coord" }, @@ -124,7 +125,7 @@ g_APIDesc = GetRelBlockSkyLight = { Params = "RelBlockX, RelBlockY, RelBlockZ", Return = "NIBBLETYPE", Notes = "Returns the skylight at the specified relative coords" }, GetRelBlockType = { Params = "RelBlockX, RelBlockY, RelBlockZ", Return = "BLOCKTYPE", Notes = "Returns the block type at the specified relative coords" }, GetRelBlockTypeMeta = { Params = "RelBlockX, RelBlockY, RelBlockZ", Return = "BLOCKTYPE, NIBBLETYPE", Notes = "Returns the block type and meta at the specified relative coords" }, - GetSize = { Params = "", Return = "SizeX, SizeY, SizeZ", Notes = "Returns the size of the area in all 3 axes." }, + GetSize = { Params = "", Return = "SizeX, SizeY, SizeZ", Notes = "Returns the size of the area in all 3 axes. See also GetCoordRange()." }, GetSizeX = { Params = "", Return = "number", Notes = "Returns the size of the held data in the x-axis" }, GetSizeY = { Params = "", Return = "number", Notes = "Returns the size of the held data in the y-axis" }, GetSizeZ = { Params = "", Return = "number", Notes = "Returns the size of the held data in the z-axis" }, @@ -699,7 +700,7 @@ end</pre> GetLevel = { Params = "EnchantmentNumID", Return = "number", Notes = "Returns the level of the specified enchantment stored in this object; 0 if not stored" }, IsEmpty = { Params = "", Return = "bool", Notes = "Returns true if the object stores no enchantments" }, SetLevel = { Params = "EnchantmentNumID, Level", Return = "", Notes = "Sets the level for the specified enchantment, adding it if not stored before or removing it if level < = 0" }, - StringToEnchantmentID = { Params = "EnchantmentTextID", Return = "number", Notes = "(static) Returns the enchantment numerical ID, -1 if not understood. Case insensitive" }, + StringToEnchantmentID = { Params = "EnchantmentTextID", Return = "number", Notes = "(static) Returns the enchantment numerical ID, -1 if not understood. Case insensitive. Also understands plain numbers." }, ToString = { Params = "", Return = "string", Notes = "Returns the string description of all the enchantments stored in this object, in numerical-ID form" }, }, Constants = @@ -1969,7 +1970,7 @@ cPluginManager.AddHook(cPluginManager.HOOK_CHAT, OnChatMessage); BroadcastChatSuccess = { Params = "Message", Return = "", Notes = "Prepends Green [INFO] / colours entire text (depending on ShouldUseChatPrefixes()) and broadcasts message. For success messages." }, BroadcastChatWarning = { Params = "Message", Return = "", Notes = "Prepends Rose [WARN] / colours entire text (depending on ShouldUseChatPrefixes()) and broadcasts message. For concerning events, such as plugin reload etc." }, CreateAndInitializeWorld = { Params = "WorldName", Return = "{{cWorld|cWorld}}", Notes = "Creates a new world and initializes it. If there is a world whith the same name it returns nil." }, - FindAndDoWithPlayer = { Params = "PlayerName, CallbackFunction", Return = "", Notes = "Calls the given callback function for the given player." }, + FindAndDoWithPlayer = { Params = "PlayerName, CallbackFunction", Return = "", Notes = "Calls the given callback function for all players with names partially (or fully) matching the name string provided." }, ForEachPlayer = { Params = "CallbackFunction", Return = "", Notes = "Calls the given callback function for each player. The callback function has the following signature: <pre class=\"prettyprint lang-lua\">function Callback({{cPlayer|cPlayer}})</pre>" }, ForEachWorld = { Params = "CallbackFunction", Return = "", Notes = "Calls the given callback function for each world. The callback function has the following signature: <pre class=\"prettyprint lang-lua\">function Callback({{cWorld|cWorld}})</pre>" }, GetCraftingRecipes = { Params = "", Return = "{{cCraftingRecipe|cCraftingRecipe}}", Notes = "Returns the CraftingRecipes object" }, @@ -2294,10 +2295,14 @@ end IsGameModeCreative = { Params = "", Return = "bool", Notes = "Returns true if the current gamemode is gmCreative." }, IsGameModeSurvival = { Params = "", Return = "bool", Notes = "Returns true if the current gamemode is gmSurvival." }, IsPVPEnabled = { Params = "", Return = "bool", Notes = "Returns whether PVP is enabled in the world settings." }, - IsWeatherRain = { Params = "", Return = "bool", Notes = "Returns true if the current weather is rain." }, - IsWeatherStorm = { Params = "", Return = "bool", Notes = "Returns true if the current weather is a storm." }, + IsWeatherRain = { Params = "", Return = "bool", Notes = "Returns true if the current world is raining." }, + IsWeatherRainAt = { Params = "BlockX, BlockZ", Return = "bool", Notes = "Returns true if the specified location is raining (takes into account biomes)." }, + IsWeatherStorm = { Params = "", Return = "bool", Notes = "Returns true if the current world is stormy." }, + IsWeatherStormAt = { Params = "BlockX, BlockZ", Return = "bool", Notes = "Returns true if the specified location is stormy (takes into account biomes)." }, IsWeatherSunny = { Params = "", Return = "bool", Notes = "Returns true if the current weather is sunny." }, - IsWeatherWet = { Params = "", Return = "bool", Notes = "Returns true if the current weather has any precipitation (rain or storm)." }, + IsWeatherSunnyAt = { Params = "BlockX, BlockZ", Return = "bool", Notes = "Returns true if the current weather is sunny at the specified location (takes into account biomes)." }, + IsWeatherWet = { Params = "", Return = "bool", Notes = "Returns true if the current world has any precipitation (rain or storm)." }, + IsWeatherWetAt = { Params = "BlockX, BlockZ", Return = "bool", Notes = "Returns true if the specified location has any precipitation (rain or storm) (takes into account biomes)." }, QueueBlockForTick = { Params = "BlockX, BlockY, BlockZ, TicksToWait", Return = "", Notes = "Queues the specified block to be ticked after the specified number of gameticks." }, QueueSaveAllChunks = { Params = "", Return = "", Notes = "Queues all chunks to be saved in the world storage thread" }, QueueSetBlock = { Params = "BlockX, BlockY, BlockZ, BlockType, BlockMeta, TickDelay", Return = "", Notes = "Queues the block to be set to the specified blocktype and meta after the specified amount of game ticks. Uses SetBlock() for the actual setting, so simulators are woken up and block entities are handled correctly." }, @@ -2929,9 +2934,11 @@ end { -- No sorting is provided for these, they will be output in the same order as defined here { FileName = "Writing-a-MCServer-plugin.html", Title = "Writing a MCServer plugin" }, - { FileName = "SettingUpDecoda.html", Title = "Setting up the Decoda Lua IDE" }, - { FileName = "SettingUpZeroBrane.html", Title = "Setting up the ZeroBrane Studio Lua IDE" }, - { FileName = "WebWorldThreads.html", Title = "Webserver vs World threads" }, + { FileName = "InfoFile.html", Title = "Using the Info.lua file" }, + { FileName = "SettingUpDecoda.html", Title = "Setting up the Decoda Lua IDE" }, + { FileName = "SettingUpZeroBrane.html", Title = "Setting up the ZeroBrane Studio Lua IDE" }, + { FileName = "UsingChunkStays.html", Title = "Using ChunkStays" }, + { FileName = "WebWorldThreads.html", Title = "Webserver vs World threads" }, } } ; diff --git a/MCServer/Plugins/APIDump/Hooks/OnDisconnect.lua b/MCServer/Plugins/APIDump/Hooks/OnDisconnect.lua index a3301a8c6..204cb63d2 100644 --- a/MCServer/Plugins/APIDump/Hooks/OnDisconnect.lua +++ b/MCServer/Plugins/APIDump/Hooks/OnDisconnect.lua @@ -2,23 +2,33 @@ return { HOOK_DISCONNECT = { - CalledWhen = "A player has explicitly disconnected.", + CalledWhen = [[ + A client has disconnected, either by explicitly sending the disconnect packet (in older protocols) or + their connection was terminated + ]], DefaultFnName = "OnDisconnect", -- also used as pagename Desc = [[ - This hook is called when a client is about to be disconnected from the server, for whatever reason. - - <p><b>Note that this hook will be removed after <1.7 protocol support is removed, as it was originally a hook for - the client sending the server a disconnect packet, which no longer happens.</b></p> + This hook is called when a client has disconnected from the server, for whatever reason. It is also + called when the client sends the Disconnect packet (only in pre-1.7 protocols). This hook is not called + for server ping connections.</p> + <p> + Note that the hook is called even for connections to players who failed to auth. In such a case there's + no {{cPlayer}} object associated with the client.</p> + <p> + See also the {{OnHandshake|HOOK_HANDSHAKE}} hook which is called when the client connects (and presents + a handshake message, so that they are not just status-pinging). If you need to store a per-player + object, use the {{OnPlayerJoined|HOOK_PLAYER_JOINED}} and {{OnPlayerDestroyed|HOOK_PLAYER_DESTROYED}} + hooks instead, those are guaranteed to have the {{cPlayer}} object associated. ]], Params = { - { Name = "Player", Type = "{{cPlayer}}", Notes = "The player who has disconnected" }, + { Name = "Client", Type = "{{cClientHandle}}", Notes = "The client who has disconnected" }, { Name = "Reason", Type = "string", Notes = "The reason that the client has sent in the disconnect packet" }, }, Returns = [[ If the function returns false or no value, MCServer calls other plugins' callbacks for this event. If the function returns true, no other plugins are called for this event. In either case, - the player is disconnected. + the client is disconnected. ]], }, -- HOOK_DISCONNECT } diff --git a/MCServer/Plugins/APIDump/Hooks/OnProjectileHitBlock.lua b/MCServer/Plugins/APIDump/Hooks/OnProjectileHitBlock.lua index 1588d420c..72cf85821 100644 --- a/MCServer/Plugins/APIDump/Hooks/OnProjectileHitBlock.lua +++ b/MCServer/Plugins/APIDump/Hooks/OnProjectileHitBlock.lua @@ -10,6 +10,11 @@ return Params = { { Name = "ProjectileEntity", Type = "{{cProjectileEntity}}", Notes = "The projectile that hit an entity." }, + { Name = "BlockX", Type = "number", Notes = "The X-coord where the projectile hit." }, + { Name = "BlockY", Type = "number", Notes = "The Y-coord where the projectile hit." }, + { Name = "BlockZ", Type = "number", Notes = "The Z-coord where the projectile hit." }, + { Name = "BlockFace", Type = "number", Notes = "The side of the block where the projectile hit." }, + { Name = "BlockHitPos", Type = "Vector3d", Notes = "The exact position where the projectile hit." }, }, Returns = [[ If the function returns false or no value, the next plugin's callback is called. If the function diff --git a/MCServer/Plugins/APIDump/InfoFile.html b/MCServer/Plugins/APIDump/InfoFile.html new file mode 100644 index 000000000..3fff06d20 --- /dev/null +++ b/MCServer/Plugins/APIDump/InfoFile.html @@ -0,0 +1,246 @@ +<!DOCTYPE html> +<html> + <head> + <title>MCServer - Info.lua file</title> + <link rel="stylesheet" type="text/css" href="main.css" /> + <link rel="stylesheet" type="text/css" href="prettify.css" /> + <script src="prettify.js"></script> + <script src="lang-lua.js"></script> + <meta charset="UTF-8"> + </head> + <body> + <div id="content"> + <h1>Info.lua file</h1> + <h2>Contents</h2> + <ul> + <li><a href="#Introduction">Introduction</a></li> + <li><a href="#Overall">The overall structure</a></li> + <li><a href="#AdditionalInformation">AdditionalInformation table</a></li> + <li><a href="#Commands">Commands table</a></li> + <li><a href="#ConsoleCommands">ConsoleCommands table</a></li> + <li><a href="#Permissions">Permissions table</a></li> + <li><a href="#Using">Using the file in code</a></li> + <li><a href="#Examples">Examples</a></li> + </ul> + + + <hr /> + <a name="Introduction"><h2>Introduction</h2></a> + + <p>For a long time MCServer plugins were plagued by poor documentation. The plugins worked, people who wrote them knew how to use them, but for anyone new to the plugin it was a terrible ordeal learning how to use it. Most of the times, the plugin authors only wrote what commands the plugin supported, sometimes not even that. Then, there was a call to action to put an end to this, to make documenting the plugins easy and at the same time centralized. Thus, the Info.lua file was born.</p> + + <p>Most plugins have some parts that are the same across all the plugins. These are commands, console commands and their permissions. If a plugin implemented a command, it would practically copy & paste the same code over and over again. So it makes sense to extract only unique information, centralize it and automate all the parts around it. This was another reason for the Info.lua file - it is a central hub of commands, console commands and their permissions.</p> + + <p>Last, but not least, we want to make a plugin repository on the web in the future, a repository that would store plugins, their descriptions, comments. It makes sense that the centralized information can be parsed by the repository automatically, so that advanced things, such as searching for a plugin based on a command, or determining whether two plugins collide command-wise, are possible.</p> + + <p>After this file format has been devised, a tool has been written that allows for an easy generation of the documentation for the plugin in various formats. It outputs the documentation in a format that is perfect for pasting into the forum. It generates documentation in a Markup format to use in README.md on GitHub and similar sites. The clever thing is that you don't need to keep all those formats in sync manually - you edit the Info.lua file and this tool will re-generate the documentation for you.</p> + + <p>So to sum up, the Info.lua file contains the plugins' commands, console commands, their permissions and possibly the overall plugin documentation, in a structured manner that can be parsed by a program, yet is human readable and editable.</p> + + + <hr /> + <a name="Overall"><h2>The overall structure</h2></a> + + <p>The file consist of a declaration of a single Lua table, g_PluginInfo. This table contains all the information, structured, as its members. Each member can be a structure by itself. The entire file is a valid Lua source file, so any tool that syntax-checks Lua source can syntax-check this file. The file is somewhat forward- and backward- compatible, in the sense that it can be extended in any way without breaking.</p> + <p>Here's a skeleton of the file:</p> +<pre class="prettyprint lang-lua"> +g_PluginInfo = +{ + Name = "Example Plugin", + Date = "2014-06-12", + Description = "This is an example plugin that shows how to use the Info.lua file", + + -- The following members will be documented in greater detail later: + AdditionalInformation = {}, + Commands = {}, + ConsoleCommands = {}, + Permissions = {}, +} +</pre> + <p>As you can see, the structure is pretty straightforward. Note that the order of the elements inside the table is not important (Lua property).</p> + + <p>The first few elements are for book-keeping. They declare the plugin's name, the date in ISO-format, representing the version of the plugin, and the description. The idea is that the description sums up what the plugin is all about, within some two or three sentences.</p> + + + <hr /> + <a name="AdditionalInformation"><h2>AdditionalInformation table</h2></a> + + <p>This table is used for more detailed description of the plugin. If there is any non-trivial setup process, dependencies, describe them here. This is where the description should get detailed. Don't worry about using several paragraphs of text here, if it makes the plugin easier to understand.</p> + + <p>The table should have the following layout:</p> +<pre class="prettyprint lang-lua"> +AdditionalInformation = +{ + { + Title = "Chapter 1", + Contents = "Describe one big aspect of the plugin here", + }, + { + Title = "Chapter 2", + Contents = "Describe another big topic", + }, +} +</pre> + <p>The idea here is that the tool that is used to generate the documentation from the Info.lua file will create a linkified table of contents and then each of the information elements' contents. This information should be all that is needed to successfully configure, run and manage the plugin.</p> + + + <hr /> + <a name="Commands"><h2>Commands table</h2></a> + + <p>The commands table lists all the commands that the plugin implements, together with their handler functions, required permissions, help strings and further information. The table supports recursion, which allows plugins to create multi-word commands easily (such as "//schematic load" and "//schematic save"), each having its own separate handler.</p> + + <p>The table uses structure similar to the following:</p> +<pre class="prettyprint lang-lua"> +Commands = +{ + ["/cmd1"] = + { + HelpString = "Performs the first action", + Permission = "firstplugin.cmds.1", + Alias = "/c1", + Handler = HandleCmd1, + ParameterCombinations = + { + { + Params = "x y z", + Help = "Performs the first action at the specified coordinates", + }, + { + Params = "-p", + Help = "Performs the first action at the player's coordinates", + } + }, + }, + ["/cmd2"] = + { + Alias = {"/c2", "//c2" }, + Subcommands = + { + sub1 = -- This declares a "/cmd2 sub1" command + { + HelpString = "Performs the second action's first subcommand", + Permission = "firstplugin.cmds.2.1", + Alias = "1", + Handler = HandleCmd2Sub1, + ParameterCombinations = + { + { + Params = "x y z", + Help = "Performs the second action's first subcommand at the specified coordinates", + }, + { + Params = "-p", + Help = "Performs the second action's first subcommand at the player's coordinates", + } + }, + }, + sub2 = -- Declares a "/cmd2 sub2" command + { + HelpString = "Performs the second action's second subcommand", + Permission = "firstplugin.cmds.2.2", + Handler = HandleCmd2Sub2, + }, + }, + }, +} +</pre> + + <p>Although it may seem overwhelming at first, there is a "method to this madness". Each element of the Commands table defines one command. Most commands start with a slash, so the special Lua syntax for table elements with non-standard names needs to be applied (<code>["/cmd1"] =</code>). The command can either specify subcommands, or a handler function (specifying both is UndefinedBehavior). Subcommands uses the same structure as the entire Commands table, recursively.</p> + + <p>The permission element specifies that the command is only available with the specified permission. Note that the permission for subcommand's parent isn't checked when the subcommand is called. This means that specifying the permission for a command that has subcommands has no effect whatsoever, but is discouraged because we may add processing for that in the future.</p> + + <p>The ParameterCombinations table is used only for generating the documentation, it lists the various combinations of parameters that the command supports. It's worth specifying even if the command supports only one combination, because that combination will get documented this way.</p> + + <p>The Alias member specifies any possible aliases for the command. Each alias is registered separately and if there is a subcommand table, it is applied to all aliases, just as one would expect. You can specify either a single string as the value (if there's only one alias), or a table of strings for multiple aliases. Commands with no aliases do not need to specify this member at all.</p> + + + <hr /> + <a name="ConsoleCommands"><h2>ConsoleCommands table</h2> + + <p>This table serves a purpose similar to that of the Commands table, only these commands are provided for the server console. Therefore, there are no permissions specified for these commands. Since most console commands don't use a leading slash, the command names don't need the special syntax. Also, the handler function doesn't receive the Player parameter.</p> + + <p>Here's an example of a ConsoleCommands table:</p> +<pre class="prettyprint lang-lua"> +ConsoleCommands = +{ + concmd = + { + HelpString = "Performs the console action", + Subcommands = + { + sub1 = + { + HelpString = "Performs the console action's first subcommand", + Handler = HandleConCmdSub1, + ParameterCombinations = + { + { + Params = "x y z", + Help = "Performs the console action's first subcommand at the specified coordinates", + }, + }, + }, + sub2 = + { + HelpString = "Performs the console action's second subcommand", + Handler = HandleConCmdSub2, + }, + }, + }, +} +</pre> + + + <hr /> + <a name="Permissions"><h2>Permissions table</h2></a> + + <p>The purpose of this table is to document permissions that the plugin uses. The documentation generator automatically collects the permissions specified in the Command table; the Permissions table adds a description for these permissions and may declare other permissions that aren't specifically included in the Command table.</p> + +<pre class="prettyprint lang-lua"> +Permissions = +{ + ["firstplugin.cmd.1.1"] = + { + Description = "Allows the players to build high towers using the first action.", + RecommendedGroups = "players", + }, + ["firstplugin.cmd.2.1"] = + { + Description = "Allows the players to kill entities using the second action. Note that this may be misused to kill other players, too.", + RecommendedGroups = "admins, mods", + }, +} +</pre> + + <p>The RecommendedGroup element lists, in plain English, the intended groups for which the permission should be enabled on a typical server. Plugin authors are advised to create reasonable defaults, prefering security to openness, so that admins using these settings blindly don't expose their servers to malicious users.</p> + + + <hr /> + <a name="Using"><h2>Using the file in code</h2></a> + + <p>Just writing the Info.lua file and saving it to the plugin folder is not enough for it to actually be used. Your plugin needs to include the following boilerplate code, preferably in its Initialize() function:</p> +<pre class="prettyprint lang-lua"> +-- Use the InfoReg shared library to process the Info.lua file: +dofile(cPluginManager:GetPluginsPath() .. "/InfoReg.lua") +RegisterPluginInfoCommands() +RegisterPluginInfoConsoleCommands() +</pre> + + <p>Of course, if your plugin doesn't have any console commands, it doesn't need to call the RegisterPluginInfoConsoleCommands() function, and similarly if it doesn't have any in-game commands, it doesn't need to call the RegisterPluginInfoCommands() function.</p> + + + <hr /> + <a name="Examples"><h2>Examples</h2></a> + + <p>There are several plugins that already implement this approach. You can visit them for inspiration and to see what the generated documentation looks like:</p> + <ul> + <li>Gallery plugin: <a href="https://github.com/mc-server/Gallery/blob/master/Info.lua">Info.lua</a>, <a href="http://forum.mc-server.org/showthread.php?tid=1306">Forum</a> documentation</li> + <li>WorldEdit plugin: <a href="https://github.com/mc-server/WorldEdit/blob/master/Info.lua">Info.lua</a>, <a href="http://forum.mc-server.org/showthread.php?tid=870">Forum</a> and <a href="https://github.com/mc-server/WorldEdit">MarkDown</a> documentation</li> + </ul> + + <script> + prettyPrint(); + </script> + </div> + </body> +</html> diff --git a/MCServer/Plugins/APIDump/UsingChunkStays.html b/MCServer/Plugins/APIDump/UsingChunkStays.html new file mode 100644 index 000000000..d3ecc6674 --- /dev/null +++ b/MCServer/Plugins/APIDump/UsingChunkStays.html @@ -0,0 +1,167 @@ +<!DOCTYPE html> +<html> + <head> + <title>MCServer - Using ChunkStays</title> + <link rel="stylesheet" type="text/css" href="main.css" /> + <link rel="stylesheet" type="text/css" href="prettify.css" /> + <script src="prettify.js"></script> + <script src="lang-lua.js"></script> + <meta charset="UTF-8"> + </head> + <body> + <div id="content"> + <h1>Using ChunkStays</h1> + <p> + A plugin may need to manipulate data in arbitrary chunks, and it needs a way to make the server + guarantee that the chunks are available in memory.</p> + + <h2>The problem</h2> + <p> + Usually when plugins want to manipulate larger areas of world data, they need to make sure that the + server has the appropriate chunks loaded in the memory. When the data being manipulated can be further + away from the connected players, or the data is being manipulated from a console handler, there is a + real chance that the chunks are not loaded.</p> + <p> + This gets even more important when using the <a href="cBlockArea.html">cBlockArea</a> class for reading + and writing. Those functions will fail when any of the required chunks aren't valid. This means that + either the block area has incomplete data (Read() failed) or incomplete data has been written to the + world (Write() failed). Recovery from this is near impossible - you can't simply read or write again + later, because the world may have changed in the meantime.</p> + + <h2>The solution</h2> + <p> + The naive solution would be to monitor chunk loads and unloads, and postpone the operations until all + the chunks are available. This would be quite ineffective and also very soon it would become very + difficult to maintain, if there were multiple code paths requiring this handling.</p> + <p> + An alternate approach has been implemented, accessible through a single (somewhat hidden) function + call: <a href="cWorld.html">cWorld:ChunkStay()</a>. All that this call basically does is, it tells the + server "Load these chunks for me, and call this callback function once you have them all." And the + server does exactly that - it remembers the callback and asks the world loader / generator to provide + the chunks. Once the chunks become available, it calls the callback function for the plugin.</p> + <p> + There are a few gotcha-s, though. If the code that was requesting the read or write had access to some + of the volatile objects, such as <a href="cPlayer.html">cPlayer</a> or + <a href="cEntity.html">cEntity</a> objects, those cannot be accessed by the callback anymore, because + they may have become invalid in the meantime - the player may have disconnected, the entity may have + despawned. So the callback must use the longer way to access such objects, such as calling + <a href="cWorld.html">cWorld:DoWithEntityByID()</a> or + <a href="cWorld.html">cWorld:DoWithPlayer()</a>.</p> + + <h2>The example</h2> + <p> + As a simple example, consider a theoretical plugin that allows a player to save the immediate + surroundings of the spawn into a schematic file. The player issues a command to initiate the save, and + the plugin reads a 50 x 50 x 50 block area around the spawn into a cBlockArea and saves it on the disk + as "<PlayerName>_spawn.schematic". When it's done with the saving, it wants to send a message to the + player to let them know the command has succeeded.</p> + <p> + The first attempt shows the naive approach. It simply reads the block area and saves it, then sends the + message. I'll repeat once more, this code is <b>the wrong way</b> to do it!</p> +<pre class="prettyprint lang-lua"> +function HandleCommandSaveSpawn(a_Split, a_Player) + -- Get the coords for the spawn: + local SpawnX = a_Player:GetWorld():GetSpawnX() + local SpawnY = a_Player:GetWorld():GetSpawnY() + local SpawnZ = a_Player:GetWorld():GetSpawnZ() + local Bounds = cCuboid(SpawnX - 25, SpawnY - 25, SpawnZ - 25, SpawnX + 25, SpawnY + 25, SpawnZ + 25) + Bounds:ClampY(0, 255) + + -- Read the area around spawn into a cBlockArea, save to file: + local Area = cBlockArea() + local FileName = a_Player:GetName() .. "_spawn.schematic" + Area:Read(a_Player:GetWorld(), Bounds, cBlockArea.baTypes + cBlockArea.baMetas) + Area:SaveToSchematicFile(FileName) + + -- Notify the player: + a_Player:SendMessage(cCompositeChat("The spawn has been saved", mtInfo)) + return true +end +</pre> + <p> + Now if the player goes exploring far and uses the command to save their spawn, the chunks aren't + loaded, so the BlockArea reading fails, the BlockArea contains bad data. Note that the plugin fails to + do any error checking and if the area isn't read from the world, it happily saves the incomplete data + and says "hey, everything's right", althought it has just trashed any previous backup of the spawn + schematic with nonsense data.</p> + <hr/> + <p> + The following script uses the ChunkStay method to alleviate chunk-related problems. This is <b>the + right way</b> of doing it:</p> +<pre class="prettyprint lang-lua"> +function HandleCommandSaveSpawn(a_Split, a_Player) + -- Get the coords for the spawn: + local SpawnX = a_Player:GetWorld():GetSpawnX() + local SpawnY = a_Player:GetWorld():GetSpawnY() + local SpawnZ = a_Player:GetWorld():GetSpawnZ() + local Bounds = cCuboid(SpawnX - 25, SpawnY - 25, SpawnZ - 25, SpawnX + 25, SpawnY + 25, SpawnZ + 25) + Bounds:ClampY(0, 255) + + -- Get a list of chunks that we need loaded: + local MinChunkX = math.floor((SpawnX - 25) / 16) + local MaxChunkX = math.ceil ((SpawnX + 25) / 16) + local MinChunkZ = math.floor((SpawnZ - 25) / 16) + local MaxChunkZ = math.ceil ((SpawnZ + 25) / 16) + local Chunks = {} + for x = MinChunkX, MaxChunkX do + for z = MinChunkZ, MaxChunkZ do + table.insert(Chunks, {x, z}) + end + end -- for x + + -- Store the player's name and world to use in the callback, because the a_Player object may no longer be valid: + local PlayerName = a_Player:GetName() + local World = a_Player:GetWorld() + + -- This is the callback that is executed once all the chunks are loaded: + local OnAllChunksAvailable = function() + -- Read the area around spawn into a cBlockArea, save to file: + local Area = cBlockArea() + local FileName = PlayerName .. "_spawn.schematic" + if (Area:Read(World, Bounds, cBlockArea.baTypes + cBlockArea.baMetas)) then + Area:SaveToSchematicFile(FileName) + Msg = cCompositeChat("The spawn has been saved", mtInfo) + else + Msg = cCompositeChat("Cannot save the spawn", mtFailure) + end + + -- Notify the player: + -- Note that we cannot use a_Player here, because it may no longer be valid (if the player disconnected before the command completes) + World:DoWithPlayer(PlayerName, + function (a_CBPlayer) + a_CBPlayer:SendMessage(Msg) + end + ) + end + + -- Ask the server to load our chunks and notify us once it's done: + World:ChunkStay(Chunks, nil, OnAllChunksAvailable) + + -- Note that code here may get executed before the callback is called! + -- The ChunkStay says "once you have the chunks", not "wait until you have the chunks" + -- So you can't notify the player here, because the saving needn't have occurred yet. + + return true +end +</pre> + <p> + Note that this code does its error checking of the Area:Read() function, and it will not overwrite the + previous file unless it actually has the correct data. If you're wondering how the reading could fail + when we've got the chunks loaded, there's still the issue of free RAM - if the memory for the area + cannot be allocated, it cannot be read even with all the chunks present. So we still do need that + check.</p> + + <h2>The conclusion</h2> + <p> + Although it makes the code a little bit longer and is a bit more difficult to grasp at first, the + ChunkStay is a useful technique to add to your repertoire. It is to be used whenever you need access to + chunks that may potentially be inaccessible, and you really need the data.</p> + <p>Possibly the biggest hurdle in using the ChunkStay is the fact that it does its work in the + background, thus invalidating all cPlayer and cEntity objects your function may hold, so you need to + re-acquire them from their IDs and names. This is the penalty for using multi-threaded code.</p> + <script> + prettyPrint(); + </script> + </div> + </body> +</html> diff --git a/MCServer/Plugins/APIDump/WebWorldThreads.html b/MCServer/Plugins/APIDump/WebWorldThreads.html index fc80a6178..ee0b172e6 100644 --- a/MCServer/Plugins/APIDump/WebWorldThreads.html +++ b/MCServer/Plugins/APIDump/WebWorldThreads.html @@ -39,31 +39,31 @@ <h2>Example</h2> The Core has the facility to kick players using the web interface. It used the following code for the kicking (inside the webadmin handler): - <pre class="prettyprint lang-lua"> - local KickPlayerName = Request.Params["players-kick"] - local FoundPlayerCallback = function(Player) - if (Player:GetName() == KickPlayerName) then - Player:GetClientHandle():Kick("You were kicked from the game!") - end +<pre class="prettyprint lang-lua"> +local KickPlayerName = Request.Params["players-kick"] +local FoundPlayerCallback = function(Player) + if (Player:GetName() == KickPlayerName) then + Player:GetClientHandle():Kick("You were kicked from the game!") + end +end +cRoot:Get():FindAndDoWithPlayer(KickPlayerName, FoundPlayerCallback) +</pre> +The cRoot:FindAndDoWithPlayer() is unsafe and could have caused a deadlock. The new solution is queue a task; but since we don't know in which world the player is, we need to queue the task to all worlds: +<pre class="prettyprint lang-lua"> +cRoot:Get():ForEachWorld( -- For each world... + function(World) + World:QueueTask( -- ... queue a task... + function(a_World) + a_World:DoWithPlayer(KickPlayerName, -- ... to walk the playerlist... + function (a_Player) + a_Player:GetClientHandle():Kick("You were kicked from the game!") -- ... and kick the player end - cRoot:Get():FindAndDoWithPlayer(KickPlayerName, FoundPlayerCallback) - </pre> - The cRoot:FindAndDoWithPlayer() is unsafe and could have caused a deadlock. The new solution is queue a task; but since we don't know in which world the player is, we need to queue the task to all worlds: - <pre class="prettyprint lang-lua"> - cRoot:Get():ForEachWorld( -- For each world... - function(World) - World:QueueTask( -- ... queue a task... - function(a_World) - a_World:DoWithPlayer(KickPlayerName, -- ... to walk the playerlist... - function (a_Player) - a_Player:GetClientHandle():Kick("You were kicked from the game!") -- ... and kick the player - end - ) - end - ) - end - ) - </pre> + ) + end + ) + end +) +</pre> <script> prettyPrint(); </script> diff --git a/MCServer/Plugins/APIDump/main.css b/MCServer/Plugins/APIDump/main.css index aa26bd186..8041e0d01 100644 --- a/MCServer/Plugins/APIDump/main.css +++ b/MCServer/Plugins/APIDump/main.css @@ -39,7 +39,8 @@ pre body { - min-width: 800px; + min-width: 400px; + max-width: 1200px; width: 95%; margin: 10px auto; background-color: white; diff --git a/MCServer/Plugins/APIDump/main_APIDump.lua b/MCServer/Plugins/APIDump/main_APIDump.lua index 52199740b..a25bab9cf 100644 --- a/MCServer/Plugins/APIDump/main_APIDump.lua +++ b/MCServer/Plugins/APIDump/main_APIDump.lua @@ -27,10 +27,14 @@ local function LoadAPIFiles(a_Folder, a_DstTable) -- We only want .lua files from the folder: if (cFile:IsFile(FileName) and fnam:match(".*%.lua$")) then local TablesFn, Err = loadfile(FileName); - if (TablesFn == nil) then + if (type(TablesFn) ~= "function") then LOGWARNING("Cannot load API descriptions from " .. FileName .. ", Lua error '" .. Err .. "'."); else local Tables = TablesFn(); + if (type(Tables) ~= "table") then + LOGWARNING("Cannot load API descriptions from " .. FileName .. ", returned object is not a table (" .. type(Tables) .. ")."); + break + end for k, cls in pairs(Tables) do a_DstTable[k] = cls; end |