1
0

Merge branch 'master' into Enchanting

This commit is contained in:
daniel0916 2014-04-19 20:56:29 +02:00
commit cb90029f72
10 changed files with 377 additions and 29 deletions

View File

@ -2929,9 +2929,10 @@ 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 = "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" },
}
} ;

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,116 @@
#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
#include "BeaconEntity.h"
#include "../BlockArea.h"
cBeaconEntity::cBeaconEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World) :
super(E_BLOCK_BEACON, a_BlockX, a_BlockY, a_BlockZ, a_World)
{
}
int cBeaconEntity::GetPyramidLevel(void)
{
cBlockArea Area;
int MinY = GetPosY() - 4;
if (MinY < 0)
{
MinY = 0;
}
int MaxY = GetPosY() - 1;
if (MaxY < 0)
{
MaxY = 0;
}
Area.Read(
m_World,
GetPosX() - 4, GetPosX() + 4,
MinY, MaxY,
GetPosZ() - 4, GetPosZ() + 4,
cBlockArea::baTypes
);
int Layer = 1;
int MiddleXZ = 4;
for (int Y = Area.GetSizeY() - 1; Y > 0; Y--)
{
for (int X = MiddleXZ - Layer; X <= (MiddleXZ + Layer); X++)
{
for (int Z = MiddleXZ - Layer; Z <= (MiddleXZ + Layer); Z++)
{
if (!IsMineralBlock(Area.GetRelBlockType(X, Y, Z)))
{
return Layer - 1;
}
}
}
Layer++;
}
return Layer - 1;
}
bool cBeaconEntity::IsMineralBlock(BLOCKTYPE a_BlockType)
{
switch(a_BlockType)
{
case E_BLOCK_DIAMOND_BLOCK:
case E_BLOCK_GOLD_BLOCK:
case E_BLOCK_IRON_BLOCK:
case E_BLOCK_EMERALD_BLOCK:
{
return true;
}
}
return false;
}
bool cBeaconEntity::Tick(float a_Dt, cChunk & a_Chunk)
{
return false;
}
void cBeaconEntity::SaveToJson(Json::Value& a_Value)
{
}
void cBeaconEntity::SendTo(cClientHandle & a_Client)
{
}
void cBeaconEntity::UsedBy(cPlayer * a_Player)
{
}

View File

@ -0,0 +1,44 @@
#pragma once
#include "BlockEntity.h"
namespace Json
{
class Value;
}
class cBeaconEntity :
public cBlockEntity
{
typedef cBlockEntity super;
public:
/** The initial constructor */
cBeaconEntity(int a_BlockX, int a_BlockY, int a_BlockZ, cWorld * a_World);
/** Returns the amount of layers the pyramid below the beacon has. */
int GetPyramidLevel(void);
/** Returns true if the block is a diamond block, a golden block, an iron block or an emerald block. */
static bool IsMineralBlock(BLOCKTYPE a_BlockType);
// cBlockEntity overrides:
virtual void SaveToJson(Json::Value& a_Value ) override;
virtual void SendTo(cClientHandle & a_Client) override;
virtual void UsedBy(cPlayer * a_Player) override;
virtual bool Tick(float a_Dt, cChunk & /* a_Chunk */) override;
} ;

View File

@ -4,6 +4,7 @@
// Implements the cBlockEntity class that is the common ancestor for all block entities
#include "Globals.h"
#include "BeaconEntity.h"
#include "BlockEntity.h"
#include "ChestEntity.h"
#include "CommandBlockEntity.h"
@ -26,6 +27,7 @@ cBlockEntity * cBlockEntity::CreateByBlockType(BLOCKTYPE a_BlockType, NIBBLETYPE
{
switch (a_BlockType)
{
case E_BLOCK_BEACON: return new cBeaconEntity (a_BlockX, a_BlockY, a_BlockZ, a_World);
case E_BLOCK_CHEST: return new cChestEntity (a_BlockX, a_BlockY, a_BlockZ, a_World);
case E_BLOCK_COMMAND_BLOCK: return new cCommandBlockEntity(a_BlockX, a_BlockY, a_BlockZ, a_World);
case E_BLOCK_DISPENSER: return new cDispenserEntity (a_BlockX, a_BlockY, a_BlockZ, a_World);

View File

@ -1299,6 +1299,7 @@ void cChunk::CreateBlockEntities(void)
BLOCKTYPE BlockType = cChunkDef::GetBlock(m_BlockTypes, x, y, z);
switch (BlockType)
{
case E_BLOCK_BEACON:
case E_BLOCK_CHEST:
case E_BLOCK_COMMAND_BLOCK:
case E_BLOCK_DISPENSER:
@ -1429,6 +1430,7 @@ void cChunk::SetBlock(int a_RelX, int a_RelY, int a_RelZ, BLOCKTYPE a_BlockType,
// If the new block is a block entity, create the entity object:
switch (a_BlockType)
{
case E_BLOCK_BEACON:
case E_BLOCK_CHEST:
case E_BLOCK_COMMAND_BLOCK:
case E_BLOCK_DISPENSER:

View File

@ -771,6 +771,16 @@ void cEntity::TickBurning(cChunk & a_Chunk)
{
// Remember the current burning state:
bool HasBeenBurning = (m_TicksLeftBurning > 0);
if (GetWorld()->GetWeather() == eWeather_Rain)
{
if (HasBeenBurning)
{
m_TicksLeftBurning = 0;
OnFinishedBurning();
}
return;
}
// Do the burning damage:
if (m_TicksLeftBurning > 0)

View File

@ -540,7 +540,7 @@ void cMonster::KilledBy(cEntity * a_Killer)
break;
}
}
if (a_Killer != NULL)
if ((a_Killer != NULL) && (!IsBaby()))
{
m_World->SpawnExperienceOrb(GetPosX(), GetPosY(), GetPosZ(), Reward);
}
@ -1003,7 +1003,8 @@ void cMonster::HandleDaylightBurning(cChunk & a_Chunk)
(a_Chunk.GetSkyLight(RelX, RelY, RelZ) == 15) && // In the daylight
(a_Chunk.GetBlock(RelX, RelY, RelZ) != E_BLOCK_SOULSAND) && // Not on soulsand
(GetWorld()->GetTimeOfDay() < (12000 + 1000)) && // It is nighttime
!IsOnFire() // Not already burning
!IsOnFire() && // Not already burning
(GetWorld()->GetWeather() != eWeather_Rain) // Not raining
)
{
// Burn for 100 ticks, then decide again

View File

@ -1691,6 +1691,11 @@ int cWorld::SpawnFallingBlock(int a_X, int a_Y, int a_Z, BLOCKTYPE BlockType, NI
int cWorld::SpawnExperienceOrb(double a_X, double a_Y, double a_Z, int a_Reward)
{
if (a_Reward < 1)
{
return -1;
}
cExpOrb * ExpOrb = new cExpOrb(a_X, a_Y, a_Z, a_Reward);
ExpOrb->Initialize(this);
return ExpOrb->GetUniqueID();