2389 lines
64 KiB
Lua
2389 lines
64 KiB
Lua
|
|
-- 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()
|
|
TestPluginCalls()
|
|
|
|
TestBlockAreasString()
|
|
TestStringBase64()
|
|
-- TestUUIDFromName()
|
|
-- TestRankMgr()
|
|
TestFileExt()
|
|
-- TestFileLastMod()
|
|
TestPluginInterface()
|
|
|
|
local LastSelfMod = cFile:GetLastModificationTime(a_Plugin:GetLocalFolder() .. "/Debuggers.lua")
|
|
LOG("Debuggers.lua last modified on " .. os.date("%Y-%m-%dT%H:%M:%S", LastSelfMod))
|
|
|
|
--[[
|
|
-- 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 TestPluginInterface()
|
|
cPluginManager:DoWithPlugin("Core",
|
|
function (a_CBPlugin)
|
|
if (a_CBPlugin:GetStatus() == cPluginManager.psLoaded) then
|
|
LOG("Core plugin was found, version " .. a_CBPlugin:GetVersion())
|
|
else
|
|
LOG("Core plugin is not loaded")
|
|
end
|
|
end
|
|
)
|
|
|
|
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
|
|
)
|
|
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 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 TestPluginCalls()
|
|
-- In order to test the inter-plugin communication, we're going to call Core's ReturnColorFromChar() function
|
|
-- It is a rather simple function that doesn't need any tables as its params and returns a value, too
|
|
-- Note the signature: function ReturnColorFromChar( Split, char ) ... return cChatColog.Gray ... end
|
|
-- The Split parameter should be a table, but it is not used in that function anyway,
|
|
-- so we can get away with passing nil to it.
|
|
|
|
LOG("Debuggers: Calling NoSuchPlugin.FnName()...")
|
|
cPluginManager:CallPlugin("NoSuchPlugin", "FnName", "SomeParam")
|
|
LOG("Debuggers: Calling Core.NoSuchFunction()...")
|
|
cPluginManager:CallPlugin("Core", "NoSuchFunction", "SomeParam")
|
|
LOG("Debuggers: Calling Core.ReturnColorFromChar(..., \"8\")...")
|
|
local Gray = cPluginManager:CallPlugin("Core", "ReturnColorFromChar", "split", "8")
|
|
if (Gray ~= cChatColor.Gray) then
|
|
LOGWARNING("Debuggers: Call failed, exp " .. cChatColor.Gray .. ", got " .. (Gray or "<nil>"))
|
|
else
|
|
LOG("Debuggers: Call succeeded")
|
|
end
|
|
LOG("Debuggers: Inter-plugin calls done.")
|
|
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 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 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 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 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] .. " <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(), "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] .. " <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 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 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 "<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",
|
|
"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 "<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 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 HandleTeamsCmd(a_Split, a_Player)
|
|
local Scoreboard = a_Player:GetWorld():GetScoreBoard()
|
|
|
|
a_Player:SendMessage("Teams: " .. table.concat(Scoreboard:GetTeamNames(), ", "))
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
|
|
|
|
|