-- Implements give and item commands and console commands -- Table to store the blacklist local ItemBlackList = {} local CommandUsage = "Usage: %s %s" local ItemCommandUsageTail = " [amount] [data] [dataTag]" local GiveCommandUsageTail = " " .. ItemCommandUsageTail local MessagePlayerFailure = "Player not found" local MessageAmountFailure = "The number you have entered ( %d ) is too big, it must be at most 64" local MessageItemNameFailure = "There is no such item with name [ %s ]" local MessageDataTagFailure = "Error processing dataTag: " local MessageUnknownError = "< Unknown Error >" local MessageGiveSuccessful = "Given [ %s ] x %d" local MessageBadAmount = "Amount must be a number" local MessageBadData = "Data must be a number" local UnbalancedCurleyBracesFailure = "Missing or unexpected '{' or '}' detected" local UnbalancedSquareBracketsFailure = "Missing or unexpected '[' or ']' detected" local StartWithBraceFailure = "DataTag must start with a '{'" local EndWithBraceFailure = "DataTag must end with a '}'" local BlackListHeaderComment = "# Contains the list of items that cannot be obtained through the give and item commands.\n" local BlackListHeaderComment2 = "# Add items to this file to add them to the blacklist\n" local BlackListHeaderComment3 = "# or remove them from this file to remove them from the blacklist.\n" local BlackListFileName = "itemblacklist" local BlackListFileCreationError = "%s: Could not create the file: %s\nError Message: %s" local MaxNumberOfItems = 64 --- Takes the given NBT DataTag and converts it to a table -- -- @param DataTag String in the format of a minecraft NBT data tag -- -- @return Table of the data tag values if successful, and nil with an error message if a problem is encountered -- local function SplitDataTag( DataTag ) -- Table where the assembled DataTag table is temporarily stored local Sandbox = {} -- Table where users strings are stored to protect from processing local SavedStrings = {} --- Save user strings to table to preserve them from additional processing -- -- @param StringToSave The string to be saved -- -- @return The string "%s#" where # is the position in the table where the string was saved -- local function PreserveString( StringToSave ) table.insert( SavedStrings, StringToSave ) return "\"%s" .. table.getn( SavedStrings ) .. "\"" end --- Handle the un-escaped strings that Mojang allows into vanilla minecraft data tags -- and store them into the SavedStrings table -- -- @param StringToFix The string to be escaped -- -- @return The string ":[%s#]" where # is the position in the table where the string was saved -- local function HandleUnquotedString( StringToFix ) StringToFix = string.gsub( StringToFix, ",", "\",\"" ) return ":[" .. PreserveString( StringToFix ) .. "]" end --- Implements basic sanity checking on the DataTag to generate more meaningful error messages -- -- @return True if checks pass, false with an error message otherwise -- local function VerifyDataTagFormat() -- Verify string starts and ends with expected brace if string.sub( DataTag, 1, 1 ) ~= "{" then return nil, StartWithBraceFailure end if string.sub( DataTag, -1, -1 ) ~= "}" then return nil, EndWithBraceFailure end -- Check for balanced curly braces and square brackets local FirstLoc, LastLoc = string.find( DataTag, "%b{}" ) if FirstLoc ~= 1 or LastLoc ~= string.len( DataTag ) then return nil, UnbalancedCurleyBracesFailure end local DataTagLen = string.len( DataTag ) FirstLoc, LastLoc = string.find( DataTag, "%b[]" ) if FirstLoc and LastLoc then local FirstBrace = math.min( string.find( DataTag, "%[" ) or DataTagLen , string.find( DataTag, "%]" ) or DataTagLen ) local LastBrace = math.max( string.find( DataTag, "%[", LastLoc + 1 ) or 0, string.find( DataTag, "%]", LastLoc + 1 ) or 0) -- Loop over the string and make sure we've found all the square bracket pairs while LastLoc < LastBrace do local _, Tmp = string.find( DataTag, "%b[]", LastLoc + 1 ) if not Tmp then break end LastLoc = Tmp Tmp = math.max( string.find( DataTag, "%[", LastLoc + 1 ) or 0 , string.find( DataTag, "%]", LastLoc + 1 ) or 0 ) if Tmp and Tmp > LastBrace then LastBrace = Tmp end end if ( FirstLoc > FirstBrace ) or ( LastLoc < LastBrace ) then return nil, UnbalancedSquareBracketsFailure end end return true end -- This preserves the users names, lore, etc before further processing -- We run prior to correcting escaping unquoted strings to allow for any -- combination of symbols in strings that are entered by the user with -- the appropriate leading and trailing quotes in place DataTag = string.gsub( DataTag, "\"([^\"]+)\"", PreserveString ) -- This handles the unquoted strings in-between certain square brackets that vanilla allows -- Currently you cannot use the a `]` in those strings, -- but everything else is handled DataTag = string.gsub( DataTag, ":%s-%[%s-([^{\"%s][^%]]+[^\"])%]", HandleUnquotedString ) -- Preliminary sanity checks on DataTag local Success, errMsg = VerifyDataTagFormat() if not Success then return nil, errMsg end -- Modify the vanilla minecraft dataTag format to a lua table format DataTag = string.gsub( DataTag, ":", "=" ) DataTag = string.gsub( DataTag, "%[", "{" ) DataTag = string.gsub( DataTag, "%]", "}" ) -- Restore lore, names, etc. into the string for index, value in ipairs( SavedStrings ) do DataTag = string.gsub( DataTag, "%%s" .. index, value ) end -- Load the DataTag string into lua local DataTagFunc, err = loadstring( "dt = " .. DataTag ) if not DataTagFunc then return nil, err end setfenv(DataTagFunc, Sandbox) -- TODO: Investigate switching to xpcall() for better error handling local Success, errMsg = pcall(DataTagFunc) if not Success then return nil, errMsg end return Sandbox.dt, nil end --- Handles `give` and `item` commands, other then usage strings -- which are taken care of in their registered handlers -- -- @param SafeCommand If true, then don't give unsafe items, if false then don't filter out items -- -- @return False if PlayerName or ItemName are missing, or Amount or DataValue are invalid, true otherwise -- local function GiveItemCommand( Split, Player, SafeCommand ) local PlayerName = Split[2] local lcPlayerName = string.lower( PlayerName or "" ) local ItemName = Split[3] local Amount = tonumber( Split[4] ) or 1 local DataValue = tonumber( Split[5] ) or 0 if not PlayerName or not ItemName or Amount < 1 or DataValue < 0 then return false end -- Make sure that if an amount was given, that it was actually a number if Split[4] and not tonumber( Split[4] ) then if Player then SendMessageFailure( Player, MessageBadAmount ) else LOG( MessageBadAmount ) end return true end -- Make sure that if a data value was given that it was actually a number if Split[5] and not tonumber( Split[5] ) then if Player then SendMessageFailure( Player, MessageBadData ) else LOG( MessageBadData ) end return true end -- Get the item from the arguments and check it's valid. local Item = cItem() local FoundItem = StringToItem( ItemName .. ( DataValue ~= 0 and ( ":" .. DataValue ) or "" ), Item) -- StringToItem does not check if item is valid if not IsValidItem( Item.m_ItemType ) then FoundItem = false end --- Checks to see if the given item is on the blacklist -- -- @return False if checking is disabled or if the item is not blacklisted, true otherwise -- local function CheckUnSafeItem() -- If using unsafegive or unsafeitem, do not check if item is blacklisted if not SafeCommand then return false end return ItemBlackList[ Item.m_ItemType ] end -- Check to see if the item is blacklisted if CheckUnSafeItem() then FoundItem = false end -- If the Item doesn't exist, return and inform the caller if not FoundItem then local Message = string.format( MessageItemNameFailure, ItemName ) if Player then SendMessageFailure( Player, Message ) else LOG( Message ) end return true end -- Process the DataTag information, if available if Split[6] then -- The DataTag values may be split across a few different indicies if it contains any spaces, -- so concat the remainder of split together for processing local DataTagTable, errMsg = SplitDataTag( table.concat( Split, " ", 6 ) ) if not DataTagTable then local Message = string.format( "%s%s", MessageDataTagFailure, errMsg or MessageUnknownError ) if Player then SendMessageFailure( Player, Message ) else LOG( Message ) end return true end if DataTagTable.display then -- Set a custom name if given if DataTagTable.display.Name then Item.m_CustomName = DataTagTable.display.Name end -- Set Lore if given if DataTagTable.display.Lore then Item.m_LoreTable = DataTagTable.display.Lore end end -- Add enchantments, if given and the item is enchantable if DataTagTable.ench and cItem:IsEnchantable(Item.m_ItemType, true) then for _, enchants in ipairs(DataTagTable.ench) do Item.m_Enchantments:SetLevel(enchants.id,enchants.lvl) end end end -- Vanilla only allows a player to receive 64 items at a time, respecting that limit here if Amount > MaxNumberOfItems then local Message = string.format(MessageAmountFailure, Amount) if Player then SendMessageFailure( Player, Message ) else LOG( Message ) end return true end --- Callback function used to add items to a players inventory -- -- @param NewPlayer The cPlayer object representing the player to give the item to -- -- @return True if successful, false if the player cannot be found -- local function giveItems( NewPlayer ) if string.lower( NewPlayer:GetName() ) ~= lcPlayerName then return false end Item:AddCount(Amount - 1) NewPlayer:GetInventory():AddItem( Item ) local MessageHead = string.format( MessageGiveSuccessful, ( ( Item.m_CustomName ~= "" ) and Item.m_CustomName or ItemToString( Item ) ), Amount ) local MessageTail = " to " .. NewPlayer:GetName() SendMessageSuccess( NewPlayer, MessageHead ) if Player and NewPlayer:GetName() ~= Player:GetName() then SendMessageSuccess( Player, MessageHead .. MessageTail ) end LOG( ( Player and Player:GetName() or "Console" ) .. ": " .. MessageHead .. MessageTail ) return true end -- Finally give the items to the player. -- Check to make sure that giving items was successful. if not cRoot:Get():FindAndDoWithPlayer( PlayerName, giveItems ) then if Player then SendMessageFailure( Player, MessagePlayerFailure ) else LOG( MessagePlayerFailure ) end end return true end --- Handle the give console command, wrapper for HandleGiveCommand -- Necessary due to Cuberite now supplying additional parameters -- function HandleConsoleGive( Split, FullCmd ) return HandleGiveCommand( Split ) end --- Handle the `give` console and in-game command -- Usage: give [amount] [data] [dataTag] -- function HandleGiveCommand( Split, Player ) if not GiveItemCommand( Split, Player, true ) then local Message = string.format( CommandUsage, Split[1] , GiveCommandUsageTail ) if Player then SendMessage( Player, Message ) else LOG( Message ) end end return true end --- Handle the unsafegive console command, wrapper for HandleUnsafeGiveCommand -- Necessary due to Cuberite now supplying additional parameters -- function HandleConsoleUnsafeGive( Split, FullCmd ) return HandleUnsafeGiveCommand( Split ) end --- Handle the `unsafegive` console and in-game command -- Usage: unsafegive [amount] [data] [dataTag] -- function HandleUnsafeGiveCommand( Split, Player ) if not GiveItemCommand( Split, Player, false ) then local Message = string.format( CommandUsage, Split[1] , GiveCommandUsageTail ) if Player then SendMessage( Player, Message ) else LOG( Message ) end end return true end --- Handle the `item` in-game command -- Usage: item [amount] [data] [dataTag] -- function HandleItemCommand( Split, Player ) table.insert( Split, 2, Player:GetName() ) if not GiveItemCommand( Split, Player, true ) then local Message = string.format( CommandUsage, Split[1] , ItemCommandUsageTail ) SendMessage( Player, Message ) end return true end --- Handle the `unsafeitem` in-game command -- Usage: unsafeitem [amount] [data] [dataTag] -- function HandleUnsafeItemCommand( Split, Player ) table.insert( Split, 2, Player:GetName() ) if not GiveItemCommand( Split, Player, false ) then local Message = string.format( CommandUsage, Split[1] , ItemCommandUsageTail ) SendMessage( Player, Message ) end return true end --- Initialize the Item Blacklist -- If the blacklist file cannot be found it attempts to create a new one -- function IntializeItemBlacklist( Plugin ) -- Technical blocks that should NOT be given to players by default local DefaultBlackList = { [E_BLOCK_PISTON_EXTENSION] = true, [E_BLOCK_PISTON_MOVED_BLOCK] = true, [E_BLOCK_FLOWER_POT] = true, [E_BLOCK_BED] = true, [E_BLOCK_HEAD] = true, [E_BLOCK_SIGN_POST] = true, [E_BLOCK_WALLSIGN] = true, [E_BLOCK_BREWING_STAND] = true, [E_BLOCK_CAULDRON] = true, [E_BLOCK_OAK_DOOR] = true, [E_BLOCK_SPRUCE_DOOR] = true, [E_BLOCK_BIRCH_DOOR] = true, [E_BLOCK_JUNGLE_DOOR] = true, [E_BLOCK_ACACIA_DOOR] = true, [E_BLOCK_DARK_OAK_DOOR] = true, [E_BLOCK_IRON_DOOR] = true, [E_BLOCK_TRIPWIRE] = true, [E_BLOCK_REDSTONE_WIRE] = true, [E_BLOCK_REDSTONE_ORE_GLOWING] = true, [E_BLOCK_REDSTONE_TORCH_OFF] = true, [E_BLOCK_REDSTONE_REPEATER_ON] = true, [E_BLOCK_REDSTONE_REPEATER_OFF] = true, [E_BLOCK_REDSTONE_LAMP_ON] = true, [E_BLOCK_ACTIVE_COMPARATOR] = true, [E_BLOCK_INACTIVE_COMPARATOR] = true, [E_BLOCK_INVERTED_DAYLIGHT_SENSOR] = true, [E_BLOCK_STATIONARY_WATER] = true, [E_BLOCK_WATER] = true, [E_BLOCK_LAVA] = true, [E_BLOCK_STATIONARY_LAVA] = true, [E_BLOCK_CROPS] = true, [E_BLOCK_POTATOES] = true, [E_BLOCK_CARROTS] = true, [E_BLOCK_PUMPKIN_STEM] = true, [E_BLOCK_MELON_STEM] = true, [E_BLOCK_REEDS] = true, [E_BLOCK_NETHER_WART] = true, [E_BLOCK_COCOA_POD] = true, [E_BLOCK_CAKE] = true, [E_BLOCK_END_PORTAL] = true, [E_BLOCK_NETHER_PORTAL] = true, [E_BLOCK_FIRE] = true, [E_BLOCK_DOUBLE_RED_SANDSTONE_SLAB] = true, [E_BLOCK_DOUBLE_WOODEN_SLAB] = true, [E_BLOCK_DOUBLE_STONE_SLAB] = true, [E_BLOCK_WALL_BANNER] = true, [E_BLOCK_STANDING_BANNER] = true, [E_BLOCK_PURPUR_DOUBLE_SLAB] = true, [E_BLOCK_BEETROOTS] = true, [E_BLOCK_END_GATEWAY] = true, [E_BLOCK_FROSTED_ICE] = true, } -- First, try to open the Item BlackList file if it exists local ItemBlackListFile, ErrMsg = io.open( BlackListFileName, "rb" ) if ItemBlackListFile then -- If it exists, read in the blacklisted items for value in ItemBlackListFile:lines() do -- Ignore comments value = string.gsub( value, "#+%s-.*", "" ) -- Convert the name into an item to get its item type local TempItem = cItem() local Success = StringToItem( value, TempItem ) if Success and IsValidItem( TempItem.m_ItemType ) then ItemBlackList[TempItem.m_ItemType] = true end end ItemBlackListFile:close() else -- If we couldn't read the file, then try to create the file with the default blacklist ItemBlackListFile, ErrMsg = io.open( BlackListFileName, "wb" ) if ItemBlackListFile then ItemBlackListFile:write( BlackListHeaderComment, BlackListHeaderComment2, BlackListHeaderComment3, "\n\n" ) for index, _ in pairs( DefaultBlackList ) do ItemBlackListFile:write( ItemTypeToString( index ), "\n" ) end ItemBlackListFile:flush() ItemBlackListFile:close() else -- If we can't create the file, log it LOG( string.format( BlackListFileCreationError, Plugin:GetName(), BlackListFileName, ( ErrMsg or MessageUnknownError ) ) ) end ItemBlackList = DefaultBlackList end return true end