From 59b80c84fa3eecfa30641b1e42c10a6081cdd241 Mon Sep 17 00:00:00 2001 From: STRWarrior Date: Wed, 25 Dec 2013 22:08:41 +0100 Subject: [PATCH 1/8] This generates gravel in Extreme Hills M biomes. --- src/Generating/DistortedHeightmap.cpp | 22 ++++++++++++++++++++-- src/Generating/DistortedHeightmap.h | 1 - 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Generating/DistortedHeightmap.cpp b/src/Generating/DistortedHeightmap.cpp index 342a4483f..a2d1c084a 100644 --- a/src/Generating/DistortedHeightmap.cpp +++ b/src/Generating/DistortedHeightmap.cpp @@ -101,6 +101,13 @@ static cDistortedHeightmap::sBlockInfo tbMycelium[] = {E_BLOCK_DIRT, 0}, } ; +static cDistortedHeightmap::sBlockInfo tbGravel[] = +{ + {E_BLOCK_GRAVEL, 0}, + {E_BLOCK_DIRT, 0}, + {E_BLOCK_DIRT, 0}, + {E_BLOCK_DIRT, 0}, +} ; @@ -146,6 +153,7 @@ static cPattern patDirt (tbDirt, ARRAYCOUNT(tbDirt)); static cPattern patPodzol (tbPodzol, ARRAYCOUNT(tbPodzol)); static cPattern patGrassLess(tbGrassLess, ARRAYCOUNT(tbGrassLess)); static cPattern patMycelium (tbMycelium, ARRAYCOUNT(tbMycelium)); +static cPattern patGravel (tbGravel, ARRAYCOUNT(tbGravel)); static cPattern patOFSand (tbOFSand, ARRAYCOUNT(tbOFSand)); static cPattern patOFClay (tbOFClay, ARRAYCOUNT(tbOFClay)); @@ -675,7 +683,6 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in case biSavanna: case biSavannaPlateau: case biSunflowerPlains: - case biExtremeHillsM: case biFlowerForest: case biTaigaM: case biSwamplandM: @@ -686,7 +693,6 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in case biBirchForestHillsM: case biRoofedForestM: case biColdTaigaM: - case biExtremeHillsPlusM: case biSavannaM: case biSavannaPlateauM: { @@ -737,6 +743,18 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in FillColumnMesa(a_ChunkDesc, a_RelX, a_RelZ); return; } + + case biExtremeHillsPlusM: + case biExtremeHillsM: + { + // Select the pattern to use - gravel or grass: + NOISE_DATATYPE NoiseX = ((NOISE_DATATYPE)(m_CurChunkX * cChunkDef::Width + a_RelX)) / FrequencyX; + NOISE_DATATYPE NoiseY = ((NOISE_DATATYPE)(m_CurChunkZ * cChunkDef::Width + a_RelZ)) / FrequencyZ; + NOISE_DATATYPE Val = m_OceanFloorSelect.CubicNoise2D(NoiseX, NoiseY); + const sBlockInfo * Pattern = (Val < -0.1) ? patGravel.Get() : patGrass.Get(); + FillColumnPattern(a_ChunkDesc, a_RelX, a_RelZ, Pattern); + return; + } default: ASSERT(!"Unhandled biome"); return; diff --git a/src/Generating/DistortedHeightmap.h b/src/Generating/DistortedHeightmap.h index e6b3c9d3f..8139a8b89 100644 --- a/src/Generating/DistortedHeightmap.h +++ b/src/Generating/DistortedHeightmap.h @@ -125,7 +125,6 @@ protected: /// Returns the pattern to use for an ocean floor in the specified column const sBlockInfo * ChooseOceanFloorPattern(int a_RelX, int a_RelZ); - // cTerrainHeightGen overrides: virtual void GenHeightMap(int a_ChunkX, int a_ChunkZ, cChunkDef::HeightMap & a_HeightMap) override; virtual void InitializeHeightGen(cIniFile & a_IniFile) override; From 2f59a93f2a09e7ae2dee93aa9925acb2d49841e1 Mon Sep 17 00:00:00 2001 From: STRWarrior Date: Wed, 25 Dec 2013 22:10:27 +0100 Subject: [PATCH 2/8] Re-added empty line. --- src/Generating/DistortedHeightmap.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Generating/DistortedHeightmap.h b/src/Generating/DistortedHeightmap.h index 8139a8b89..e6b3c9d3f 100644 --- a/src/Generating/DistortedHeightmap.h +++ b/src/Generating/DistortedHeightmap.h @@ -125,6 +125,7 @@ protected: /// Returns the pattern to use for an ocean floor in the specified column const sBlockInfo * ChooseOceanFloorPattern(int a_RelX, int a_RelZ); + // cTerrainHeightGen overrides: virtual void GenHeightMap(int a_ChunkX, int a_ChunkZ, cChunkDef::HeightMap & a_HeightMap) override; virtual void InitializeHeightGen(cIniFile & a_IniFile) override; From caf3b6d70cc1735f511925660ca3ea24ff2ae1ac Mon Sep 17 00:00:00 2001 From: STRWarrior Date: Wed, 25 Dec 2013 22:43:26 +0100 Subject: [PATCH 3/8] Normal extreme hills (plus) now generate a stone/grass pattern. --- src/Generating/DistortedHeightmap.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Generating/DistortedHeightmap.cpp b/src/Generating/DistortedHeightmap.cpp index a2d1c084a..4bb01c36f 100644 --- a/src/Generating/DistortedHeightmap.cpp +++ b/src/Generating/DistortedHeightmap.cpp @@ -109,6 +109,13 @@ static cDistortedHeightmap::sBlockInfo tbGravel[] = {E_BLOCK_DIRT, 0}, } ; +static cDistortedHeightmap::sBlockInfo tbStone[] = +{ + {E_BLOCK_STONE, 0}, + {E_BLOCK_STONE, 0}, + {E_BLOCK_STONE, 0}, + {E_BLOCK_STONE, 0}, +} ; @@ -154,6 +161,7 @@ static cPattern patPodzol (tbPodzol, ARRAYCOUNT(tbPodzol)); static cPattern patGrassLess(tbGrassLess, ARRAYCOUNT(tbGrassLess)); static cPattern patMycelium (tbMycelium, ARRAYCOUNT(tbMycelium)); static cPattern patGravel (tbGravel, ARRAYCOUNT(tbGravel)); +static cPattern patStone (tbStone, ARRAYCOUNT(tbStone)); static cPattern patOFSand (tbOFSand, ARRAYCOUNT(tbOFSand)); static cPattern patOFClay (tbOFClay, ARRAYCOUNT(tbOFClay)); @@ -656,7 +664,6 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in { case biOcean: case biPlains: - case biExtremeHills: case biForest: case biTaiga: case biSwampland: @@ -679,7 +686,6 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in case biRoofedForest: case biColdTaiga: case biColdTaigaHills: - case biExtremeHillsPlus: case biSavanna: case biSavannaPlateau: case biSunflowerPlains: @@ -744,6 +750,18 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in return; } + case biExtremeHillsPlus: + case biExtremeHills: + { + // Select the pattern to use - gravel or grass: + NOISE_DATATYPE NoiseX = ((NOISE_DATATYPE)(m_CurChunkX * cChunkDef::Width + a_RelX)) / FrequencyX; + NOISE_DATATYPE NoiseY = ((NOISE_DATATYPE)(m_CurChunkZ * cChunkDef::Width + a_RelZ)) / FrequencyZ; + NOISE_DATATYPE Val = m_OceanFloorSelect.CubicNoise2D(NoiseX, NoiseY); + const sBlockInfo * Pattern = (Val < -0.1) ? patStone.Get() : patGrass.Get(); + FillColumnPattern(a_ChunkDesc, a_RelX, a_RelZ, Pattern); + return; + } + case biExtremeHillsPlusM: case biExtremeHillsM: { From e282eb73c89c2feb567de767c0f9f34896fe8f93 Mon Sep 17 00:00:00 2001 From: STRWarrior Date: Wed, 25 Dec 2013 23:05:22 +0100 Subject: [PATCH 4/8] Changed tbGravel. Vanilla has 3 layers of gravel and then stone. --- src/Generating/DistortedHeightmap.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Generating/DistortedHeightmap.cpp b/src/Generating/DistortedHeightmap.cpp index 4bb01c36f..b6f3866e4 100644 --- a/src/Generating/DistortedHeightmap.cpp +++ b/src/Generating/DistortedHeightmap.cpp @@ -104,9 +104,9 @@ static cDistortedHeightmap::sBlockInfo tbMycelium[] = static cDistortedHeightmap::sBlockInfo tbGravel[] = { {E_BLOCK_GRAVEL, 0}, - {E_BLOCK_DIRT, 0}, - {E_BLOCK_DIRT, 0}, - {E_BLOCK_DIRT, 0}, + {E_BLOCK_GRAVEL, 0}, + {E_BLOCK_GRAVEL, 0}, + {E_BLOCK_STONE, 0}, } ; static cDistortedHeightmap::sBlockInfo tbStone[] = From b767fd784c1ccc563f3e11cefa0eb825732cd284 Mon Sep 17 00:00:00 2001 From: STRWarrior Date: Wed, 25 Dec 2013 23:18:33 +0100 Subject: [PATCH 5/8] Extreme Hills M biomes now generate gravel, stone and grass patterns. --- src/Generating/DistortedHeightmap.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Generating/DistortedHeightmap.cpp b/src/Generating/DistortedHeightmap.cpp index b6f3866e4..0a4a0940a 100644 --- a/src/Generating/DistortedHeightmap.cpp +++ b/src/Generating/DistortedHeightmap.cpp @@ -111,7 +111,7 @@ static cDistortedHeightmap::sBlockInfo tbGravel[] = static cDistortedHeightmap::sBlockInfo tbStone[] = { - {E_BLOCK_STONE, 0}, + {E_BLOCK_STONE, 0}, {E_BLOCK_STONE, 0}, {E_BLOCK_STONE, 0}, {E_BLOCK_STONE, 0}, @@ -769,7 +769,15 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in NOISE_DATATYPE NoiseX = ((NOISE_DATATYPE)(m_CurChunkX * cChunkDef::Width + a_RelX)) / FrequencyX; NOISE_DATATYPE NoiseY = ((NOISE_DATATYPE)(m_CurChunkZ * cChunkDef::Width + a_RelZ)) / FrequencyZ; NOISE_DATATYPE Val = m_OceanFloorSelect.CubicNoise2D(NoiseX, NoiseY); - const sBlockInfo * Pattern = (Val < -0.1) ? patGravel.Get() : patGrass.Get(); + const sBlockInfo * Pattern; + if (Val <= 0.0) + { + Pattern = (Val < -0.3) ? patGravel.Get() : patGrass.Get(); + } + else + { + Pattern = (Val < 0.3) ? patStone.Get() : patGrass.Get(); + } FillColumnPattern(a_ChunkDesc, a_RelX, a_RelZ, Pattern); return; } From 6884d4235ea81f64d79ee24c7aae7c91694ed47a Mon Sep 17 00:00:00 2001 From: STRWarrior Date: Thu, 26 Dec 2013 14:37:48 +0100 Subject: [PATCH 6/8] Simplefied Extreme Hills M pattern select. --- src/Generating/DistortedHeightmap.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Generating/DistortedHeightmap.cpp b/src/Generating/DistortedHeightmap.cpp index 0a4a0940a..15e352e30 100644 --- a/src/Generating/DistortedHeightmap.cpp +++ b/src/Generating/DistortedHeightmap.cpp @@ -753,7 +753,7 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in case biExtremeHillsPlus: case biExtremeHills: { - // Select the pattern to use - gravel or grass: + // Select the pattern to use - stone or grass: NOISE_DATATYPE NoiseX = ((NOISE_DATATYPE)(m_CurChunkX * cChunkDef::Width + a_RelX)) / FrequencyX; NOISE_DATATYPE NoiseY = ((NOISE_DATATYPE)(m_CurChunkZ * cChunkDef::Width + a_RelZ)) / FrequencyZ; NOISE_DATATYPE Val = m_OceanFloorSelect.CubicNoise2D(NoiseX, NoiseY); @@ -765,19 +765,11 @@ void cDistortedHeightmap::ComposeColumn(cChunkDesc & a_ChunkDesc, int a_RelX, in case biExtremeHillsPlusM: case biExtremeHillsM: { - // Select the pattern to use - gravel or grass: + // Select the pattern to use - gravel, stone or grass: NOISE_DATATYPE NoiseX = ((NOISE_DATATYPE)(m_CurChunkX * cChunkDef::Width + a_RelX)) / FrequencyX; NOISE_DATATYPE NoiseY = ((NOISE_DATATYPE)(m_CurChunkZ * cChunkDef::Width + a_RelZ)) / FrequencyZ; NOISE_DATATYPE Val = m_OceanFloorSelect.CubicNoise2D(NoiseX, NoiseY); - const sBlockInfo * Pattern; - if (Val <= 0.0) - { - Pattern = (Val < -0.3) ? patGravel.Get() : patGrass.Get(); - } - else - { - Pattern = (Val < 0.3) ? patStone.Get() : patGrass.Get(); - } + const sBlockInfo * Pattern = (Val < -0.9) ? patStone.Get() : ((Val > 0) ? patGravel.Get() : patGrass.Get()); FillColumnPattern(a_ChunkDesc, a_RelX, a_RelZ, Pattern); return; } From f1142af455eea674c07ab426fe5ef7d4302b16c9 Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Thu, 26 Dec 2013 14:55:19 +0000 Subject: [PATCH 7/8] Server now handles death messages --- src/Entities/Player.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp index 0fa8254ce..67d5a47ef 100644 --- a/src/Entities/Player.cpp +++ b/src/Entities/Player.cpp @@ -820,6 +820,22 @@ void cPlayer::KilledBy(cEntity * a_Killer) m_Inventory.Clear(); m_World->SpawnItemPickups(Pickups, GetPosX(), GetPosY(), GetPosZ(), 10); SaveToDisk(); // Save it, yeah the world is a tough place ! + + if (a_Killer == NULL) + { + GetWorld()->BroadcastChat(Printf("%s[DEATH] %s%s was killed by environmental damage", cChatColor::Red.c_str(), cChatColor::White.c_str(), GetName().c_str())); + } + else if (a_Killer->IsPlayer()) + { + GetWorld()->BroadcastChat(Printf("%s[DEATH] %s%s was killed by %s", cChatColor::Red.c_str(), cChatColor::White.c_str(), GetName().c_str(), ((cPlayer *)a_Killer)->GetName().c_str())); + } + else + { + AString KillerClass = a_Killer->GetClass(); + KillerClass.erase(KillerClass.begin()); // Erase the 'c' of the class (e.g. "cWitch" -> "Witch") + + GetWorld()->BroadcastChat(Printf("%s[DEATH] %s%s was killed by a %s", cChatColor::Red.c_str(), cChatColor::White.c_str(), GetName().c_str(), KillerClass.c_str())); + } } From d41f724a4034724f0c1da72cad15bd0a274ec62d Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Thu, 26 Dec 2013 15:11:48 +0000 Subject: [PATCH 8/8] Writing a plugin APIDump article (#382) --- MCServer/Plugins/APIDump/WebWorldThreads.html | 100 +++---- .../APIDump/Writing-a-MCServer-plugin.html | 253 ++++++++++++++++++ MCServer/Plugins/APIDump/main.css | 6 + 3 files changed, 310 insertions(+), 49 deletions(-) create mode 100644 MCServer/Plugins/APIDump/Writing-a-MCServer-plugin.html diff --git a/MCServer/Plugins/APIDump/WebWorldThreads.html b/MCServer/Plugins/APIDump/WebWorldThreads.html index 2f117ab7c..6caf40e6d 100644 --- a/MCServer/Plugins/APIDump/WebWorldThreads.html +++ b/MCServer/Plugins/APIDump/WebWorldThreads.html @@ -8,62 +8,64 @@ -

Webserver vs World threads

-

- This article will explain the threading issues that arise between the webserver and world threads are of concern to plugin authors.

-

- Generally, plugins that provide webadmin pages should be quite careful about their interactions. Most operations on MCServer objects requires synchronization, that MCServer provides automatically and transparently to plugins - when a block is written, the chunkmap is locked, or when an entity is being manipulated, the entity list is locked. Each plugin also has a mutex lock, so that only one thread at a time may be executing plugin code.

-

- This locking can be a source of deadlocks for plugins that are not written carefully.

+
+

Webserver vs World threads

+

+ This article will explain the threading issues that arise between the webserver and world threads are of concern to plugin authors.

+

+ Generally, plugins that provide webadmin pages should be quite careful about their interactions. Most operations on MCServer objects requires synchronization, that MCServer provides automatically and transparently to plugins - when a block is written, the chunkmap is locked, or when an entity is being manipulated, the entity list is locked. Each plugin also has a mutex lock, so that only one thread at a time may be executing plugin code.

+

+ This locking can be a source of deadlocks for plugins that are not written carefully.

-

Example scenario

-

Consider the following example. A plugin provides a webadmin page that allows the admin to kick players off the server. When the admin presses the "Kick" button, the plugin calls cWorld:DoWithPlayer() with a callback to kick the player. Everything seems to be working fine now.

-

- A new feature is developed in the plugin, now the plugin adds a new in-game command so that the admins can kick players while they're playing the game. The plugin registers a command callback with cPluginManager.AddCommand(). Now there are problems bound to happen.

-

- Suppose that two admins are in, one is using the webadmin and the other is in-game. Both try to kick a player at the same time. The webadmin locks the plugin, so that it can execute the plugin code, but right at this moment the OS switches threads. The world thread locks the world so that it can access the list of in-game commands, receives the in-game command, it tries to lock the plugin. The plugin is already locked, so the world thread is put on hold. After a while, the webadmin thread is woken up again and continues processing. It tries to lock the world so that it can traverse the playerlist, but the lock is already held by the world thread. Now both threads are holding one lock each and trying to grab the other lock, and are therefore deadlocked.

+

Example scenario

+

Consider the following example. A plugin provides a webadmin page that allows the admin to kick players off the server. When the admin presses the "Kick" button, the plugin calls cWorld:DoWithPlayer() with a callback to kick the player. Everything seems to be working fine now.

+

+ A new feature is developed in the plugin, now the plugin adds a new in-game command so that the admins can kick players while they're playing the game. The plugin registers a command callback with cPluginManager.AddCommand(). Now there are problems bound to happen.

+

+ Suppose that two admins are in, one is using the webadmin and the other is in-game. Both try to kick a player at the same time. The webadmin locks the plugin, so that it can execute the plugin code, but right at this moment the OS switches threads. The world thread locks the world so that it can access the list of in-game commands, receives the in-game command, it tries to lock the plugin. The plugin is already locked, so the world thread is put on hold. After a while, the webadmin thread is woken up again and continues processing. It tries to lock the world so that it can traverse the playerlist, but the lock is already held by the world thread. Now both threads are holding one lock each and trying to grab the other lock, and are therefore deadlocked.

-

How to avoid the deadlock

-

- There are two main ways to avoid such a deadlock. The first approach is using tasks: Everytime you need to execute a task inside a world, instead of executing it, queue it, using cWorld:QueueTask(). This handy utility can will call the given function inside the world's TickThread, thus eliminating the deadlock, because now there's only one thread. However, this approach will not let you get data back. You cannot query the player list, or the entities, or anything - because when the task runs, the webadmin page has already been served to the browser.

-

- To accommodate this, you'll need to use the second approach - preparing and caching data in the tick thread, possibly using callbacks. This means that the plugin will have global variables that will store the data, and update those variables when the data changes; then the webserver thread will only read those variables, instead of calling the world functions. For example, if a webpage was to display the list of currently connected players, the plugin should maintain a global variable, g_WorldPlayers, which would be a table of worlds, each item being a list of currently connected players. The webadmin handler would read this variable and create the page from it; the plugin would use HOOK_PLAYER_JOINED and HOOK_DISCONNECT to update the variable.

+

How to avoid the deadlock

+

+ There are two main ways to avoid such a deadlock. The first approach is using tasks: Everytime you need to execute a task inside a world, instead of executing it, queue it, using cWorld:QueueTask(). This handy utility can will call the given function inside the world's TickThread, thus eliminating the deadlock, because now there's only one thread. However, this approach will not let you get data back. You cannot query the player list, or the entities, or anything - because when the task runs, the webadmin page has already been served to the browser.

+

+ To accommodate this, you'll need to use the second approach - preparing and caching data in the tick thread, possibly using callbacks. This means that the plugin will have global variables that will store the data, and update those variables when the data changes; then the webserver thread will only read those variables, instead of calling the world functions. For example, if a webpage was to display the list of currently connected players, the plugin should maintain a global variable, g_WorldPlayers, which would be a table of worlds, each item being a list of currently connected players. The webadmin handler would read this variable and create the page from it; the plugin would use HOOK_PLAYER_JOINED and HOOK_DISCONNECT to update the variable.

-

What to avoid

-

- Now that we know what the danger is and how to avoid it, how do we know if our code is susceptible?

-

- The general rule of thumb is to avoid calling any functions that read or write lists of things in the webserver thread. This means most ForEach() and DoWith() functions. Only cRoot:ForEachWorld() is safe - because the list of worlds is not expected to change, so it is not guarded by a mutex. Getting and setting world's blocks is, naturally, unsafe, as is calling other plugins, or creating entities.

+

What to avoid

+

+ Now that we know what the danger is and how to avoid it, how do we know if our code is susceptible?

+

+ The general rule of thumb is to avoid calling any functions that read or write lists of things in the webserver thread. This means most ForEach() and DoWith() functions. Only cRoot:ForEachWorld() is safe - because the list of worlds is not expected to change, so it is not guarded by a mutex. Getting and setting world's blocks is, naturally, unsafe, as is calling other plugins, or creating entities.

-

Example

- The Core has the facility to kick players using the web interface. It used the following code for the kicking (inside the webadmin handler): -
-		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)
-		
- 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: -
-		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
+			

Example

+ The Core has the facility to kick players using the web interface. It used the following code for the kicking (inside the webadmin handler): +
+			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)
+			
+ 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: +
+			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
 			)
-		  end
-		)
-		
- +
+ +
\ No newline at end of file diff --git a/MCServer/Plugins/APIDump/Writing-a-MCServer-plugin.html b/MCServer/Plugins/APIDump/Writing-a-MCServer-plugin.html new file mode 100644 index 000000000..3ab997dcd --- /dev/null +++ b/MCServer/Plugins/APIDump/Writing-a-MCServer-plugin.html @@ -0,0 +1,253 @@ + + + + + + MCS Plugin Tutorial + + + + + + +
+

Writing a MCServer plugin

+

+ This article will explain how to write a basic plugin. It details basic requirements + for a plugin, explains how to register a hook and bind a command, and gives plugin + standards details. +

+

+ Let us begin. In order to begin development, we must firstly obtain a compiled copy + of MCServer, and make sure that the Core plugin is within the Plugins folder, and activated. + Core handles much of the MCServer end-user experience and is a necessary component of + plugin development, as necessary plugin components depend on sone of its functions. +

+

+ Next, we must obtain a copy of CoreMessaging.lua. This can be found + here. + This is used to provide messaging support that is compliant with MCServer standards. +

+

Creating the basic template

+

+ Plugins are written in Lua. Therefore, create a new Lua file. You can create as many files as you wish, with + any filename - MCServer bungs them all together at runtime, however, let us create a file called main.lua for now. + Format it like so: +

+
+			local PLUGIN
+			
+			function Initialize( Plugin )
+				Plugin:SetName( "DerpyPlugin" )
+				Plugin:SetVersion( 1 )
+				
+				PLUGIN = Plugin
+
+				-- Hooks
+		
+				local PluginManager = cPluginManager:Get()
+				-- Command bindings
+
+				LOG( "Initialised " .. Plugin:GetName() .. " v." .. Plugin:GetVersion() )
+				return true
+			end
+			
+			function OnDisable()
+				LOG(PLUGIN:GetName() .. " is shutting down...")
+			end
+			
+

+ Now for an explanation of the basics. +

    +
  • function Initialize is called on plugin startup. It is the place where the plugin is set up.
  • +
  • Plugin:SetName sets the name of the plugin.
  • +
  • Plugin:SetVersion sets the revision number of the plugin. This must be an integer.
  • +
  • LOG logs to console a message, in this case, it prints that the plugin was initialised.
  • +
  • The PLUGIN variable just stores this plugin's object, so GetName() can be called in OnDisable (as no Plugin parameter is passed there, contrary to Initialize).
  • +
  • function OnDisable is called when the plugin is disabled, commonly when the server is shutting down. Perform cleanup and logging here.
  • +
+ Be sure to return true for this function, else MCS thinks you plugin had failed to initialise and prints a stacktrace with an error message. +

+ +

Registering hooks

+

+ Hooks are things that MCServer calls when an internal event occurs. For example, a hook is fired when a player places a block, moves, + logs on, eats, and many other things. For a full list, see the API documentation. +

+

+ A hook can be either informative or overridable. In any case, returning false will not trigger a response, but returning true will cancel + the hook and prevent it from being propagated further to other plugins. An overridable hook simply means that there is visible behaviour + to a hook's cancellation, such as a chest being prevented from being opened. There are some exceptions to this where only changing the value the + hook passes has an effect, and not the actual return value, an example being the HOOK_KILLING hook. See the API docs for details. +

+

+ To register a hook, insert the following code template into the "-- Hooks" area in the previous code example. +

+
+				cPluginManager.AddHook(cPluginManager.HOOK_NAME_HERE, FunctionNameToBeCalled)
+			
+

+ What does this code do? +

    +
  • cPluginManager.AddHook registers the hook. The hook name is the second parameter. See the previous API documentation link for a list of all hooks.
  • +
+ What about the third parameter, you ask? Well, it is the name of the function that MCServer calls when the hook fires. It is in this + function that you should handle or cancel the hook. +

+

+ So in total, this is a working representation of what we have so far covered. +

+
+			function Initialize( Plugin )
+				Plugin:SetName( "DerpyPlugin" )
+				Plugin:SetVersion( 1 )
+
+				cPluginManager.AddHook(cPluginManager.HOOK_PLAYER_MOVING, OnPlayerMoving)
+		
+				local PluginManager = cPluginManager:Get()
+				-- Command bindings
+
+				LOG( "Initialised " .. Plugin:GetName() .. " v." .. Plugin:GetVersion() )
+				return true
+			end
+			
+			function OnPlayerMoving(Player) -- See API docs for parameters of all hooks
+				return true -- Prohibit player movement, see docs for whether a hook is cancellable
+			end
+			
+

+ So, that code stops the player from moving. Not particularly helpful, but yes :P. Note that ALL documentation is available + on the main API docs page, so if ever in doubt, go there. +

+

Binding a command

+

Format

+

+ So now we know how to hook into MCServer, how do we bind a command, such as /explode, for a player to type? That is more complicated. + We firstly add this template to the "-- Command bindings" section of the initial example: +

+
+				-- ADD THIS IF COMMAND DOES NOT REQUIRE A PARAMETER (/explode)
+				PluginManager:BindCommand("/commandname", "permissionnode", FunctionToCall, " - Description of command")
+				
+				-- ADD THIS IF COMMAND DOES REQUIRE A PARAMETER (/explode Notch)
+				PluginManager:BindCommand("/commandname", "permissionnode", FunctionToCall, " ~ Description of command and parameter(s)")
+			
+

+ What does it do, and why are there two? +

    +
  • PluginManager:BindCommand binds a command. It takes the command name (with a slash), the permission a player needs to execute the command, the function + to call when the command is executed, and a description of the command.
  • +
+ The command name is pretty self explanatory. The permission node is basically just a string that the player's group needs to have, so you can have anything in there, + though we recommend a style such as "derpyplugin.explode". The function to call is like the ones with Hooks, but with some fixed parameters which we will come on to later, + and the description is a description of the command which is shown when "/help" is typed. +

+

+ So why are there two? Standards. A plugin that accepts a parameter MUST use a format for the description of " ~ Description of command and parms" + whereas a command that doesn't accept parameters MUST use " - Description of command" instead. Be sure to put a space before the tildes or dashes. + Additionally, try to keep the description brief and on one line on the client. +

+

Parameters

+

+ What parameters are in the function MCServer calls when the command is executed? A 'Split' array and a 'Player' object. +

+

The Split Array

+

+ The Split array is an array of all text submitted to the server, including the actual command. MCServer automatically splits the text into the array, + so plugin authors do not need to worry about that. An example of a Split array passed for the command, "/derp zubby explode" would be:

+    /derp (Split[1])
+    zubby (Split[2])
+    explode (Split[3])
+
+    The total amount of parameters passed were: 3 (#Split) +

+

The Player Object and sending them messages

+

+ The Player object is basically a pointer to the player that has executed the command. You can do things with them, but most common is sending + a message. Again, see the API documentation for fuller details. But, you ask, how do we send a message to the client? +

+

+ Remember that copy of CoreMessaging.lua that we downloaded earlier? Make sure that file is in your plugin folder, along with the main.lua file you are typing + your code in. Since MCS brings all the files together on JIT compile, we don't need to worry about requiring any files or such. Simply follow the below examples: +

+
+				-- Format: §yellow[INFO] §white%text% (yellow [INFO], white text following it)
+				-- Use: Informational message, such as instructions for usage of a command
+				SendMessage(Player, "Usage: /explode [player]")
+				
+				-- Format: §green[INFO] §white%text% (green [INFO] etc.)
+				-- Use: Success message, like when a command executes successfully
+				SendMessageSuccess(Player, "Notch was blown up!")
+				
+				-- Format: §rose[INFO] §white%text% (rose coloured [INFO] etc.)
+				-- Use: Failure message, like when a command was entered correctly but failed to run, such as when the destination player wasn't found in a /tp command
+				SendMessageFailure(Player, "Player Salted was not found")
+			
+

+ Those are the basics. If you want to output text to the player for a reason other than the three listed above, and you want to colour the text, simply concatenate + "cChatColor.*colorhere*" with your desired text, concatenate being "..". See the API docs for more details of all colours, as well as details on logging to console with + LOG("Text"). +

+

Final example and conclusion

+

+ So, a working example that checks the validity of a command, and blows up a player, and also refuses pickup collection to players with >100ms ping. +

+
+			function Initialize( Plugin )
+				Plugin:SetName( "DerpyPluginThatBlowsPeopleUp" )
+				Plugin:SetVersion( 9001 )
+		
+				local PluginManager = cPluginManager:Get()
+				PluginManager:BindCommand("/explode", "derpyplugin.explode", Explode, " ~ Explode a player");
+
+				cPluginManager.AddHook(cPluginManager.HOOK_COLLECTING_PICKUP, OnCollectingPickup)
+
+				LOG( "Initialised " .. Plugin:GetName() .. " v." .. Plugin:GetVersion() )
+				return true
+			end
+			
+			function Explode(Split, Player)
+				if #Split ~= 2
+					SendMessage(Player, "Usage: /explode [playername]") -- There was more or less than one argument (excluding the /explode bit)
+				else
+					local ExplodePlayer = function(Explodee) -- Create a callback ExplodePlayer with parameter Explodee, which MCS calls for every player on the server
+						if (Explodee:GetName() == Split[2] then -- If the player we are currently at is the one we specified as the parameter...
+							Player:GetWorld():DoExplosionAt(Explodee:GetPosX(), Explodee:GetPosY(), Explodee:GetPosZ(), false, esPlugin) -- Explode 'em; see API docs for further details of this function
+							SendMessageSuccess(Player, Split[2] .. " was successfully exploded") -- Success!
+							return true -- Break out
+						end
+					end
+					
+					cRoot:Get():FindAndDoWithPlayer(Split[2], ExplodePlayer) -- Tells MCS to loop through all players and call the callback above with the Player object it has found
+					
+					SendMessageFailure(Player, Split[2] .. " was not found") -- We have not broken out so far, therefore, the player must not exist, send failure
+				end
+				
+				return true -- Concluding return
+			end
+			
+			function OnCollectingPickup(Player, Pickup) -- Again, see the API docs for parameters of all hooks. In this case, it is a Player and Pickup object
+				if (Player:GetClientHandle():GetPing() > 100) then -- Get ping of player, in milliseconds
+					return true -- Discriminate against high latency - you don't get drops :D
+				else
+					return false -- You do get the drops! Yay~
+				end
+			end
+			
+

+ Make sure to read the comments for a description of what everything does. Also be sure to return true for all command handlers, unless you want MCS to print out an "Unknown command" message + when the command gets executed :P. Make sure to follow standards - use CoreMessaging.lua functions for messaging, dashes for no parameter commands and tildes for vice versa, + and finally, the API documentation is your friend! +

+

+ Happy coding ;) +

+ + +
+
+ + + diff --git a/MCServer/Plugins/APIDump/main.css b/MCServer/Plugins/APIDump/main.css index 5cc603a3f..797079873 100644 --- a/MCServer/Plugins/APIDump/main.css +++ b/MCServer/Plugins/APIDump/main.css @@ -49,6 +49,12 @@ header font-family: Segoe UI Light, Helvetica; } +footer +{ + text-align: center; + font-family: Segoe UI Light, Helvetica; +} + #content { padding: 0px 25px 25px 25px;