-- 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); -- _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("\nnext line\nanother line"); p:parse("text\n"); p:parse("\n"); p:parse("more text"); p:parse(""); 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 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 [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 "

The following objects have been returned by cFile:GetFolderContents():

"; end local g_Counter = 0 local g_JavaScript = [[ ]] function HandleRequest_StressTest(a_Request) if (a_Request.PostParams["counter"]) then g_Counter = g_Counter + 1 return tostring(g_Counter) end return g_JavaScript .. "

The counter below should be reloading as fast as possible

0
" 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: | If shape is p (cuboid selection), the params are sent individually for each corner click and have the following format: |||| 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 ") 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 HandleCompo(a_Split, a_Player) -- Send one composite message to self: local msg = cCompositeChat() msg:AddTextPart("Hello! ", "b@e") -- bold yellow msg:AddUrlPart("Cuberite", "http://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] .. " ") 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(), "http://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 --- 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] .. " []" 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 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] .. " []" 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 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) 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 "")))) 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 "")) 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] .. " []" 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 "")) 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 "")) 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", "http://google.com/", "http://google.com/?q=cuberite", "http://google.com/search?q=cuberite", "http://google.com/some/search?q=cuberite#results", "http://google.com/?q=cuberite#results", "http://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 "")) 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 " 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 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 " 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 "")) 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 "")) 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 HandleTeamsCmd(a_Split, a_Player) local Scoreboard = a_Player:GetWorld():GetScoreBoard() a_Player:SendMessage("Teams: " .. table.concat(Scoreboard:GetTeamNames(), ", ")) return true end