summaryrefslogblamecommitdiffstats
path: root/Server/Plugins/Debuggers/Debuggers.lua
blob: 1505904a86f7d53f0f1b4c42b80e2e90f726633b (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
 
                   
                                                                                                                                         
                        
                                                                                                
 




 
                             




                                                                                          
 







                                                                                                  
                                                                                      

                                                                                           

                                                                                       
                                                                                           
                                                                                        
 

                                                                                         


                                                                                          

                                                                 
 

                                     
 

                                            
 

                                                                  
 

                                                                                 
 


                               
 
                              
                          

                             
                     
                            








                                                                                                                 
 





                                                                                                       
                                                                                                                             



                                   
 


                   



 

                                                                              
                                                                               
                                                                                      
                                                                                       
                                                                                    
                                                                                     
                                                                                        
                                                                                         
                                                                                            




                                                                                                 





   
                          


                                                                                                        











                                                                                                          

                                      
 













                                                                                    
 










                                                                                                                       
 



                                                                       
 




                                                                       
 





























                                                                        
 



                                                                        
 




                                                                        
 



                                      
 


 











                                                                                   
                                                                  

                     
 






                                                                                 
 
                                                        
                                                            









                                                        





                                       
 


                                             
 






                             

                                                   
 







                                                                                
                                                                     
 








                                                                          
 







                                                                                                         



















                                                                              
 










                                                                          
 





                                                                                                     





   











                                                                    

                                          
 



























                                                                                                                   
 








                                          
 





















                                                            
 








                                                                                                
                                                                                             






                                                                                                                                                                                                 
 
                                                                            
                                                                        























                                                                                                                                                                                  
 


                                                                                                                  
 


                                                                                                                  
 


                                                                                                                   
 


                                                                                                                   
 


                                                                                                                  
 
































                                                                                                                             
                                           






























































































                                                                                                                                                 
                                                                                                                                                                                                                                     






                     

















                                                        























                                                                                                    

 




                                                                    
 






                     
                                   
                                             






                                                                   

                                                                          










                                                                                

                                             





   



















                                                                                                           


                                                                                                                                        





   







                                  


                                                                   
 



                                                                             





                                                                                                                   





   










                                                                     
                                     
                                 
                                                                


                            
                                       







                                                                                            

                                             
 




                                                                               






                                                                                                                                                                                                          

                                              
 











                                                                                   
 






                                                                    
 
























                                                                     
                                             









                                                                                             
 






                                                                                                                     
 



                                                                                                    
 














                                                                                                    
 
                                    
 


                                                                               
 













































                                                                                    

















                                                                                                 




 





















                                                                                                                     





















                                                                                                          





















                                                                                                                                           




































                                                                                                         
 
                                                    


                                           
                                                                  


                                                                                





                    
 
























                                                                                         
























                                                                                            

























                                                                                  












                                                                                                                 
 


                                                    
 


                                                       
 
                                                                                                 
 





                    

                                         
                                                                    



                                 
                    







                                               












                                               
 

                                                                                                     




   






                                               
 






                                                                                                             
                                               
                                      

                    
   





                                          
                                         
 

                    






































                                                                                                              


                                                    
                                                                   















                                                                                                               

















                                                                                                  
 

                                         
 







                                                                              
 
                                                                                         
                                                                                                  
















                                                                                                                                   
 






                   
                                        

                                   



                                                                     
                               





                                                                                

                                        








                                                                                          


                                                                               






                   







                                                                                                                                                                    
 





























































                                                                                                                      

                                                                                                                                                        
 















                                                                                                                                                          

   
 
 
 

                                           


                                                                                                              
 
                               
                                                                        

                           
 


                                                    
                                                                    

                           
 

                                               
                                                                
 

                                 
                                         
                                                              
               
 
                                                                    

                                                                                                                            
                                               
                                                             







                                                                                      
 



                                                                                                       
                                                                                                

                           
           
 
                                                                             
                                                                                                                            



                                                                                           
                                                                                                                                    


                           
 

                                                                       





                   
 


















                                                                    








                                                                                                                       

















































                                                                                                               



                                                         
                                                          


                                                           
 




                                                                                             
 





                   























                                                                                                       
 













                                                                                                           
 









                                                                                                                                                      
 






                                                                                                                                                                                   
 


                                                                          
 
                             
                                                                                                           











                                                                                                                                                                               



                                             
                                                                              







                                                                      

                                                                                                     
                                                                        

                                                                                                  
 





                                                               
























                                                                                                            











                                                                                                                    





                              
                                                               











                                          
                        
                                               

                                                  

                                                                     
                                                                 
                                
                   




   










                                                                          
                                      



                                                                                            

                                                   




                                                                                                                                         

                   

                                                                      




   
 





























                                                                                                




   
 





































                                                                                                             
















                                                                              
 












                                                                                                
 












                                                                                            
 
















                                                                                                                 
 


                                                         
 






                        





                                                                                          
 



















                                                                                     
 












                                                                                   

































                                                                                           











                                                                                                                                                                     





                                                                                          
 



















                                                                                     
 





                                                                                           





                   


                                       

                                                                                                 











                                                                                                                                 
 





























                                                                                      




















                                                                                                                               
 















                                                                                                                                         
 






                   


                                                                      
 













                                                                       
 







                                                                                                            
 




























                                                                                            











                                                                       

                                                    
                                                                                    




                              


                                                


                                                                
                                                                              
 









                                                                                            
                                          
 
                                          






                                                                                                                      
                                              
 






                   












                                                                                                                     
 









                                                     
 








































                                                                                                                                              
 













                                                                                                                                  




























                                                                                                                           







                                                                                                      





                                                                    


























                                                                                                         





                                                       
 







                                                                             
 






                                                                              
 






                   




                                                      
 




                                            
 
















                                               
 




                                               
 




                                               
 




                                               
 





                   
 



























                                                                                                                             






                                                                                   
 






                                                                                                  
 












                                                                                                                                        
 






                                                                               
 











                                                                                











                                                                                                                                                                    
 


                                                    
 
                                             
 
                                                                                                    
 





                    
 











                                                                                                     











                                                                                        

-- Global variables
g_DropSpensersToActivate = {};  -- A list of dispensers and droppers (as {World, X, Y Z} quadruplets) that are to be activated every tick
g_HungerReportTick = 10;
g_ShowFoodStats = false;  -- When true, each player's food stats are sent to them every 10 ticks






function Initialize(a_Plugin)
	--[[
	-- Test multiple hook handlers:
	cPluginManager.AddHook(cPluginManager.HOOK_TICK,                         OnTick1);
	cPluginManager.AddHook(cPluginManager.HOOK_TICK,                         OnTick2);
	--]]

	local PM = cPluginManager;
	PM:AddHook(cPluginManager.HOOK_PLAYER_USING_BLOCK,           OnPlayerUsingBlock);
	PM:AddHook(cPluginManager.HOOK_PLAYER_USING_ITEM,            OnPlayerUsingItem);
	PM:AddHook(cPluginManager.HOOK_TAKE_DAMAGE,                  OnTakeDamage);
	PM:AddHook(cPluginManager.HOOK_TICK,                         OnTick);
	PM:AddHook(cPluginManager.HOOK_CHAT,                         OnChat);
	PM:AddHook(cPluginManager.HOOK_PLAYER_RIGHT_CLICKING_ENTITY, OnPlayerRightClickingEntity);
	PM:AddHook(cPluginManager.HOOK_WORLD_TICK,                   OnWorldTick);
	PM:AddHook(cPluginManager.HOOK_PLUGINS_LOADED,               OnPluginsLoaded);
	PM:AddHook(cPluginManager.HOOK_PLAYER_JOINED,                OnPlayerJoined);
	PM:AddHook(cPluginManager.HOOK_PROJECTILE_HIT_BLOCK,         OnProjectileHitBlock);
	PM:AddHook(cPluginManager.HOOK_CHUNK_UNLOADING,              OnChunkUnloading);
	PM:AddHook(cPluginManager.HOOK_WORLD_STARTED,                OnWorldStarted);
	PM:AddHook(cPluginManager.HOOK_PROJECTILE_HIT_BLOCK,         OnProjectileHitBlock);
	PM:AddHook(cPluginManager.HOOK_PLAYER_RIGHT_CLICK,           OnPlayerRightClick)

	-- _X: Disabled WECUI manipulation:
	-- PM:AddHook(cPluginManager.HOOK_PLUGIN_MESSAGE,               OnPluginMessage);
	-- _X: Disabled so that the normal operation doesn't interfere with anything
	-- PM:AddHook(cPluginManager.HOOK_CHUNK_GENERATED,              OnChunkGenerated);

	-- Load the InfoReg shared library:
	dofile(cPluginManager:GetPluginsPath() .. "/InfoReg.lua")

	-- Bind all the commands:
	RegisterPluginInfoCommands();

	-- Bind all the console commands:
	RegisterPluginInfoConsoleCommands();

	a_Plugin:AddWebTab("Debuggers",  HandleRequest_Debuggers)
	a_Plugin:AddWebTab("StressTest", HandleRequest_StressTest)

	-- Enable the following line for BlockArea / Generator interface testing:
	-- PluginManager:AddHook(Plugin, cPluginManager.HOOK_CHUNK_GENERATED);

	-- TestBlockAreas()
	-- TestSQLiteBindings()
	-- TestExpatBindings()

	TestBlockAreasString()
	TestStringBase64()
	-- TestUUIDFromName()
	-- TestRankMgr()
	TestFileExt()
	-- TestFileLastMod()

	--[[
	-- Test cCompositeChat usage in console-logging:
	LOGINFO(cCompositeChat("This is a simple message with some @2 color formatting @4 and http://links.to .")
		:AddSuggestCommandPart("(Suggested command)", "cmd")
		:AddRunCommandPart("(Run command)", "cmd")
		:SetMessageType(mtInfo)
	)
	--]]

	-- Test the crash in #1889:
	cPluginManager:AddHook(cPluginManager.HOOK_PLAYER_RIGHT_CLICKING_ENTITY,
		function (a_CBPlayer, a_CBEntity)
			a_CBPlayer:GetWorld():DoWithEntityByID(  -- This will crash the server in #1889
				a_CBEntity:GetUniqueID(),
				function(Entity)
					LOG("RightClicking an entity, crash #1889 fixed. Entity is a " .. tolua.type(Entity))
				end
			)
		end
	)

	return true
end;





function TestFileExt()
	assert(cFile:ChangeFileExt("fileless_dir/", "new") == "fileless_dir/")
	assert(cFile:ChangeFileExt("fileless_dir/", ".new") == "fileless_dir/")
	assert(cFile:ChangeFileExt("pathless_file.ext", "new") == "pathless_file.new")
	assert(cFile:ChangeFileExt("pathless_file.ext", ".new") == "pathless_file.new")
	assert(cFile:ChangeFileExt("path/to/file.ext", "new") == "path/to/file.new")
	assert(cFile:ChangeFileExt("path/to/file.ext", ".new") == "path/to/file.new")
	assert(cFile:ChangeFileExt("path/to.dir/file", "new") == "path/to.dir/file.new")
	assert(cFile:ChangeFileExt("path/to.dir/file", ".new") == "path/to.dir/file.new")
	assert(cFile:ChangeFileExt("path/to.dir/file.ext", "new") == "path/to.dir/file.new")
	assert(cFile:ChangeFileExt("path/to.dir/file.ext", ".new") == "path/to.dir/file.new")
	assert(cFile:ChangeFileExt("path/to.dir/file.longext", "new") == "path/to.dir/file.new")
	assert(cFile:ChangeFileExt("path/to.dir/file.longext", ".new") == "path/to.dir/file.new")
	assert(cFile:ChangeFileExt("path/to.dir/file.", "new") == "path/to.dir/file.new")
	assert(cFile:ChangeFileExt("path/to.dir/file.", ".new") == "path/to.dir/file.new")
end





function TestFileLastMod()
	local LastSelfMod = cFile:GetLastModificationTime(a_Plugin:GetLocalFolder() .. "/Debuggers.lua")
	LOG("Debuggers.lua last modified on " .. os.date("%Y-%m-%dT%H:%M:%S", LastSelfMod))

	local f = assert(io.open("test.txt", "w"))
	f:write("test")
	f:close()
	local filetime = cFile:GetLastModificationTime("test.txt")
	local ostime = os.time()
	LOG("file time: " .. filetime .. ", OS time: " .. ostime .. ", difference: " .. ostime - filetime)
end





function TestBlockAreas()
	LOG("Testing block areas...");

	-- Debug block area merging:
	local BA1 = cBlockArea();
	local BA2 = cBlockArea();
	if (BA1:LoadFromSchematicFile("schematics/test.schematic")) then
		if (BA2:LoadFromSchematicFile("schematics/fountain.schematic")) then
			BA2:SetRelBlockType(0, 0, 0, E_BLOCK_LAPIS_BLOCK);
			BA2:SetRelBlockType(1, 0, 0, E_BLOCK_LAPIS_BLOCK);
			BA2:SetRelBlockType(2, 0, 0, E_BLOCK_LAPIS_BLOCK);
			BA1:Merge(BA2, 1, 10, 1, cBlockArea.msImprint);
			BA1:SaveToSchematicFile("schematics/merge.schematic");
		end
	else
		BA1:Create(16, 16, 16);
	end

	-- Debug block area cuboid filling:
	BA1:FillRelCuboid(2, 9, 2, 8, 2, 8, cBlockArea.baTypes, E_BLOCK_GOLD_BLOCK);
	BA1:RelLine(2, 2, 2, 9, 8, 8, cBlockArea.baTypes or cBlockArea.baMetas, E_BLOCK_SAPLING, E_META_SAPLING_BIRCH);
	BA1:SaveToSchematicFile("schematics/fillrel.schematic");

	-- Debug block area mirroring:
	if (BA1:LoadFromSchematicFile("schematics/lt.schematic")) then
		BA1:MirrorXYNoMeta();
		BA1:SaveToSchematicFile("schematics/lt_XY.schematic");
		BA1:MirrorXYNoMeta();
		BA1:SaveToSchematicFile("schematics/lt_XY2.schematic");

		BA1:MirrorXZNoMeta();
		BA1:SaveToSchematicFile("schematics/lt_XZ.schematic");
		BA1:MirrorXZNoMeta();
		BA1:SaveToSchematicFile("schematics/lt_XZ2.schematic");

		BA1:MirrorYZNoMeta();
		BA1:SaveToSchematicFile("schematics/lt_YZ.schematic");
		BA1:MirrorYZNoMeta();
		BA1:SaveToSchematicFile("schematics/lt_YZ2.schematic");
	end

	-- Debug block area rotation:
	if (BA1:LoadFromSchematicFile("schematics/rot.schematic")) then
		BA1:RotateCWNoMeta();
		BA1:SaveToSchematicFile("schematics/rot1.schematic");
		BA1:RotateCWNoMeta();
		BA1:SaveToSchematicFile("schematics/rot2.schematic");
		BA1:RotateCWNoMeta();
		BA1:SaveToSchematicFile("schematics/rot3.schematic");
		BA1:RotateCWNoMeta();
		BA1:SaveToSchematicFile("schematics/rot4.schematic");
	end

	-- Debug block area rotation:
	if (BA1:LoadFromSchematicFile("schematics/rotm.schematic")) then
		BA1:RotateCCW();
		BA1:SaveToSchematicFile("schematics/rotm1.schematic");
		BA1:RotateCCW();
		BA1:SaveToSchematicFile("schematics/rotm2.schematic");
		BA1:RotateCCW();
		BA1:SaveToSchematicFile("schematics/rotm3.schematic");
		BA1:RotateCCW();
		BA1:SaveToSchematicFile("schematics/rotm4.schematic");
	end

	-- Debug block area mirroring:
	if (BA1:LoadFromSchematicFile("schematics/ltm.schematic")) then
		BA1:MirrorXY();
		BA1:SaveToSchematicFile("schematics/ltm_XY.schematic");
		BA1:MirrorXY();
		BA1:SaveToSchematicFile("schematics/ltm_XY2.schematic");

		BA1:MirrorXZ();
		BA1:SaveToSchematicFile("schematics/ltm_XZ.schematic");
		BA1:MirrorXZ();
		BA1:SaveToSchematicFile("schematics/ltm_XZ2.schematic");

		BA1:MirrorYZ();
		BA1:SaveToSchematicFile("schematics/ltm_YZ.schematic");
		BA1:MirrorYZ();
		BA1:SaveToSchematicFile("schematics/ltm_YZ2.schematic");
	end

	LOG("Block areas test ended");
end






function TestBlockAreasString()
	-- Write one area to string, then to file:
	local BA1 = cBlockArea()
	BA1:Create(5, 5, 5, cBlockArea.baTypes + cBlockArea.baMetas)
	BA1:Fill(cBlockArea.baTypes, E_BLOCK_DIAMOND_BLOCK)
	BA1:FillRelCuboid(1, 3, 1, 3, 1, 3, cBlockArea.baTypes, E_BLOCK_GOLD_BLOCK)
	local Data = BA1:SaveToSchematicString()
	if ((type(Data) ~= "string") or (Data == "")) then
		LOG("Cannot save schematic to string")
		return
	end
	cFile:CreateFolder("schematics")
	local f = io.open("schematics/StringTest.schematic", "wb")
	f:write(Data)
	f:close()

	-- Load a second area from that file:
	local BA2 = cBlockArea()
	if not(BA2:LoadFromSchematicFile("schematics/StringTest.schematic")) then
		LOG("Cannot read schematic from string test file")
		return
	end
	BA2:Clear()

	-- Load another area from a string in that file:
	f = io.open("schematics/StringTest.schematic", "rb")
	Data = f:read("*all")
	if not(BA2:LoadFromSchematicString(Data)) then
		LOG("Cannot load schematic from string")
	end
end





function TestStringBase64()
	-- Create a binary string:
	local s = ""
	for i = 0, 255 do
		s = s .. string.char(i)
	end

	-- Roundtrip through Base64:
	local Base64 = Base64Encode(s)
	local UnBase64 = Base64Decode(Base64)

	assert(UnBase64 == s)
end





function TestUUIDFromName()
	LOG("Testing UUID-from-Name resolution...")

	-- Test by querying a few existing names, along with a non-existent one:
	local PlayerNames =
	{
		"xoft",
		"aloe_vera",
		"nonexistent_player",
	}
	-- WARNING: Blocking operation! DO NOT USE IN TICK THREAD!
	local UUIDs = cMojangAPI:GetUUIDsFromPlayerNames(PlayerNames)

	-- Log the results:
	for _, name in ipairs(PlayerNames) do
		local UUID = UUIDs[name]
		if (UUID == nil) then
			LOG("  UUID(" .. name .. ") not found.")
		else
			LOG("  UUID(" .. name .. ") = \"" .. UUID .. "\"")
		end
	end

	-- Test once more with the same players, valid-only. This should go directly from cache, so fast.
	LOG("Testing again with the same valid players...")
	local ValidPlayerNames =
	{
		"xoft",
		"aloe_vera",
	}
	UUIDs = cMojangAPI:GetUUIDsFromPlayerNames(ValidPlayerNames);

	-- Log the results:
	for _, name in ipairs(ValidPlayerNames) do
		local UUID = UUIDs[name]
		if (UUID == nil) then
			LOG("  UUID(" .. name .. ") not found.")
		else
			LOG("  UUID(" .. name .. ") = \"" .. UUID .. "\"")
		end
	end

	-- Test yet again, cache-only:
	LOG("Testing once more, cache only...")
	local PlayerNames3 =
	{
		"xoft",
		"aloe_vera",
		"notch",  -- Valid player name, but not cached (most likely :)
	}
	UUIDs = cMojangAPI:GetUUIDsFromPlayerNames(PlayerNames3, true)

	-- Log the results:
	for _, name in ipairs(PlayerNames3) do
		local UUID = UUIDs[name]
		if (UUID == nil) then
			LOG("  UUID(" .. name .. ") not found.")
		else
			LOG("  UUID(" .. name .. ") = \"" .. UUID .. "\"")
		end
	end

	LOG("UUID-from-Name resolution tests finished.")

	LOG("Performing a Name-from-UUID test...")
	-- local NameToTest = "aloe_vera"
	local NameToTest = "xoft"
	local Name = cMojangAPI:GetPlayerNameFromUUID(UUIDs[NameToTest])
	LOG("Name(" .. UUIDs[NameToTest] .. ") = '" .. Name .. "', expected '" .. NameToTest .. "'.")
	LOG("Name-from-UUID test finished.")
end





function TestRankMgr()
	LOG("Testing the rank manager")
	cRankManager:AddRank("LuaRank")
	cRankManager:AddGroup("LuaTestGroup")
	cRankManager:AddGroupToRank("LuaTestGroup", "LuaRank")
	cRankManager:AddPermissionToGroup("luaperm", "LuaTestGroup")
end





function TestSQLiteBindings()
	LOG("Testing SQLite bindings...");

	-- Debug SQLite binding
	local TestDB, ErrCode, ErrMsg = sqlite3.open("test.sqlite");
	if (TestDB ~= nil) then
		local function ShowRow(UserData, NumCols, Values, Names)
			assert(UserData == 'UserData');
			LOG("New row");
			for i = 1, NumCols do
				LOG("  " .. Names[i] .. " = " .. Values[i]);
			end
			return 0;
		end
		local sql = [=[
			CREATE TABLE numbers(num1,num2,str);
			INSERT INTO numbers VALUES(1, 11, "ABC");
			INSERT INTO numbers VALUES(2, 22, "DEF");
			INSERT INTO numbers VALUES(3, 33, "UVW");
			INSERT INTO numbers VALUES(4, 44, "XYZ");
			SELECT * FROM numbers;
		]=]
		local Res = TestDB:exec(sql, ShowRow, 'UserData');
		if (Res ~= sqlite3.OK) then
			LOG("TestDB:exec() failed: " .. Res .. " (" .. TestDB:errmsg() .. ")");
		end;
		TestDB:close();
	else
		-- This happens if for example SQLite cannot open the file (eg. a folder with the same name exists)
		LOG("SQLite3 failed to open DB! (" .. ErrCode .. ", " .. ErrMsg ..")");
	end

	LOG("SQLite bindings test ended");
end





function TestExpatBindings()
	LOG("Testing Expat bindings...");

	-- Debug LuaExpat bindings:
	local count = 0
	callbacks = {
	    StartElement = function (parser, name)
	        LOG("+ " .. string.rep(" ", count) .. name);
	        count = count + 1;
	    end,
	    EndElement = function (parser, name)
	        count = count - 1;
	        LOG("- " .. string.rep(" ", count) .. name);
	    end
	}

	local p = lxp.new(callbacks);
	p:parse("<elem1>\nnext line\nanother line");
	p:parse("text\n");
	p:parse("<elem2/>\n");
	p:parse("more text");
	p:parse("</elem1>");
	p:parse("\n");
	p:parse();  -- finishes the document
	p:close();  -- closes the parser

	LOG("Expat bindings test ended");
end





function OnUsingBlazeRod(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ)
	-- Magic rod of query: show block types and metas for both neighbors of the pointed face
	local Valid, Type, Meta = Player:GetWorld():GetBlockTypeMeta(BlockX, BlockY, BlockZ);

	if (Type == E_BLOCK_AIR) then
		Player:SendMessage(cChatColor.LightGray .. "Block {" .. BlockX .. ", " .. BlockY .. ", " .. BlockZ .. "}: air:" .. Meta);
	else
		local TempItem = cItem(Type, 1, Meta);
		Player:SendMessage(cChatColor.LightGray .. "Block {" .. BlockX .. ", " .. BlockY .. ", " .. BlockZ .. "}: " .. ItemToFullString(TempItem) .. " (" .. Type .. ":" .. Meta .. ")");
	end

	local X, Y, Z = AddFaceDirection(BlockX, BlockY, BlockZ, BlockFace);
	Valid, Type, Meta = Player:GetWorld():GetBlockTypeMeta(X, Y, Z);
	if (Type == E_BLOCK_AIR) then
		Player:SendMessage(cChatColor.LightGray .. "Block {" .. X .. ", " .. Y .. ", " .. Z .. "}: air:" .. Meta);
	else
		local TempItem = cItem(Type, 1, Meta);
		Player:SendMessage(cChatColor.LightGray .. "Block {" .. X .. ", " .. Y .. ", " .. Z .. "}: " .. ItemToFullString(TempItem) .. " (" .. Type .. ":" .. Meta .. ")");
	end
	return false;
end





function OnUsingDiamond(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ)
	-- Rclk with a diamond to test block area cropping and expanding
	local Area = cBlockArea();
	Area:Read(Player:GetWorld(),
		BlockX - 19, BlockX + 19,
		BlockY - 7, BlockY + 7,
		BlockZ - 19, BlockZ + 19
	);

	LOG("Size before cropping: " .. Area:GetSizeX() .. " x " .. Area:GetSizeY() .. " x " .. Area:GetSizeZ());
	Area:DumpToRawFile("crop0.dat");

	Area:Crop(2, 3, 0, 0, 0, 0);
	LOG("Size after cropping 1: " .. Area:GetSizeX() .. " x " .. Area:GetSizeY() .. " x " .. Area:GetSizeZ());
	Area:DumpToRawFile("crop1.dat");

	Area:Crop(2, 3, 0, 0, 0, 0);
	LOG("Size after cropping 2: " .. Area:GetSizeX() .. " x " .. Area:GetSizeY() .. " x " .. Area:GetSizeZ());
	Area:DumpToRawFile("crop2.dat");

	Area:Expand(2, 3, 0, 0, 0, 0);
	LOG("Size after expanding 1: " .. Area:GetSizeX() .. " x " .. Area:GetSizeY() .. " x " .. Area:GetSizeZ());
	Area:DumpToRawFile("expand1.dat");

	Area:Expand(3, 2, 1, 1, 0, 0);
	LOG("Size after expanding 2: " .. Area:GetSizeX() .. " x " .. Area:GetSizeY() .. " x " .. Area:GetSizeZ());
	Area:DumpToRawFile("expand2.dat");

	Area:Crop(0, 0, 0, 0, 3, 2);
	LOG("Size after cropping 3: " .. Area:GetSizeX() .. " x " .. Area:GetSizeY() .. " x " .. Area:GetSizeZ());
	Area:DumpToRawFile("crop3.dat");

	Area:Crop(0, 0, 3, 2, 0, 0);
	LOG("Size after cropping 4: " .. Area:GetSizeX() .. " x " .. Area:GetSizeY() .. " x " .. Area:GetSizeZ());
	Area:DumpToRawFile("crop4.dat");

	LOG("Crop test done");
	Player:SendMessage("Crop / expand test done.");
	return false;
end





function OnUsingEyeOfEnder(Player, BlockX, BlockY, BlockZ)
	-- Rclk with an eye of ender places a predefined schematic at the cursor
	local Area = cBlockArea();
	if not(Area:LoadFromSchematicFile("schematics/test.schematic")) then
		LOG("Loading failed");
		return false;
	end
	LOG("Schematic loaded, placing now.");
	Area:Write(Player:GetWorld(), BlockX, BlockY, BlockZ);
	LOG("Done.");
	return false;
end





function OnUsingEnderPearl(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ)
	-- Rclk with an ender pearl saves a predefined area around the cursor into a .schematic file. Also tests area copying
	local Area = cBlockArea();
	if not(Area:Read(Player:GetWorld(),
		BlockX - 8, BlockX + 8, BlockY - 8, BlockY + 8, BlockZ - 8, BlockZ + 8)
	) then
		LOG("LUA: Area couldn't be read");
		return false;
	end
	LOG("LUA: Area read, copying now.");
	local Area2 = cBlockArea();
	Area2:CopyFrom(Area);
	LOG("LUA: Copied, now saving.");
	if not(Area2:SaveToSchematicFile("schematics/test.schematic")) then
		LOG("LUA: Cannot save schematic file.");
		return false;
	end
	LOG("LUA: Done.");
	return false;
end





function OnUsingRedstoneTorch(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ)
	-- Redstone torch activates a rapid dispenser / dropper discharge (at every tick):
	local BlockType = Player:GetWorld():GetBlock(BlockX, BlockY, BlockZ);
	if (BlockType == E_BLOCK_DISPENSER) then
		table.insert(g_DropSpensersToActivate, {World = Player:GetWorld(), x = BlockX, y = BlockY, z = BlockZ});
		Player:SendMessage("Dispenser at {" .. BlockX .. ", " .. BlockY .. ", " .. BlockZ .. "} discharging");
		return true;
	elseif (BlockType == E_BLOCK_DROPPER) then
		table.insert(g_DropSpensersToActivate, {World = Player:GetWorld(), x = BlockX, y = BlockY, z = BlockZ});
		Player:SendMessage("Dropper at {" .. BlockX .. ", " .. BlockY .. ", " .. BlockZ .. "} discharging");
		return true;
	else
		Player:SendMessage("Neither a dispenser nor a dropper at {" .. BlockX .. ", " .. BlockY .. ", " .. BlockZ .. "}: " .. BlockType);
	end
	return false;
end





function OnPlayerUsingItem(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ)

	-- dont check if the direction is in the air
	if (BlockFace == BLOCK_FACE_NONE) then
		return false
	end

	local HeldItem = Player:GetEquippedItem();
	local HeldItemType = HeldItem.m_ItemType;

	if (HeldItemType == E_ITEM_STICK) then
		-- Magic sTick of ticking: set the pointed block for ticking at the next tick
		Player:SendMessage(cChatColor.LightGray .. "Setting next block tick to {" .. BlockX .. ", " .. BlockY .. ", " .. BlockZ .. "}")
		Player:GetWorld():SetNextBlockTick(BlockX, BlockY, BlockZ);
		return true
	elseif (HeldItemType == E_ITEM_BLAZE_ROD) then
		return OnUsingBlazeRod(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ);
	elseif (HeldItemType == E_ITEM_DIAMOND) then
		return OnUsingDiamond(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ);
	elseif (HeldItemType == E_ITEM_EYE_OF_ENDER) then
		return OnUsingEyeOfEnder(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ);
	elseif (HeldItemType == E_ITEM_ENDER_PEARL) then
		return OnUsingEnderPearl(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ);
	end
	return false;
end





function OnPlayerUsingBlock(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ, BlockType, BlockMeta)
	-- dont check if the direction is in the air
	if (BlockFace == BLOCK_FACE_NONE) then
		return false
	end

	local HeldItem = Player:GetEquippedItem();
	local HeldItemType = HeldItem.m_ItemType;

	if (HeldItemType == E_BLOCK_REDSTONE_TORCH_ON) then
		return OnUsingRedstoneTorch(Player, BlockX, BlockY, BlockZ, BlockFace, CursorX, CursorY, CursorZ);
	end
end





function OnTakeDamage(Receiver, TDI)
	-- Receiver is cPawn
	-- TDI is TakeDamageInfo

	-- LOG(Receiver:GetClass() .. " was dealt " .. DamageTypeToString(TDI.DamageType) .. " damage: Raw " .. TDI.RawDamage .. ", Final " .. TDI.FinalDamage .. " (" .. (TDI.RawDamage - TDI.FinalDamage) .. " covered by armor)");
	return false;
end





function OnTick1()
	-- For testing multiple hook handlers per plugin
	LOGINFO("Tick1");
end





function OnTick2()
	-- For testing multiple hook handlers per plugin
	LOGINFO("Tick2");
end





--- When set to a positive number, the following OnTick() will perform GC and decrease until 0 again
GCOnTick = 0;





function OnTick()
	-- Activate all dropspensers in the g_DropSpensersToActivate list:
	local ActivateDrSp = function(DropSpenser)
		if (DropSpenser:GetContents():GetFirstUsedSlot() == -1) then
			return true;
		end
		DropSpenser:Activate();
		return false;
	end
	-- Walk the list backwards, because we're removing some items
	local idx = #g_DropSpensersToActivate;
	for i = idx, 1, -1 do
		local DrSp = g_DropSpensersToActivate[i];
		if not(DrSp.World:DoWithDropSpenserAt(DrSp.x, DrSp.y, DrSp.z, ActivateDrSp)) then
			table.remove(g_DropSpensersToActivate, i);
		end
	end


	-- If GCOnTick > 0, do a garbage-collect and decrease by one
	if (GCOnTick > 0) then
		collectgarbage();
		GCOnTick = GCOnTick - 1;
	end

	return false;
end





function OnWorldTick(a_World, a_Dt)
	-- Report food stats, if switched on:
	local Tick = a_World:GetWorldAge();
	if (not(g_ShowFoodStats) or (math.mod(Tick, 10) ~= 0)) then
		return false;
	end
	a_World:ForEachPlayer(
		function(a_Player)
			a_Player:SendMessage(
				tostring(Tick / 10) ..
				" > FS: fl " .. a_Player:GetFoodLevel() ..
				"; sat " .. a_Player:GetFoodSaturationLevel() ..
				"; exh " .. a_Player:GetFoodExhaustionLevel()
			);
		end
	);
end





function OnChat(a_Player, a_Message)
	return false, "blabla " .. a_Message;
end





function OnPlayerRightClick(a_Player)
	-- If the player is holding a cake item, make them throw a cake block, using a FallingBlock entity:
	if (a_Player:GetInventory():GetEquippedItem().m_ItemType == E_ITEM_CAKE) then
		local World = a_Player:GetWorld()
		local Position = a_Player:GetPosition() + Vector3d(0, 1.5, 0)
		local EntityID = World:SpawnFallingBlock(Vector3i(Position), E_BLOCK_CAKE, 0)

		World:DoWithEntityByID(EntityID,
			function (Entity)
				Entity:TeleportToCoords(Position.x, Position.y, Position.z)
				Entity:SetSpeed(a_Player:GetLookVector() * 30)
			end
		)
	end
end





function OnPlayerRightClickingEntity(a_Player, a_Entity)
	LOG("Player " .. a_Player:GetName() .. " right-clicking entity ID " .. a_Entity:GetUniqueID() .. ", a " .. a_Entity:GetClass());
	return false;
end





function OnPluginsLoaded()
	LOG("All plugins loaded");
end





function OnChunkGenerated(a_World, a_ChunkX, a_ChunkZ, a_ChunkDesc)
	-- Get the topmost block coord:
	local Height = a_ChunkDesc:GetHeight(0, 0);

	-- Create a sign there:
	a_ChunkDesc:SetBlockTypeMeta(0, Height + 1, 0, E_BLOCK_SIGN_POST, 0);
	local BlockEntity = a_ChunkDesc:GetBlockEntity(0, Height + 1, 0);
	if (BlockEntity ~= nil) then
		local SignEntity = tolua.cast(BlockEntity, "cSignEntity");
		SignEntity:SetLines("Chunk:", tonumber(a_ChunkX) .. ", " .. tonumber(a_ChunkZ), "", "(Debuggers)");
	end

	-- Update the heightmap:
	a_ChunkDesc:SetHeight(0, 0, Height + 1);
end





-- Function "round" copied from http://lua-users.org/wiki/SimpleRound
function round(num, idp)
	local mult = 10^(idp or 0)
	if num >= 0 then return math.floor(num * mult + 0.5) / mult
	else return math.ceil(num * mult - 0.5) / mult end
end





function HandleNickCmd(Split, Player)
	if (Split[2] == nil) then
		Player:SendMessage("Usage: /nick [CustomName]");
		return true;
	end

	Player:SetCustomName(Split[2]);
	Player:SendMessageSuccess("Custom name setted to " .. Player:GetCustomName() .. "!")
	return true
end





function HandleListEntitiesCmd(Split, Player)
	local NumEntities = 0;

	local ListEntity = function(Entity)
		if (Entity:IsDestroyed()) then
			-- The entity has already been destroyed, don't list it
			return false;
		end;
		local cls = Entity:GetClass();
		Player:SendMessage("  " .. Entity:GetUniqueID() .. ": " .. cls .. "   {" .. round(Entity:GetPosX(), 2) .. ", " .. round(Entity:GetPosY(), 2) .. ", " .. round(Entity:GetPosZ(), 2) .."}");
		if (cls == "cPickup") then
			local Pickup = Entity;
			tolua.cast(Pickup, "cPickup");
			Player:SendMessage("    Age: " .. Pickup:GetAge() .. ", IsCollected: " .. tostring(Pickup:IsCollected()));
		end
		NumEntities = NumEntities + 1;
	end

	Player:SendMessage("Listing all entities...");
	Player:GetWorld():ForEachEntity(ListEntity);
	Player:SendMessage("List finished, " .. NumEntities .. " entities listed");
	return true;
end





function HandleKillEntitiesCmd(Split, Player)
	local NumEntities = 0;

	local KillEntity = function(Entity)
		-- kill everything except for players:
		if (Entity:GetEntityType() ~= cEntity.etPlayer) then
			Entity:Destroy();
			NumEntities = NumEntities + 1;
		end;
	end

	Player:SendMessage("Killing all entities...");
	Player:GetWorld():ForEachEntity(KillEntity);
	Player:SendMessage("Killed " .. NumEntities .. " entities.");
	return true;
end





function HandleWoolCmd(Split, Player)
	local Wool = cItem(E_BLOCK_WOOL, 1, E_META_WOOL_BLUE);
	Player:GetInventory():SetArmorSlot(0, Wool);
	Player:GetInventory():SetArmorSlot(1, Wool);
	Player:GetInventory():SetArmorSlot(2, Wool);
	Player:GetInventory():SetArmorSlot(3, Wool);
	Player:SendMessage("You have been bluewooled :)");
	return true;
end





function HandleTestWndCmd(a_Split, a_Player)
	local WindowType  = cWindow.wtHopper;
	local WindowSizeX = 5;
	local WindowSizeY = 1;
	if (#a_Split == 4) then
		WindowType  = tonumber(a_Split[2]);
		WindowSizeX = tonumber(a_Split[3]);
		WindowSizeY = tonumber(a_Split[4]);
	elseif (#a_Split ~= 1) then
		a_Player:SendMessage("Usage: /testwnd [WindowType WindowSizeX WindowSizeY]");
		return true;
	end

	-- Test out the OnClosing callback's ability to refuse to close the window
	local attempt = 1;
	local OnClosing = function(Window, Player, CanRefuse)
		Player:SendMessage("Window closing attempt #" .. attempt .. "; CanRefuse = " .. tostring(CanRefuse));
		attempt = attempt + 1;
		return CanRefuse and (attempt <= 3);  -- refuse twice, then allow, unless CanRefuse is set to true
	end

	-- Log the slot changes
	local OnSlotChanged = function(Window, SlotNum)
		LOG("Window \"" .. Window:GetWindowTitle() .. "\" slot " .. SlotNum .. " changed.");
	end

	local Window = cLuaWindow(WindowType, WindowSizeX, WindowSizeY, "TestWnd");
	local Item2 = cItem(E_ITEM_DIAMOND_SWORD, 1, 0, "1=1");
	local Item3 = cItem(E_ITEM_DIAMOND_SHOVEL);
	Item3.m_Enchantments:SetLevel(cEnchantments.enchUnbreaking, 4);
	local Item4 = cItem(Item3);  -- Copy
	Item4.m_Enchantments:SetLevel(cEnchantments.enchEfficiency, 3);  -- Add enchantment
	Item4.m_Enchantments:SetLevel(cEnchantments.enchUnbreaking, 5);  -- Overwrite existing level
	local Item5 = cItem(E_ITEM_DIAMOND_CHESTPLATE, 1, 0, "thorns=1;unbreaking=3");
	Window:SetSlot(a_Player, 0, cItem(E_ITEM_DIAMOND, 64));
	Window:SetSlot(a_Player, 1, Item2);
	Window:SetSlot(a_Player, 2, Item3);
	Window:SetSlot(a_Player, 3, Item4);
	Window:SetSlot(a_Player, 4, Item5);
	Window:SetOnClosing(OnClosing);
	Window:SetOnSlotChanged(OnSlotChanged);

	a_Player:OpenWindow(Window);

	-- To make sure that the object has the correct life-management in Lua,
	-- let's garbage-collect in the following few ticks
	GCOnTick = 10;

	return true;
end





function HandleGCCmd(a_Split, a_Player)
	collectgarbage();
	return true;
end






function HandleFastCmd(a_Split, a_Player)
	if (a_Player:GetNormalMaxSpeed() <= 0.11) then
		-- The player has normal speed, set double speed:
		a_Player:SetNormalMaxSpeed(0.2);
		a_Player:SendMessage("You are now fast");
	else
		-- The player has fast speed, set normal speed:
		a_Player:SetNormalMaxSpeed(0.1);
		a_Player:SendMessage("Back to normal speed");
	end
	return true;
end





function HandleDashCmd(a_Split, a_Player)
	if (a_Player:GetSprintingMaxSpeed() <= 0.14) then
		-- The player has normal sprinting speed, set double Sprintingspeed:
		a_Player:SetSprintingMaxSpeed(0.4);
		a_Player:SendMessage("You can now sprint very fast");
	else
		-- The player has fast sprinting speed, set normal sprinting speed:
		a_Player:SetSprintingMaxSpeed(0.13);
		a_Player:SendMessage("Back to normal sprinting");
	end
	return true;
end;





function HandleGenRailsCmd(a_Split, a_Player)
	local MAX_RAIL_META = 9
	local pos = a_Player:GetPosition()
	local ba = cBlockArea:new()
	ba:Create(2 * MAX_RAIL_META + 3, 4, 3, cBlockArea.baTypes + cBlockArea.baMetas)
	ba:FillRelCuboid(0, 2 * MAX_RAIL_META + 2, 0, 0, 0, 2, cBlockArea.baTypes, E_BLOCK_STONE)
	ba:FillRelCuboid(0, 2 * MAX_RAIL_META + 2, 1, 3, 0, 2, cBlockArea.baTypes, E_BLOCK_AIR)
	for x = 0, MAX_RAIL_META do
		ba:SetRelBlockTypeMeta(2 * x + 1, 1, 1, E_BLOCK_RAIL, x)
	end
	ba:Write(a_Player:GetWorld(), pos:Floor())
	return true
end





function HandleGetCustomNameCmd(a_Split, a_Player)
	local item = a_Player:GetInventory():GetEquippedItem()
	if (not(item.m_CustomName) or (item.m_CustomName == "")) then
		a_Player:SendMessage("The custom name is empty")
		return true
	end
	local dispCN = string.gsub(item.m_CustomName, ".",
		function(a_Char)
			if (a_Char < " ") then
				return string.byte(a_Char)
			end
			return a_Char
		end
	)
	a_Player:SendMessage(string.format("The custom name is %d bytes: %s", string.len(item.m_CustomName), dispCN))
	return true
end





function HandleGetLoreCmd(a_Split, a_Player)
	local item = a_Player:GetInventory():GetEquippedItem()
	if (not(item.m_Lore) or (item.m_Lore == "")) then
		a_Player:SendMessage("The lore is empty")
		return true
	end
	local dispLore = string.gsub(item.m_Lore, ".",
		function(a_Char)
			if (a_Char < " ") then
				return string.byte(a_Char)
			end
			return a_Char
		end
	)
	a_Player:SendMessage(string.format("The lore is %d bytes: %s", string.len(item.m_Lore), dispLore))
	return true
end





function HandleGetPropCmd(a_Split, a_Player)
	local item = a_Player:GetInventory():GetEquippedItem()
	if not(item.m_DebuggersCustomProp) then
		a_Player:SendMessage("The custom property is not set.")
		return true
	end
	local dispValue = string.gsub(item.m_DebuggersCustomProp, ".",
		function(a_Char)
			if (a_Char < " ") then
				return string.byte(a_Char)
			end
			return a_Char
		end
	)
	a_Player:SendMessage(string.format("The custom property value is %d bytes: %s", string.len(item.m_DebuggersCustomProp), dispValue))
	return true
end





function HandleHungerCmd(a_Split, a_Player)
	a_Player:SendMessage("FoodLevel: " .. a_Player:GetFoodLevel());
	a_Player:SendMessage("FoodSaturationLevel: " .. a_Player:GetFoodSaturationLevel());
	a_Player:SendMessage("FoodTickTimer: " .. a_Player:GetFoodTickTimer());
	a_Player:SendMessage("FoodExhaustionLevel: " .. a_Player:GetFoodExhaustionLevel());
	a_Player:SendMessage("FoodPoisonedTicksRemaining: " .. a_Player:GetFoodPoisonedTicksRemaining());
	return true;
end





function HandlePoisonCmd(a_Split, a_Player)
	a_Player:FoodPoison(15 * 20);
	return true;
end





function HandleStarveCmd(a_Split, a_Player)
	a_Player:SetFoodLevel(0);
	a_Player:SendMessage("You are now starving");
	return true;
end





function HandleFoodLevelCmd(a_Split, a_Player)
	if (#a_Split ~= 2) then
		a_Player:SendMessage("Missing an argument: the food level to set");
		return true;
	end

	a_Player:SetFoodLevel(tonumber(a_Split[2]));
	a_Player:SetFoodSaturationLevel(5);
	a_Player:SetFoodExhaustionLevel(0);
	a_Player:SendMessage(
		"Food level set to " .. a_Player:GetFoodLevel() ..
		", saturation reset to " .. a_Player:GetFoodSaturationLevel() ..
		" and exhaustion reset to " .. a_Player:GetFoodExhaustionLevel()
	);
	return true;
end





function HandleSetCustomNameCmd(a_Split, a_Player, a_EntireCmd)
	if not(a_Split[2]) then
		a_Player:SendMessageFatal("Missing an argument: the custom name to set");
		return true
	end
	local nameToSet = a_EntireCmd:match("/setcustomname%s(.*)")
	if not(nameToSet) then
		a_Player:SendMessageFatal("Failed to extract the custom name to set")
		return true
	end
	nameToSet = nameToSet:gsub("\\([0-9][0-9][0-9])", string.char)

	local inv = a_Player:GetInventory()
	local slotNum = inv:GetEquippedSlotNum()
	local item = cItem(inv:GetEquippedItem())  -- Make a copy of the item
	item.m_CustomName = nameToSet
	inv:SetHotbarSlot(slotNum, item)
	a_Player:SendMessage("Custom name set to " .. nameToSet)
	return true
end





function HandleSetPropCmd(a_Split, a_Player, a_EntireCmd)
	if not(a_Split[2]) then
		a_Player:SendMessageFatal("Missing an argument: the property value to set");
		return true
	end
	local valueToSet = a_EntireCmd:match("/setprop%s(.*)")
	if not(valueToSet) then
		a_Player:SendMessageFatal("Failed to extract the property value to set")
		return true
	end
	valueToSet = valueToSet:gsub("\\([0-9][0-9][0-9])", string.char)

	local inv = a_Player:GetInventory()
	local slotNum = inv:GetEquippedSlotNum()
	local item = inv:GetEquippedItem()
	item.m_DebuggersCustomProp = valueToSet
	inv:SetHotbarSlot(slotNum, item)
	a_Player:SendMessage("Custom property set to " .. valueToSet)
	return true
end





function HandleSetLoreCmd(a_Split, a_Player, a_EntireCmd)
	if not(a_Split[2]) then
		a_Player:SendMessageFatal("Missing an argument: the lore to set");
		return true
	end
	local loreToSet = a_EntireCmd:match("/setlore%s(.*)")
	if not(loreToSet) then
		a_Player:SendMessageFatal("Failed to extract the lore to set")
		return true
	end
	loreToSet = loreToSet:gsub("\\([0-9][0-9][0-9])", string.char)

	local inv = a_Player:GetInventory()
	local slotNum = inv:GetEquippedSlotNum()
	local item = cItem(inv:GetEquippedItem())  -- Make a copy of the item
	local oldLore = item.m_Lore
	item.m_Lore = loreToSet
	inv:SetHotbarSlot(slotNum, item)
	a_Player:SendMessage("Lore set to " .. loreToSet)
	return true
end





function HandleSpideyCmd(a_Split, a_Player)
	-- Place a line of cobwebs from the player's eyes until non-air block, in the line-of-sight of the player
	local World = a_Player:GetWorld();

	local Callbacks = {
		OnNextBlock = function(a_BlockX, a_BlockY, a_BlockZ, a_BlockType, a_BlockMeta)
			if (a_BlockType ~= E_BLOCK_AIR) then
				-- abort the trace
				return true;
			end
			World:SetBlock(a_BlockX, a_BlockY, a_BlockZ, E_BLOCK_COBWEB, 0);
		end
	};

	local EyePos = a_Player:GetEyePosition();
	local LookVector = a_Player:GetLookVector();
	LookVector:Normalize();

	-- Start cca 2 blocks away from the eyes
	local Start = EyePos + LookVector + LookVector;
	local End = EyePos + LookVector * 50;

	cLineBlockTracer.Trace(World, Callbacks, Start.x, Start.y, Start.z, End.x, End.y, End.z);

	return true;
end





function HandleEnchCmd(a_Split, a_Player)
	local Wnd = cLuaWindow(cWindow.wtEnchantment, 1, 1, "Ench");
	a_Player:OpenWindow(Wnd);
	Wnd:SetProperty(0, 10);
	Wnd:SetProperty(1, 15);
	Wnd:SetProperty(2, 25);
	return true;
end





function HandleFoodStatsCmd(a_Split, a_Player)
	g_ShowFoodStats = not(g_ShowFoodStats);
	return true;
end





function HandleArrowCmd(a_Split, a_Player)
	local World = a_Player:GetWorld();
	local Pos = a_Player:GetEyePosition();
	local Speed = a_Player:GetLookVector();
	Speed:Normalize();
	Pos = Pos + Speed;

	World:CreateProjectile(Pos.x, Pos.y, Pos.z, cProjectileEntity.pkArrow, a_Player, Speed * 10);
	return true;
end





function HandleFireballCmd(a_Split, a_Player)
	local World = a_Player:GetWorld();
	local Pos = a_Player:GetEyePosition();
	local Speed = a_Player:GetLookVector();
	Speed:Normalize();
	Pos = Pos + Speed * 2;

	World:CreateProjectile(Pos.x, Pos.y, Pos.z, cProjectileEntity.pkGhastFireball, a_Player, Speed * 10);
	return true;
end




function HandleAddExperience(a_Split, a_Player)
	a_Player:DeltaExperience(200);

	return true;
end





function HandleRemoveXp(a_Split, a_Player)
	a_Player:SetCurrentExperience(0);

	return true;
end





function HandleFill(a_Split, a_Player)
	local World = a_Player:GetWorld();
	local ChunkX = a_Player:GetChunkX();
	local ChunkZ = a_Player:GetChunkZ();
	World:ForEachBlockEntityInChunk(ChunkX, ChunkZ,
		function(a_BlockEntity)
			local BlockType = a_BlockEntity:GetBlockType();
			if (
				(BlockType == E_BLOCK_CHEST) or
				(BlockType == E_BLOCK_DISPENSER) or
				(BlockType == E_BLOCK_DROPPER) or
				(BlockType == E_BLOCK_FURNACE) or
				(BlockType == E_BLOCK_HOPPER)
			) then
				-- This block entity has items (inherits from cBlockEntityWithItems), fill it:
				-- Note that we're not touching lit furnaces, don't wanna mess them up
				local EntityWithItems = tolua.cast(a_BlockEntity, "cBlockEntityWithItems");
				local ItemGrid = EntityWithItems:GetContents();
				local NumSlots = ItemGrid:GetNumSlots();
				local ItemToSet = cItem(E_ITEM_GOLD_NUGGET);
				for i = 0, NumSlots - 1 do
					if (ItemGrid:GetSlot(i):IsEmpty()) then
						ItemGrid:SetSlot(i, ItemToSet);
					end
				end
			end
		end
	);
	return true;
end





function HandleFurnaceRecipe(a_Split, a_Player)
	local HeldItem = a_Player:GetEquippedItem();
	local Out, NumTicks, In = cRoot:GetFurnaceRecipe(HeldItem);
	if (Out ~= nil) then
		a_Player:SendMessage(
			"Furnace turns " .. ItemToFullString(In) ..
			" to " .. ItemToFullString(Out) ..
			" in " .. NumTicks .. " ticks (" ..
			tostring(NumTicks / 20) .. " seconds)."
		);
	else
		a_Player:SendMessage("There is no furnace recipe that would smelt " .. ItemToString(HeldItem));
	end
	return true;
end





function HandleFurnaceFuel(a_Split, a_Player)
	local HeldItem = a_Player:GetEquippedItem();
	local NumTicks = cRoot:GetFurnaceFuelBurnTime(HeldItem);
	if (NumTicks > 0) then
		a_Player:SendMessage(
			ItemToFullString(HeldItem) .. " would power a furnace for " .. NumTicks ..
			" ticks (" .. tostring(NumTicks / 20) .. " seconds)."
		);
	else
		a_Player:SendMessage(ItemToString(HeldItem) .. " will not power furnaces.");
	end
	return true;
end





function HandleSched(a_Split, a_Player)
	local World = a_Player:GetWorld()

	-- Schedule a broadcast of a countdown message:
	for i = 1, 10 do
		World:ScheduleTask(i * 20,
			function(a_World)
				a_World:BroadcastChat("Countdown: " .. 11 - i)
			end
		)
	end

	-- Schedule a broadcast of the final message and a note to the originating player
	-- Note that we CANNOT use the a_Player in the callback - what if the player disconnected?
	-- Therefore we store the player's EntityID
	local PlayerID = a_Player:GetUniqueID()
	World:ScheduleTask(220,
		function(a_World)
			a_World:BroadcastChat("Countdown: BOOM")
			a_World:DoWithEntityByID(PlayerID,
				function(a_Entity)
					if (a_Entity:IsPlayer()) then
						-- Although unlikely, it is possible that this player is not the originating player
						-- However, I leave this as an excercise to you to fix this "bug"
						local Player = tolua.cast(a_Entity, "cPlayer")
						Player:SendMessage("Countdown finished")
					end
				end
			)
		end
	)

	return true
end





function HandleRMItem(a_Split, a_Player)
	-- Check params:
	if (a_Split[2] == nil) then
		a_Player:SendMessage("Usage: /rmitem <Item> [Count]")
		return true
	end

	-- Parse the item type:
	local Item = cItem()
	if (not StringToItem(a_Split[2], Item)) then
		a_Player:SendMessageFailure(a_Split[2] .. " isn't a valid item")
		return true
	end

	-- Parse the optional item count
	if (a_Split[3] ~= nil) then
		local Count = tonumber(a_Split[3])
		if (Count == nil) then
			a_Player:SendMessageFailure(a_Split[3] .. " isn't a valid number")
			return true
		end

		Item.m_ItemCount = Count
	end

	-- Remove the item:
	local NumRemovedItems = a_Player:GetInventory():RemoveItem(Item)
	a_Player:SendMessageSuccess("Removed " .. NumRemovedItems .. " Items!")
	return true
end





function HandleRequest_Debuggers(a_Request)
	local FolderContents = cFile:GetFolderContents("./");
	return "<p>The following objects have been returned by cFile:GetFolderContents():<ul><li>" .. table.concat(FolderContents, "</li><li>") .. "</li></ul></p>";
end





local g_Counter = 0
local g_JavaScript =
[[
<script>
function createXHR()
{
	var request = false;
	try {
		request = new ActiveXObject('Msxml2.XMLHTTP');
	}
	catch (err2)
	{
		try
		{
			request = new ActiveXObject('Microsoft.XMLHTTP');
		}
		catch (err3)
		{
			try
			{
				request = new XMLHttpRequest();
			}
			catch (err1)
			{
				request = false;
			}
		}
	}
	return request;
}

function RefreshCounter()
{
	var xhr = createXHR();
	xhr.onreadystatechange = function()
	{
		if (xhr.readyState == 4)
		{
			document.getElementById("cnt").innerHTML = xhr.responseText;
		}
	};
	xhr.open("POST", "/~webadmin/Debuggers/StressTest", true);
	xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
	xhr.send("counter=true");
}

setInterval(RefreshCounter, 10)
</script>
]]

function HandleRequest_StressTest(a_Request)
	if (a_Request.PostParams["counter"]) then
		g_Counter = g_Counter + 1
		return tostring(g_Counter)
	end
	return g_JavaScript .. "<p>The counter below should be reloading as fast as possible</p><div id='cnt'>0</div>"
end





function OnPluginMessage(a_Client, a_Channel, a_Message)
	LOGINFO("Received a plugin message from client " .. a_Client:GetUsername() .. ": channel '" .. a_Channel .. "', message '" .. a_Message .. "'");

	if (a_Channel == "REGISTER") then
		if (a_Message:find("WECUI")) then
			-- The client has WorldEditCUI mod installed, test the comm by sending a few WECUI messages:
			--[[
			WECUI messages have the following generic format:
			<shape>|<params>
			If shape is p (cuboid selection), the params are sent individually for each corner click and have the following format:
			<point-index>|<x>|<y>|<z>|<volume>
			point-index is 0 or 1 (lclk / rclk)
			volume is the 3D volume of the current cuboid selected (all three coords' deltas multiplied), including the edge blocks; -1 if N/A
			--]]
			-- Select a 51 * 51 * 51 block cuboid:
			a_Client:SendPluginMessage("WECUI", "p|0|50|50|50|-1");
			a_Client:SendPluginMessage("WECUI", "p|1|100|100|100|132651");  -- 132651 = 51 * 51 * 51
		end
	end
end





function HandleChunkStay(a_Split, a_Player)
	-- As an example of using ChunkStay, this call will load 3x3 chunks around the specified chunk coords,
	-- then build an obsidian pillar in the middle of each one.
	-- Once complete, the player will be teleported to the middle pillar

	if (#a_Split ~= 3) then
		a_Player:SendMessageInfo("Usage: /cs <ChunkX> <ChunkZ>")
		return true
	end

	local ChunkX = tonumber(a_Split[2])
	local ChunkZ = tonumber(a_Split[3])
	if ((ChunkX == nil) or (ChunkZ == nil)) then
		a_Player:SendMessageFailure("Invalid chunk coords.")
		return true
	end

	local World = a_Player:GetWorld()
	local PlayerID = a_Player:GetUniqueID()
	a_Player:SendMessageInfo("Loading chunks, stand by...");

	-- Set the wanted chunks:
	local Chunks = {}
	for z = -1, 1 do for x = -1, 1 do
		table.insert(Chunks, {ChunkX + x, ChunkZ + z})
	end end

	-- The function that is called when all chunks are available
	-- Will perform the actual action with all those chunks
	-- Note that the player needs to be referenced using their EntityID - in case they disconnect before the chunks load
	local OnAllChunksAvailable = function()
		LOGINFO("ChunkStay all chunks now available")
		-- Build something on the neighboring chunks, to verify:
		for z = -1, 1 do for x = -1, 1 do
			local BlockX = (ChunkX + x) * 16 + 8
			local BlockZ = (ChunkZ + z) * 16 + 8
			for y = 20, 80 do
				World:SetBlock(BlockX, y, BlockZ, E_BLOCK_OBSIDIAN, 0)
			end
		end end

		-- Teleport the player there for visual inspection:
		World:DoWithEntityByID(PlayerID,
			function (a_CallbackPlayer)
				a_CallbackPlayer:TeleportToCoords(ChunkX * 16 + 8, 85, ChunkZ * 16 + 8)
				a_CallbackPlayer:SendMessageSuccess("ChunkStay fully available")
			end
		)
	end

	-- This function will be called for each chunk that is made available
	-- Note that the player needs to be referenced using their EntityID - in case they disconnect before the chunks load
	local OnChunkAvailable = function(a_ChunkX, a_ChunkZ)
		LOGINFO("ChunkStay now has chunk [" .. a_ChunkX .. ", " .. a_ChunkZ .. "]")
		World:DoWithEntityByID(PlayerID,
			function (a_CallbackPlayer)
				a_CallbackPlayer:SendMessageInfo("ChunkStay now has chunk [" .. a_ChunkX .. ", " .. a_ChunkZ .. "]")
			end
		)
	end

	-- Process the ChunkStay:
	World:ChunkStay(Chunks, OnChunkAvailable, OnAllChunksAvailable)
	return true
end





function HandleCakeCmd(a_Split, a_Player)
	local lookVector = a_Player:GetLookVector()
	local pos = a_Player:GetEyePosition() + lookVector
	local world = a_Player:GetWorld()
	local speed = lookVector * 10
	local cakeID = world:SpawnFallingBlock(pos, E_BLOCK_CAKE, 0)
	world:DoWithEntityByID(cakeID,
		function(a_CBCake)
			a_CBCake:SetSpeed(speed)
		end
	)
	a_Player:SendMessage("Your cake is served")
	return true
end





function HandleClientVersionCmd(a_Split, a_Player)
	a_Player:SendMessage("Your client version number is " .. a_Player:GetClientHandle():GetProtocolVersion() ..".")
	return true
end





function HandleComeCmd(a_Split, a_Player)
	-- Find the first solid block under the player (in case they are flying):
	local playerWorld = a_Player:GetWorld()
	local playerPos = a_Player:GetPosition()
	local toPos = Vector3i(playerPos)
	if (toPos.y < 1) then
		a_Player:SendMessageFailure("Cannot navigate to you, you're too low in the world")
		return true
	end
	while not(cBlockInfo:IsSolid(playerWorld:GetBlock(toPos.x, toPos.y, toPos.z))) do
		if (toPos.y <= 0) then
			a_Player:SendMessageFailure("Cannot navigate to you, there's no solid block below you")
			return true
		end
		toPos.y = toPos.y - 1
	end

	-- Find the mob to navigate:
	local mob
	local playerLook = a_Player:GetLookVector():NormalizeCopy()
	local maxDot = 0
	playerWorld:ForEachEntity(
		function (a_CBEntity)
			local dir = (a_CBEntity:GetPosition() - playerPos)
			dir:Normalize()
			local dot = dir:Dot(playerLook)
			if (dot > maxDot) then
				maxDot = dot
				mob = a_CBEntity
			end
		end
	)
	if not(mob) then
		a_Player:SendMessageFailure("Cannot navigate to you, there's no mob this way")
		return true
	end

	mob:MoveToPosition(Vector3d(toPos))
	a_Player:SendMessageSuccess((
		string.format("Navigating the %s to position {%d, %d, %d}",
			cMonster:MobTypeToString(mob:GetMobType()), toPos.x, toPos.y, toPos.z
		)
	))
	return true
end





function HandleCompo(a_Split, a_Player)
	-- Send one composite message to self:
	local msg = cCompositeChat()
	msg:AddTextPart("Hello! ", "b@e")  -- bold yellow
	msg:AddUrlPart("Cuberite", "https://cuberite.org")
	msg:AddTextPart(" rules! ")
	msg:AddRunCommandPart("Set morning", "/time set 0")
	a_Player:SendMessage(msg)

	-- Broadcast another one to the world:
	local msg2 = cCompositeChat()
	msg2:AddSuggestCommandPart(a_Player:GetName(), "/tell " .. a_Player:GetName() .. " ")
	msg2:AddTextPart(" knows how to use cCompositeChat!");
	a_Player:GetWorld():BroadcastChat(msg2)

	return true
end





function HandleSetBiome(a_Split, a_Player)
	local Biome = biJungle
	local Size = 20
	local SplitSize = #a_Split
	if (SplitSize > 3) then
		a_Player:SendMessage("Too many parameters. Usage: " .. a_Split[1] .. " <BiomeType>")
		return true
	end

	if (SplitSize >= 2) then
		Biome = StringToBiome(a_Split[2])
		if (Biome == biInvalidBiome) then
			a_Player:SendMessage("Unknown biome: '" .. a_Split[2] .. "'. Command ignored.")
			return true
		end
	end
	if (SplitSize >= 3) then
		Size = tostring(a_Split[3])
		if (Size == nil) then
			a_Player:SendMessage("Unknown size: '" .. a_Split[3] .. "'. Command ignored.")
			return true
		end
	end

	local BlockX = math.floor(a_Player:GetPosX())
	local BlockZ = math.floor(a_Player:GetPosZ())
	a_Player:GetWorld():SetAreaBiome(BlockX - Size, BlockX + Size, BlockZ - Size, BlockZ + Size, Biome)
	a_Player:SendMessage(
		"Blocks {" .. (BlockX - Size) .. ", " .. (BlockZ - Size) ..
		"} - {" .. (BlockX + Size) .. ", " .. (BlockZ + Size) ..
		"} set to biome #" .. tostring(Biome) .. "."
	)
	return true
end





function HandleWESel(a_Split, a_Player)
	-- Check if the selection is a cuboid:
	local IsCuboid = cPluginManager:CallPlugin("WorldEdit", "IsPlayerSelectionCuboid")
	if (IsCuboid == nil) then
		a_Player:SendMessage(cCompositeChat():SetMessageType(mtFailure):AddTextPart("Cannot adjust selection, WorldEdit is not loaded"))
		return true
	elseif (IsCuboid == false) then
		a_Player:SendMessage(cCompositeChat():SetMessageType(mtFailure):AddTextPart("Cannot adjust selection, the selection is not a cuboid"))
		return true
	end

	-- Get the selection:
	local SelCuboid = cCuboid()
	local IsSuccess = cPluginManager:CallPlugin("WorldEdit", "GetPlayerCuboidSelection", a_Player, SelCuboid)
	if not(IsSuccess) then
		a_Player:SendMessage(cCompositeChat():SetMessageType(mtFailure):AddTextPart("Cannot adjust selection, WorldEdit reported failure while getting current selection"))
		return true
	end

	-- Adjust the selection:
	local NumBlocks = tonumber(a_Split[2] or "1") or 1
	SelCuboid:Expand(NumBlocks, NumBlocks, 0, 0, NumBlocks, NumBlocks)

	-- Set the selection:
	IsSuccess = cPluginManager:CallPlugin("WorldEdit", "SetPlayerCuboidSelection", a_Player, SelCuboid)
	if not(IsSuccess) then
		a_Player:SendMessage(cCompositeChat():SetMessageType(mtFailure):AddTextPart("Cannot adjust selection, WorldEdit reported failure while setting new selection"))
		return true
	end
	a_Player:SendMessage(cCompositeChat():SetMessageType(mtInformation):AddTextPart("Successfully adjusted the selection by " .. NumBlocks .. " block(s)"))
	return true
end





function OnPlayerJoined(a_Player)
	-- Test composite chat chaining:
	a_Player:SendMessage(cCompositeChat()
		:AddTextPart("Hello, ")
		:AddUrlPart(a_Player:GetName(), "https://cuberite.org", "u@2")
		:AddSuggestCommandPart(", and welcome.", "/help", "u")
		:AddRunCommandPart(" SetDay", "/time set 0")
	)
end





function OnProjectileHitBlock(a_Projectile, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_BlockHitPos)
	-- Test projectile hooks by setting the blocks they hit on fire:
	local BlockX, BlockY, BlockZ = AddFaceDirection(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace)
	local World = a_Projectile:GetWorld()

	World:SetBlock(BlockX, BlockY, BlockZ, E_BLOCK_FIRE, 0)
end





function OnChunkUnloading(a_World, a_ChunkX, a_ChunkZ)
	-- Do not let chunk [0, 0] unload, so that it continues ticking [cWorld:SetChunkAlwaysTicked() test]
	if ((a_ChunkX == 0) and (a_ChunkZ == 0)) then
		return true
	end
end





function OnWorldStarted(a_World)
	-- Make the chunk [0, 0] in every world keep ticking [cWorld:SetChunkAlwaysTicked() test]
	a_World:ChunkStay({{0, 0}}, nil,
		function()
			-- The chunk is loaded, make it always tick:
			a_World:SetChunkAlwaysTicked(0, 0, true)
		end
	)
end





function OnProjectileHitBlock(a_ProjectileEntity, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_BlockHitPos)
	-- This simple test is for testing issue #1326 - simply declaring this hook would crash the server upon call
	LOG("Projectile hit block")
	LOG("  Projectile EntityID: " .. a_ProjectileEntity:GetUniqueID())
	LOG("  Block: {" .. a_BlockX .. ", " .. a_BlockY .. ", " .. a_BlockZ .. "}, face " .. a_BlockFace)
	LOG("  HitPos: {" .. a_BlockHitPos.x .. ", " .. a_BlockHitPos.y .. ", " .. a_BlockHitPos.z .. "}")
end





local PossibleItems =
{
	cItem(E_ITEM_DIAMOND),
	cItem(E_ITEM_GOLD),
	cItem(E_ITEM_IRON),
	cItem(E_ITEM_DYE, 1, E_META_DYE_BLUE),  -- Lapis lazuli
	cItem(E_ITEM_COAL),
}





function HandlePickups(a_Split, a_Player)
	local PlayerX = a_Player:GetPosX()
	local PlayerY = a_Player:GetPosY()
	local PlayerZ = a_Player:GetPosZ()
	local World = a_Player:GetWorld()
	local Range = 12
	for x = 0, Range do for z = 0, Range do
		local px = PlayerX + x - Range / 2
		local pz = PlayerZ + z - Range / 2
		local Items = cItems()
		Items:Add(PossibleItems[math.random(#PossibleItems)])
		World:SpawnItemPickups(Items, px, PlayerY, pz, 0)
	end end  -- for z, for x
	return true
end





function HandlePlugMsg(a_Split, a_Player)
	local ch = a_Player:GetClientHandle()
	ch:SendPluginMessage("TestCh", "some\0string\1with\2funny\3chars")
	return true
end





function HandlePoof(a_Split, a_Player)
	local PlayerPos = Vector3d(a_Player:GetPosition())  -- Create a copy of the position
	PlayerPos.y = PlayerPos.y - 1
	local Box = cBoundingBox(PlayerPos, 4, 2)
	local NumEntities = 0
	a_Player:GetWorld():ForEachEntityInBox(Box,
		function (a_Entity)
			if not(a_Entity:IsPlayer()) then
				local AddSpeed = a_Entity:GetPosition() - PlayerPos  -- Speed away from the player
				a_Entity:AddSpeed(AddSpeed * 32 / (AddSpeed:SqrLength() + 1))  -- The further away, the less speed to add
				NumEntities = NumEntities + 1
			end
		end
	)
	a_Player:SendMessage("Poof! (" .. NumEntities .. " entities)")
	return true
end





-- List of hashing functions to test:
local HashFunctions =
{
	{"md5", md5 },
	{"cCryptoHash.md5", cCryptoHash.md5 },
	{"cCryptoHash.md5HexString", cCryptoHash.md5HexString },
	{"cCryptoHash.sha1", cCryptoHash.sha1 },
	{"cCryptoHash.sha1HexString", cCryptoHash.sha1HexString },
}

-- List of strings to try hashing:
local HashExamples =
{
	"",
	"\0",
	"test",
}

function HandleConsoleHash(a_Split)
	for _, str in ipairs(HashExamples) do
		LOG("Hashing string \"" .. str .. "\":")
		for _, hash in ipairs(HashFunctions) do
			if not(hash[2]) then
				LOG("Hash function " .. hash[1] .. " doesn't exist in the API!")
			else
				LOG(hash[1] .. "() = " .. hash[2](str))
			end
		end  -- for hash - HashFunctions[]
	end  -- for str - HashExamples[]
	return true
end





function HandleConsoleHitTrace(a_Split)
	local world = cRoot:Get():GetDefaultWorld()
	local s = Vector3d(0, 70, 0)
	local e = Vector3d(100, 75, 100)
	if (tonumber(a_Split[2])) then
		s.x = tonumber(a_Split[2])
	end
	if (tonumber(a_Split[3])) then
		s.y = tonumber(a_Split[3])
	end
	if (tonumber(a_Split[4])) then
		s.z = tonumber(a_Split[4])
	end
	if (tonumber(a_Split[5])) then
		e.x = tonumber(a_Split[5])
	end
	if (tonumber(a_Split[6])) then
		e.y = tonumber(a_Split[6])
	end
	if (tonumber(a_Split[7])) then
		e.z = tonumber(a_Split[7])
	end
	local res, hitCoords, hitBlockCoords, hitBlockFace = cLineBlockTracer:FirstSolidHitTrace(world, s, e)
	if (res) then
		return true, string.format("The line hits block {%d, %d, %d} at point {%f, %f, %f}, face %s",
			hitBlockCoords.x, hitBlockCoords.y, hitBlockCoords.z,
			hitCoords.x, hitCoords.y, hitCoords.z,
			BlockFaceToString(hitBlockFace)
		)
	else
		return true, "The two points specified don't have a solid block between them."
	end
end





--- Monitors the state of the "inh" entity-spawning hook
-- if false, the hook is installed before the "inh" command processing
local isInhHookInstalled = false

function HandleConsoleInh(a_Split, a_FullCmd)
	-- Check the param:
	local kindStr = a_Split[2] or "pkArrow"
	local kind = cProjectileEntity[kindStr]
	if (kind == nil) then
		return true, "There's no projectile kind '" .. kindStr .. "'."
	end

	-- Get the world to test in:
	local world = cRoot:Get():GetDefaultWorld()
	if (world == nil) then
		return true, "Cannot test inheritance, no default world"
	end

	-- Install the hook, if needed:
	if not(isInhHookInstalled) then
		cPluginManager:AddHook(cPluginManager.HOOK_SPAWNING_ENTITY,
			function (a_CBWorld, a_CBEntity)
				LOG("New entity is spawning:")
				LOG("  Lua type:        '" .. type(a_CBEntity)           .. "'")
				LOG("  ToLua type:      '" .. tolua.type(a_CBEntity)     .. "'")
				LOG("  GetEntityType(): '" .. a_CBEntity:GetEntityType() .. "'")
				LOG("  GetClass():      '" .. a_CBEntity:GetClass()      .. "'")
			end
		)
		isInhHookInstalled = true
	end

	-- Create the projectile:
	LOG("Creating a " .. kindStr .. " projectile in world " .. world:GetName() .. "...")
	local msg
	world:ChunkStay({{0, 0}},
		nil,
		function ()
			-- Create a projectile at {8, 100, 8}:
			local entityID = world:CreateProjectile(8, 100, 8, kind, nil, nil)
			if (entityID < 0) then
				msg = "Cannot test inheritance, projectile creation failed."
				return
			end
			LOG("Entity created, ID #" .. entityID)

			-- Call a function on the newly created entity:
			local hasExecutedCallback = false
			world:DoWithEntityByID(
				entityID,
				function (a_CBEntity)
					LOG("Projectile created and found using the DoWithEntityByID() callback")
					LOG("Lua type:        '" .. type(a_CBEntity)           .. "'")
					LOG("ToLua type:      '" .. tolua.type(a_CBEntity)     .. "'")
					LOG("GetEntityType(): '" .. a_CBEntity:GetEntityType() .. "'")
					LOG("GetClass():      '" .. a_CBEntity:GetClass()      .. "'")
					hasExecutedCallback = true
				end
			)
			if not(hasExecutedCallback) then
				msg = "The callback failed to execute"
				return
			end

			msg = "Inheritance test finished"
		end
	)

	return true, msg
end





function HandleConsoleLoadChunk(a_Split)
	-- Check params:
	local numParams = #a_Split
	if (numParams ~= 3) and (numParams ~= 4) then
		return true, "Usage: " .. a_Split[1] .. " <ChunkX> <ChunkZ> [<WorldName>]"
	end

	-- Get the chunk coords:
	local chunkX = tonumber(a_Split[2])
	if (chunkX == nil) then
		return true, "Not a number: '" .. a_Split[2] .. "'"
	end
	local chunkZ = tonumber(a_Split[3])
	if (chunkZ == nil) then
		return true, "Not a number: '" .. a_Split[3] .. "'"
	end

	-- Get the world:
	local world
	if (a_Split[4] == nil) then
		world = cRoot:Get():GetDefaultWorld()
	else
		world = cRoot:Get():GetWorld(a_Split[4])
		if (world == nil) then
			return true, "There's no world named '" .. a_Split[4] .. "'."
		end
	end

	-- Queue a ChunkStay for the chunk, log a message when the chunk is loaded:
	world:ChunkStay({{chunkX, chunkZ}}, nil,
		function()
			LOG("Chunk [" .. chunkX .. ", " .. chunkZ .. "] is loaded")
		end
	)
	return true
end





function HandleConsoleLosTrace(a_Split)
	local world = cRoot:Get():GetDefaultWorld()
	local s = Vector3d(0, 70, 0)
	local e = Vector3d(100, 75, 100)
	if (tonumber(a_Split[2])) then
		s.x = tonumber(a_Split[2])
	end
	if (tonumber(a_Split[3])) then
		s.y = tonumber(a_Split[3])
	end
	if (tonumber(a_Split[4])) then
		s.z = tonumber(a_Split[4])
	end
	if (tonumber(a_Split[5])) then
		e.x = tonumber(a_Split[5])
	end
	if (tonumber(a_Split[6])) then
		e.y = tonumber(a_Split[6])
	end
	if (tonumber(a_Split[7])) then
		e.z = tonumber(a_Split[7])
	end
	local res = cLineBlockTracer:LineOfSightTrace(world, s, e, cLineBlockTracer.losAir)
	if (res) then
		return true, "The two points can see each other."
	else
		return true, "The two points cannot see each other"
	end
end





function HandleConsolePluginStats(a_Split)
	cPluginManager:ForEachPlugin(
		function (a_CBPlugin)
			LOG("Plugin in " .. a_CBPlugin:GetFolderName() .. " has an API name of " .. a_CBPlugin:GetName() .. " and status " .. a_CBPlugin:GetStatus())
		end
	)
	return true
end




function HandleConsolePrepareChunk(a_Split)
	-- Check params:
	local numParams = #a_Split
	if (numParams ~= 3) and (numParams ~= 4) then
		return true, "Usage: " .. a_Split[1] .. " <ChunkX> <ChunkZ> [<WorldName>]"
	end

	-- Get the chunk coords:
	local chunkX = tonumber(a_Split[2])
	if (chunkX == nil) then
		return true, "Not a number: '" .. a_Split[2] .. "'"
	end
	local chunkZ = tonumber(a_Split[3])
	if (chunkZ == nil) then
		return true, "Not a number: '" .. a_Split[3] .. "'"
	end

	-- Get the world:
	local world
	if (a_Split[4] == nil) then
		world = cRoot:Get():GetDefaultWorld()
	else
		world = cRoot:Get():GetWorld(a_Split[4])
		if (world == nil) then
			return true, "There's no world named '" .. a_Split[4] .. "'."
		end
	end

	-- Queue the chunk for preparing, log a message when prepared:
	world:PrepareChunk(chunkX, chunkZ,
		function(a_CBChunkX, a_CBChunkZ)
			LOG("Chunk [" .. chunkX .. ", " .. chunkZ .. "] has been prepared")
		end
	)
	return true
end





function HandleConsoleSchedule(a_Split)
	local prev = os.clock()
	LOG("Scheduling a task for 5 seconds in the future (current os.clock is " .. prev .. ")")
	cRoot:Get():GetDefaultWorld():ScheduleTask(5 * 20,
		function ()
			local current = os.clock()
			local diff = current - prev
			LOG("Scheduled function is called. Current os.clock is " .. current .. ", difference is " .. diff .. ")")
		end
	)
	return true, "Task scheduled"
end





--- Returns the square of the distance from the specified point to the specified line
local function SqDistPtFromLine(x, y, x1, y1, x2, y2)
	local dx = x - x1
	local dy = y - y1
	local px = x2 - x1
	local py = y2 - y1
	local ss = px * dx + py * dy
	local ds = px * px + py * py

	if (ss < 0) then
		-- Return sqdistance from point 1
		return dx * dx + dy * dy
	end
	if (ss > ds) then
		-- Return sqdistance from point 2
		return ((x2 - x) * (x2 - x) + (y2 - y) * (y2 - y))
	end

	-- Return sqdistance from the line
	if ((px * px + py * py) == 0) then
		return dx * dx + dy * dy
	else
		return (py * dx - px * dy) * (py * dx - px * dy) / (px * px + py * py)
	end
end





function HandleConsoleTestBbox(a_Split, a_EntireCmd)
	-- Test bbox intersection:
	local bbox1 = cBoundingBox(0, 5, 0, 5, 0, 5)
	local bbox2 = cBoundingBox(bbox1)  -- Make a copy
	bbox2:Move(20, 20, 20)
	local bbox3 = cBoundingBox(bbox1)  -- Make a copy
	bbox3:Move(2, 2, 2)
	local doesIntersect, intersection = bbox1:Intersect(bbox2)
	LOG("Bbox 2 intersection: " .. tostring(doesIntersect))
	LOG("  Intersection type: " .. type(intersection) .. " / " .. tolua.type(intersection))
	if (intersection) then
		LOG("  {" .. intersection:GetMinX() .. ", " .. intersection:GetMinY() .. ", " .. intersection:GetMinZ() .. "}")
		LOG("  {" .. intersection:GetMaxX() .. ", " .. intersection:GetMaxY() .. ", " .. intersection:GetMaxZ() .. "}")
	end
	doesIntersect, intersection = bbox1:Intersect(bbox3)
	LOG("Bbox 3 intersection: " .. tostring(doesIntersect))
	LOG("  Intersection type: " .. type(intersection) .. " / " .. tolua.type(intersection))
	if (intersection) then
		LOG("  {" .. intersection:GetMinX() .. ", " .. intersection:GetMinY() .. ", " .. intersection:GetMinZ() .. "}")
		LOG("  {" .. intersection:GetMaxX() .. ", " .. intersection:GetMaxY() .. ", " .. intersection:GetMaxZ() .. "}")
	end

	-- Test line intersection:
	local lines =
	{
		{ Vector3d(5, 0, 5), Vector3d(5, 1, 5) },
		{ Vector3d(0, 0, 0), Vector3d(0, 1, 0) },
	}
	for idx, line in ipairs(lines) do
		local doesIntersect, coeff, face = bbox2:CalcLineIntersection(line[1], line[2])
		LOG("Line " .. idx .. " intersection: " .. tostring(doesIntersect))
		LOG("  Coeff: " .. tostring(coeff))
		LOG("  Face: " .. tostring(face))
		local doesIntersect2, coeff2, face2 = cBoundingBox:CalcLineIntersection(bbox2:GetMin(), bbox2:GetMax(), line[1], line[2])
		assert(doesIntersect == doesIntersect2)
		assert(coeff == coeff2)
		assert(face == face2)
	end

	return true
end





function HandleConsoleTestCall(a_Split, a_EntireCmd)
	LOG("Testing inter-plugin calls")
	LOG("Note: These will fail if the Core plugin is not enabled")

	-- Test calling the HandleConsoleWeather handler:
	local pm = cPluginManager
	LOG("Calling Core's HandleConsoleWeather")
	local isSuccess = pm:CallPlugin("Core", "HandleConsoleWeather",
		{
			"/weather",
			"rain",
		}
	)
	if (type(isSuccess) == "boolean") then
		LOG("Success")
	else
		LOG("FAILED")
	end

	-- Test injecting some code:
	LOG("Injecting code into the Core plugin")
	isSuccess = pm:CallPlugin("Core", "dofile", pm:GetCurrentPlugin():GetLocalFolder() .. "/Inject.lua")
	if (type(isSuccess) == "boolean") then
		LOG("Success")
	else
		LOG("FAILED")
	end

	-- Test the full capabilities of the table-passing API, using the injected function:
	LOG("Calling injected code")
	isSuccess = pm:CallPlugin("Core", "injectedPrintParams",
		{
			"test",
			nil,
			{
				"test",
				"test"
			},
			[10] = "test",
			["test"] = "test",
			[{"test"}] = "test",
			[true] = "test",
		}
	)
	if (type(isSuccess) == "boolean") then
		LOG("Success")
	else
		LOG("FAILED")
	end

	return true
end





function HandleConsoleTestErr(a_Split, a_EntireCmd)
	cRoot:Get():GetDefaultWorld():ForEachEntity(
		function (a_CBEntity)
			error("This error should not abort the server")
		end
	)
end





function HandleConsoleTestJson(a_Split, a_EntireCmd)
	LOG("Testing Json parsing...")
	local t1 = cJson:Parse([[{"a": 1, "b": "2", "c": [3, "4", 5], "d": true }]])
	assert(t1.a == 1)
	assert(t1.b == "2")
	assert(t1.c[1] == 3)
	assert(t1.c[2] == "4")
	assert(t1.c[3] == 5)
	assert(t1.d == true)
	LOG("Json parsing example 1 successful")

	local t2, msg = cJson:Parse([[{"some": invalid, json}]])
	assert(t2 == nil)
	assert(type(msg) == "string")
	LOG("Json parsing an invalid string: Error message returned: " .. msg)

	local isSuccess, t3
	isSuccess, t3, msg = pcall(cJson.Parse, cJson, nil)
	if (isSuccess) then
		LOG(string.format("Json parsing a 'nil' produced a %s and a %s, msg is %s.",
			type(t3), type(msg), msg or "<nil>"
		))
	else
		LOG("Json parsing a 'nil' raised an error")
	end

	LOG("Json parsing test succeeded")

	LOG("Testing Json serializing...")
	local s1, msg1 = cJson:Serialize({a = 1, b = "2", c = {3, "4", 5}, d = true}, {indentation = " "})
	LOG("Serialization result: " .. (s1 or ("ERROR: " .. (msg1 or "<no message>"))))

	local s2, msg2 = cJson:Serialize({valueA = 1, valueB = {3, badValue = "4", 5}, d = true}, {indentation = " "})
	assert(not(s2), "Serialization should have failed")
	LOG("Serialization correctly failed with message: " .. (msg2 or "<no message>"))

	LOG("Json serializing test succeeded")

	return true
end





function HandleConsoleTestTracer(a_Split, a_EntireCmd)
	-- Check required params:
	if not(a_Split[7]) then
		return true, "Usage: " .. a_Split[1] .. " <x1> <y1> <z1> <x2> <y2> <z2> [<WorldName>]"
	end
	local Coords = {}
	for i = 1, 6 do
		local v = tonumber(a_Split[i + 1])
		if not(v) then
			return true, "Parameter " .. (i + 1) .. " (" .. tostring(a_Split[i + 1]) .. ") not a number "
		end
		Coords[i] = v
	end

	-- Get the world in which to test:
	local World
	if (a_Split[8]) then
		World = cRoot:GetWorld(a_Split[2])
	else
		World = cRoot:Get():GetDefaultWorld()
	end
	if not(World) then
		return true, "No such world"
	end

	-- Define the callbacks to use for tracing:
	local Callbacks =
	{
		OnNextBlock = function(a_BlockX, a_BlockY, a_BlockZ, a_BlockType, a_BlockMeta, a_EntryFace)
			LOG(string.format("{%d, %d, %d}: %s", a_BlockX, a_BlockY, a_BlockZ, ItemToString(cItem(a_BlockType, 1, a_BlockMeta))))
		end,
		OnNextBlockNoData = function(a_BlockX, a_BlockY, a_BlockZ, a_EntryFace)
			LOG(string.format("{%d, %d, %d} (no data)", a_BlockX, a_BlockY, a_BlockZ))
		end,
		OnNoChunk = function()
			LOG("Chunk not loaded")
		end,
		OnNoMoreHits = function()
			LOG("Trace finished")
		end,
		OnOutOfWorld = function()
			LOG("Out of world")
		end,
		OnIntoWorld = function()
			LOG("Into world")
		end,
	}

	-- Approximate the chunks needed for the trace by iterating over all chunks and measuring their center's distance from the traced line
	local Chunks = {}
	local sx = math.floor(Coords[1] / 16)
	local sz = math.floor(Coords[3] / 16)
	local ex = math.floor(Coords[4] / 16)
	local ez = math.floor(Coords[6] / 16)
	local sgnx = (sx < ex) and 1 or -1
	local sgnz = (sz < ez) and 1 or -1
	for z = sz, ez, sgnz do
		local ChunkCenterZ = z * 16 + 8
		for x = sx, ex, sgnx do
			local ChunkCenterX = x * 16 + 8
			local sqdist = SqDistPtFromLine(ChunkCenterX, ChunkCenterZ, Coords[1], Coords[3], Coords[4], Coords[6])
			if (sqdist <= 128) then
				table.insert(Chunks, {x, z})
			end
		end
	end

	-- Load the chunks and do the trace once loaded:
	World:ChunkStay(Chunks,
		nil,
		function()
			cLineBlockTracer:Trace(World, Callbacks, Coords[1], Coords[2], Coords[3], Coords[4], Coords[5], Coords[6])
		end
	)
	return true
end





function HandleConsoleTestUrlClient(a_Split, a_EntireCmd)
	local url = a_Split[2] or "https://github.com"
	local isSuccess, msg = cUrlClient:Get(url,
		function (a_Body, a_SecondParam)
			if not(a_Body) then
				-- An error has occurred, a_SecondParam is the error message
				LOG("Error while retrieving URL \"" .. url .. "\": " .. (a_SecondParam or "<no message>"))
				return
			end
			-- Body received, a_SecondParam is the HTTP headers dictionary-table
			assert(type(a_Body) == "string")
			assert(type(a_SecondParam) == "table")
			LOG("URL body received, length is " .. string.len(a_Body) .. " bytes and there are these headers:")
			for k, v in pairs(a_SecondParam) do
				LOG("  \"" .. k .. "\": \"" .. v .. "\"")
			end
			LOG("(headers list finished)")
		end
	)
	if not(isSuccess) then
		LOG("cUrlClient request failed: " .. (msg or "<no message>"))
	end
	return true
end





function HandleConsoleTestUrlParser(a_Split, a_EntireCmd)
	LOG("Testing cUrlParser...")
	local UrlsToTest =
	{
		"invalid URL",
		"https://github.com",
		"ftp://anonymous:user@example.com@ftp.cuberite.org:9921/releases/2015/2015-12-25.zip",
		"ftp://anonymous:user:name:with:colons@example.com@ftp.cuberite.org:9921",
		"https://google.com/",
		"https://google.com/?q=cuberite",
		"https://google.com/search?q=cuberite",
		"https://google.com/some/search?q=cuberite#results",
		"https://google.com/?q=cuberite#results",
		"https://google.com/#results",
		"ftp://cuberite.org:9921/releases/2015/2015-12-25.zip",
		"mailto:support@cuberite.org",
	}
	for _, u in ipairs(UrlsToTest) do
		LOG("URL: " .. u)
		local scheme, username, password, host, port, path, query, fragment = cUrlParser:Parse(u)
		if not(scheme) then
			LOG("  Error: " .. (username or "<nil>"))
		else
			LOG("  Scheme   = " .. scheme)
			LOG("  Username = " .. username)
			LOG("  Password = " .. password)
			LOG("  Host     = " .. host)
			LOG("  Port     = " .. port)
			LOG("  Path     = " .. path)
			LOG("  Query    = " .. query)
			LOG("  Fragment = " .. fragment)
		end
	end
	LOG("cUrlParser test complete")
	return true
end





function HandleConsoleUuid(a_Split, a_EntireCmd)
	-- Check params:
	local playerName = a_Split[2]
	if not(playerName) then
		return true, "Usage: uuid <PlayerName>"
	end

	-- Query with cache:
	LOG("Player " .. playerName .. ":")
	local cachedUuid = cMojangAPI:GetUUIDFromPlayerName(playerName, true)
	if not(cachedUuid) then
		LOG("  - not in the UUID cache")
	else
		LOG("  - in the cache: \"" .. cachedUuid .. "\"")
	end

	-- Query online:
	local onlineUuid = cMojangAPI:GetUUIDFromPlayerName(playerName, false)
	if not(onlineUuid) then
		LOG("  - UUID not available online")
	else
		LOG("  - online: \"" .. onlineUuid .. "\"")
	end

	return true
end





function HandleConsoleBBox(a_Split)
	local bbox = cBoundingBox(0, 10, 0, 10, 0, 10)
	local v1 = Vector3d(1, 1, 1)
	local v2 = Vector3d(5, 5, 5)
	local v3 = Vector3d(11, 11, 11)

	if (bbox:IsInside(v1)) then
		LOG("v1 is inside bbox")
	else
		LOG("v1 is not inside bbox")
	end

	if (bbox:IsInside(v2)) then
		LOG("v2 is inside bbox")
	else
		LOG("v2 is not inside bbox")
	end

	if (bbox:IsInside(v3)) then
		LOG("v3 is inside bbox")
	else
		LOG("v3 is not inside bbox")
	end

	if (bbox:IsInside(v1, v2)) then
		LOG("v1*v2 is inside bbox")
	else
		LOG("v1*v2 is not inside bbox")
	end

	if (bbox:IsInside(v2, v1)) then
		LOG("v2*v1 is inside bbox")
	else
		LOG("v2*v1 is not inside bbox")
	end

	if (bbox:IsInside(v1, v3)) then
		LOG("v1*v3 is inside bbox")
	else
		LOG("v1*v3 is not inside bbox")
	end

	if (bbox:IsInside(v2, v3)) then
		LOG("v2*v3 is inside bbox")
	else
		LOG("v2*v3 is not inside bbox")
	end

	return true
end





function HandleConsoleDeadlock(a_Split)
	-- If given a parameter, assume it's a world name and simulate a deadlock in the world's tick thread
	if (a_Split[2]) then
		local world = cRoot:Get():GetWorld(a_Split[2])
		if (world) then
			world:ScheduleTask(0,
				function()
					-- Make a live-lock:
					while (true) do
					end
				end
			)
			return true, "Deadlock in world tick thread for world " .. a_Split[2] .. " has been scheduled."
		end
		LOG("Not a world name: " .. a_Split[2] .. "; simulating a deadlock in the command execution thread instead.")
	else
		LOG("Simulating a deadlock in the command execution thread.")
	end

	-- Make a live-lock in the command execution thread:
	while(true) do
	end
end





function HandleConsoleDownload(a_Split)
	-- Check params:
	local url = a_Split[2]
	local fnam = a_Split[3]
	if (not(url) or not(fnam)) then
		return true, "Missing parameters. Usage: download <url> <filename>"
	end

	local callbacks =
	{
		OnStatusLine = function (self, a_HttpVersion, a_Status, a_Rest)
			if (a_Status ~= 200) then
				LOG("Cannot download " .. url .. ", HTTP error code " .. a_Status)
				return
			end

			local f, err = io.open(fnam, "wb")
			if not(f) then
				LOG("Cannot download " .. url .. ", error opening the file " .. fnam .. ": " .. (err or "<no message>"))
				return
			end
			self.m_File = f
		end,

		OnBodyData = function (self, a_Data)
			if (self.m_File) then
				self.m_File:write(a_Data)
			end
		end,

		OnBodyFinished = function (self)
			if (self.m_File) then
				self.m_File:close()
				LOG("File " .. fnam .. " has been downloaded.")
			end
		end,
	}

	local isSuccess, msg = cUrlClient:Get(url, callbacks)
	if not(isSuccess) then
		LOG("Cannot start an URL download: " .. (msg or "<no message>"))
		return true
	end
	return true
end





function HandleBlkCmd(a_Split, a_Player)
	-- Gets info about the block the player is looking at.
	local World = a_Player:GetWorld();

	local Callbacks = {
		OnNextBlock = function(a_BlockX, a_BlockY, a_BlockZ, a_BlockType, a_BlockMeta)
			if (a_BlockType ~= E_BLOCK_AIR) then
				a_Player:SendMessage("Block at " .. a_BlockX .. ", " .. a_BlockY .. ", " .. a_BlockZ .. " is " .. a_BlockType .. ":" .. a_BlockMeta)
				return true;
			end
		end
	};

	local EyePos = a_Player:GetEyePosition();
	local LookVector = a_Player:GetLookVector();
	LookVector:Normalize();

	local End = EyePos + LookVector * 50;

	cLineBlockTracer.Trace(World, Callbacks, EyePos.x, EyePos.y, EyePos.z, End.x, End.y, End.z);

	return true;
end





function HandleBoomCmd(a_Split, a_Player)
	local playerPos = a_Player:GetPosition()
	a_Player:GetWorld():BroadcastParticleEffect("smoke", Vector3f(playerPos), Vector3f(), 0, 900)
	a_Player:GetWorld():BroadcastSoundEffect("entity.firework.large_blast", playerPos, 1, 1)
	a_Player:SendMessage("BOOM!")
	return true
end





function HandleTeamsCmd(a_Split, a_Player)
	local Scoreboard = a_Player:GetWorld():GetScoreBoard()

	a_Player:SendMessage("Teams: " .. table.concat(Scoreboard:GetTeamNames(), ", "))

	return true
end