├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .luacheckrc_plugin ├── Api ├── Check.lua └── Manage.lua ├── Classes ├── Clipboard.lua ├── ClipboardBlockTypeSource.lua ├── ClipboardStorage.lua ├── ConstantBlockTypeSource.lua ├── CraftScripts.lua ├── Expression.lua ├── Mask.lua ├── PlayerSelection.lua ├── PlayerState.lua ├── RandomBlockTypeSource.lua ├── ShapeGenerator.lua ├── ToolRegistrator.lua ├── UndoStack.lua └── Updater.lua ├── Commands ├── Biome.lua ├── Brush.lua ├── Clipboard.lua ├── Entities.lua ├── Generation.lua ├── History.lua ├── Navigation.lua ├── Region.lua ├── Schematic.lua ├── Scripting.lua ├── Selection.lua ├── Special.lua ├── Terraforming.lua └── Tool.lua ├── Config.lua ├── Constants.lua ├── Info.lua ├── LICENSE ├── LibrariesExpansion ├── math.lua ├── string.lua └── table.lua ├── README.md ├── Storage ├── ChangeScripts │ └── 1.sql ├── Queries │ ├── get_namedplayerselection.sql │ ├── get_playerselection.sql │ ├── remove_playerselection.sql │ ├── set_namedplayerselection.sql │ └── set_playerselection.sql └── Storage.lua ├── Tests ├── selection.lua └── test.config.cfg ├── WorldEdit.deproj ├── craftscripts ├── FloodyWater.lua └── Readme.md ├── functions.lua └── main.lua /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Install Lua 13 | run: | 14 | sudo apt install lua5.1 luarocks libsqlite3-dev 15 | sudo luarocks install luafilesystem 16 | sudo luarocks install lsqlite3 17 | sudo luarocks install luasocket 18 | sudo luarocks install luacheck 19 | sudo luarocks install luasec OPENSSL_LIBDIR=/usr/lib/x86_64-linux-gnu 20 | 21 | - name: Set up environment 22 | run: | 23 | wget -O ../InfoReg.lua https://raw.githubusercontent.com/cuberite/cuberite/master/Server/Plugins/InfoReg.lua 24 | mkdir ~/AutoAPI 25 | wget -O ~/AutoAPI.zip --no-check-certificate 'https://ci.appveyor.com/api/projects/cuberite/cuberite/artifacts/AutoAPI.zip?job=Windows-x64&pr=false&branch=master' 26 | unzip ~/AutoAPI.zip -d ~/AutoAPI 27 | wget -O ~/ManualAPI.zip --no-check-certificate 'https://ci.appveyor.com/api/projects/cuberite/cuberite/artifacts/ManualAPI.zip?job=Windows-x64&pr=false&branch=master' 28 | unzip ~/ManualAPI.zip -d ~ 29 | git clone https://github.com/cuberite/CuberitePluginChecker ~/Checker 30 | wget -O .luacheckrc --no-check-certificate 'https://ci.appveyor.com/api/projects/cuberite/cuberite/artifacts/.luacheckrc?job=Windows-x64&pr=false&branch=master' 31 | 32 | - name: Run tests 33 | run: | 34 | cd ~/Checker && lua CuberitePluginChecker.lua -p $GITHUB_WORKSPACE -a ~/AutoAPI -e ~/ManualAPI.lua -i APIImpl/All.lua -s $GITHUB_WORKSPACE/Tests/selection.lua -g 35 | cd $GITHUB_WORKSPACE && luacheck . --codes --exclude-files Tests/* 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.deuser 2 | Config.cfg 3 | @EnableMobDebug.lua 4 | Storage/storage.sqlite 5 | Storage/Backups/*.sqlite 6 | -------------------------------------------------------------------------------- /.luacheckrc_plugin: -------------------------------------------------------------------------------- 1 | -- This file contains plugin specific options or tables for luacheck 2 | -- It will be merged with cuberite's luacheck 3 | 4 | -- Ignore variables / warning codes 5 | -- Doc: http://luacheck.readthedocs.io/en/stable/warnings.html 6 | ignore = 7 | { 8 | "122", -- Mutating read-only global variable 9 | "131", -- Unused global variable 10 | "142", -- Setting undefined field of global math, string, table 11 | "143", -- Accessing undefined field of global math, table 12 | "211", -- Unused variable 13 | "221", -- Variable is never set 14 | "231", -- Variable is never accessed 15 | "311", -- Value assigned to variable is unused 16 | "411", -- Variable was previously defined 17 | "421", -- Shadowing definition of variable 18 | "423", -- Shadowing definition of loop variable 19 | "432", -- Shadowing upvalue argument 20 | "531", -- Left-hand side of assignment is too short 21 | "542", -- Empty if branch 22 | "631", -- Line too long 23 | } 24 | -------------------------------------------------------------------------------- /Api/Check.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --- Called before an operation to check whether other plugins allow the operation. 8 | -- returns true to abort operation, returns false to continue. 9 | -- a_HookName is the name of the hook to call. Everything after that are arguments for the hook. 10 | function CallHook(a_HookName, ...) 11 | assert(g_Hooks[a_HookName] ~= nil) 12 | 13 | for idx, callback in ipairs(g_Hooks[a_HookName]) do 14 | local res = cPluginManager:CallPlugin(callback.PluginName, callback.CallbackName, ...) 15 | if (res) then 16 | -- The callback wants to abort the operation 17 | return true 18 | end 19 | end 20 | 21 | return false 22 | end 23 | 24 | 25 | 26 | 27 | 28 | function GetMultipleBlockChanges(MinX, MaxX, MinZ, MaxZ, Player, World, Operation) 29 | local MinY = 256 30 | local MaxY = 0 31 | local Object = {} 32 | function Object:SetY(Y) 33 | if Y < MinY then 34 | MinY = Y 35 | elseif Y > MaxY then 36 | MaxY = Y 37 | end 38 | end 39 | 40 | function Object:Flush() 41 | local FinalCuboid = cCuboid( 42 | Vector3i(MinX, MinY, MinZ), 43 | Vector3i(MaxX, MaxY, MaxZ) 44 | ) 45 | return CallHook("OnAreaChanging", FinalCuboid, Player, World, Operation) 46 | end 47 | 48 | return Object 49 | end 50 | -------------------------------------------------------------------------------- /Api/Manage.lua: -------------------------------------------------------------------------------- 1 | 2 | -- api_Manage.lua 3 | 4 | -- Implements functions that can be called by external plugins to manipulate WorldEdit state 5 | 6 | 7 | 8 | 9 | 10 | g_Hooks = { 11 | ["OnAreaChanging"] = {}, -- Signature: function(a_AffectedAreaCuboid, a_Player, a_World, a_Operation) 12 | ["OnAreaChanged"] = {}, -- Signature: function(a_AffectedAreaCuboid, a_Player, a_World, a_Operation) 13 | ["OnAreaCopied"] = {}, -- Signature: function(a_Player, a_World, a_CopiedAreaCuboid) 14 | ["OnAreaCopying"] = {}, -- Signature: function(a_Player, a_World, a_CopiedAreaCuboid) 15 | ["OnPlayerSelectionChanged"] = {}, -- Signature: function(a_Player, a_Cuboid, a_PointNr) a_PointNr is nil if the entire selection changed. 16 | 17 | -- Not implemented 18 | ["OnPlayerSelectionChanging"] = {}, -- Signature: function(a_Player, a_Cuboid, a_PointNr) a_PointNr is nil if the entire selection changed. 19 | } 20 | 21 | 22 | 23 | 24 | 25 | -- Registers a WorldEdit hook. 26 | -- All arguments are strings. 27 | -- a_HookName is the name of the hook. (List can be seen above) 28 | -- a_PluginName is the name of the plugin that wants to register a callback 29 | -- a_CallbackName is the name of the function the plugin wants to use as the callback. 30 | function AddHook(a_HookName, a_PluginName, a_CallbackName) 31 | if ( 32 | (type(a_HookName) ~= "string") or 33 | (type(a_PluginName) ~= "string") or (a_PluginName == "") or not cPluginManager:Get():IsPluginLoaded(a_PluginName) or 34 | (type(a_CallbackName) ~= "string") or (a_CallbackName == "") 35 | ) then 36 | LOGWARNING("Invalid callback registration parameters.") 37 | LOGWARNING(" AddHook() was called with params " .. 38 | tostring(a_HookName or "") .. ", " .. 39 | tostring(a_PluginName or "") .. ", " .. 40 | tostring(a_CallbackName or "") 41 | ) 42 | 43 | return false 44 | end 45 | 46 | if (not g_Hooks[a_HookName]) then 47 | LOGWARNING("Plugin \"" .. a_PluginName .. "\" tried to register an unexisting hook called \"" .. a_HookName .. "\"") 48 | return false 49 | end 50 | 51 | table.insert(g_Hooks[a_HookName], {PluginName = a_PluginName, CallbackName = a_CallbackName}) 52 | return true 53 | end 54 | 55 | 56 | 57 | 58 | 59 | --- (OBSOLETE) Registers a function from an external plugin that will be called for each operation in the specified world 60 | -- Returns true to signalize call success to the caller 61 | -- Callbacks should have the following signature: 62 | -- function(a_AffectedAreaCuboid, a_Player, a_World, a_Operation) 63 | -- The callback should return true to abort the operation, false to continue. 64 | function RegisterAreaCallback(a_PluginName, a_FunctionName, a_WorldName) 65 | LOGWARNING("RegisterAreaCallback is obsolete. Please use AddHook(\"OnAreaChanging\", ...)") 66 | LOGWARNING(" The callback signature changed as well. All individual coordinates are now a single cCuboid") 67 | return AddHook("OnAreaChanging", a_PluginName, a_FunctionName) 68 | end 69 | 70 | 71 | 72 | 73 | 74 | --- (OBSOLETE) Registers a function from an external plugin that will be called for each time a player tries to select an new selection point. 75 | -- It returns true to signalize call success to the caller 76 | -- Callbacks should have the following signature: 77 | -- function(a_Player, a_PosX, a_PosY, a_PosZ, a_PointNr) 78 | -- a_PointNr can be 0 for Left click or 1 for right click. 79 | -- The callback should return true to abort the operation, or false to continue. 80 | function RegisterPlayerSelectingPoint(a_PluginName, a_FunctionName) 81 | LOGWARNING("RegisterPlayerSelectingPoint is obsolete. Please use AddHook(\"OnPlayerSelectionChanging\", ...)") 82 | LOGWARNING(" The callback signature changed as well. All individual coordinates are now a single cCuboid") 83 | return AddHook("OnPlayerSelectionChanging", a_PluginName, a_FunctionName) 84 | end 85 | 86 | 87 | 88 | 89 | 90 | --- Sets the player's selection to the specified cuboid. 91 | -- Returns true on success, false on failure. 92 | function SetPlayerCuboidSelection(a_Player, a_Cuboid) 93 | -- Check the params: 94 | if ( 95 | (tolua.type(a_Player) ~= "cPlayer") or 96 | (tolua.type(a_Cuboid) ~= "cCuboid") 97 | ) then 98 | LOGWARNING("Invalid SetPlayerCuboidSelection API function parameters.") 99 | LOGWARNING(" SetPlayerCuboidSelection() was called with param types \"" .. 100 | tolua.type(a_Player) .. "\" (\"cPlayer\" wanted) and \"" .. 101 | tolua.type(a_Cuboid) .. "\" (\"cCuboid\" wanted)." 102 | ) 103 | return false 104 | end 105 | 106 | -- Set the selection, both points: 107 | local State = GetPlayerState(a_Player) 108 | State.Selection:SetFirstPoint(a_Cuboid.p1.x, a_Cuboid.p1.y, a_Cuboid.p1.z) 109 | State.Selection:SetSecondPoint(a_Cuboid.p2.x, a_Cuboid.p2.y, a_Cuboid.p2.z) 110 | return true 111 | end 112 | 113 | 114 | 115 | 116 | 117 | --- Sets the specified corner-point of the player's cuboid selection to the specified Vector3i coord. 118 | -- Returns true if successful, false if selection not cuboid / other error 119 | function SetPlayerCuboidSelectionPoint(a_Player, a_PointNumber, a_CoordVector) 120 | -- Check the params: 121 | if ( 122 | (tolua.type(a_Player) ~= "cPlayer") or 123 | (tonumber(a_PointNumber) == nil) or 124 | (tolua.type(a_CoordVector) ~= "Vector3i") 125 | ) then 126 | LOGWARNING("Invalid SetPlayerCuboidSelectionPoint API function parameters.") 127 | LOGWARNING(" SetPlayerCuboidSelection() was called with param types \"" .. 128 | tolua.type(a_Player) .. "\" (\"cPlayer\" wanted), \"" .. 129 | type(a_PointNumber) .. "\" (\"number\" wanted) and \"" .. 130 | tolua.type(a_CoordVector) .. "\" (\"cVector3i\" wanted)." 131 | ) 132 | return false 133 | end 134 | 135 | -- Set the specified selection point: 136 | local State = GetPlayerState(a_Player) 137 | if (tonumber(a_PointNumber) == 1) then 138 | State.Selection:SetFirstPoint(a_CoordVector) 139 | elseif (tonumber(a_PointNumber) == 2) then 140 | State.Selection:SetSecondPoint(a_CoordVector) 141 | else 142 | LOGWARNING("Invalid SetPlayerCuboidSelectionPoint API function parameters.") 143 | LOGWARNING(" SetPlayerCuboidSelection() was called with invalid point number " .. a_PointNumber) 144 | return false 145 | end 146 | return true 147 | end 148 | 149 | 150 | 151 | 152 | 153 | --- If the player's selection is a cuboid (as oposed to sphere / cylinder / ...), returns true; 154 | -- Returns false if player's selection is not a cuboid 155 | function IsPlayerSelectionCuboid(a_Player) 156 | -- Current WE version has only cuboid selections 157 | return true 158 | end 159 | 160 | 161 | 162 | 163 | 164 | --- If the player's selection is a cuboid, sets a_CuboidToSet to the selection cuboid and returns true 165 | -- Returns false if player's selection is not a cuboid. 166 | -- Note that we can't return a cCuboid instance - it would be owned by this plugin's Lua state and it could 167 | -- delete it at any time, making the variable in the other state point to bad memory, crashing the server. 168 | function GetPlayerCuboidSelection(a_Player, a_CuboidToSet) 169 | -- Check the params: 170 | if ( 171 | (tolua.type(a_Player) ~= "cPlayer") or 172 | (tolua.type(a_CuboidToSet) ~= "cCuboid") 173 | ) then 174 | LOGWARNING("Invalid SetPlayerCuboidSelection API function parameters.") 175 | LOGWARNING(" SetPlayerCuboidSelection() was called with param types \"" .. 176 | tolua.type(a_Player) .. "\" (\"cPlayer\" wanted) and \"" .. 177 | tolua.type(a_CuboidToSet) .. "\" (\"cCuboid\" wanted)." 178 | ) 179 | return false 180 | end 181 | 182 | -- Set the output cuboid to the selection: 183 | local State = GetPlayerState(a_Player) 184 | a_CuboidToSet:Assign(State.Selection.Cuboid) 185 | return true 186 | end 187 | 188 | 189 | 190 | 191 | 192 | --- Pushes an undo from the current contents of the area 193 | -- a_World is the world where to read the area 194 | -- a_Cuboid is the area's coords (both points are inclusive) 195 | -- a_Description holds the user-visible description of the undo savepoint 196 | -- Assumes that all the chunks for the cuboid are in memory 197 | -- Returns true on success, false and optional message on failure 198 | function WEPushUndo(a_Player, a_World, a_Cuboid, a_Description) 199 | -- Check the params: 200 | if ( 201 | (tolua.type(a_Player) ~= "cPlayer") or 202 | (tolua.type(a_World) ~= "cWorld") or 203 | (tolua.type(a_Cuboid) ~= "cCuboid") or 204 | (type(a_Description) ~= "string") 205 | ) then 206 | LOGWARNING("Invalid WEPushUndo API function parameters.") 207 | LOGWARNING(" WePushUndo() was called with these param types:") 208 | LOGWARNING(" " .. tolua.type(a_Player) .. " (cPlayer wanted),") 209 | LOGWARNING(" " .. tolua.type(a_World) .. " (cWorld wanted),") 210 | LOGWARNING(" " .. tolua.type(a_Cuboid) .. " (cCuboid wanted),") 211 | LOGWARNING(" " .. type(a_Description) .. " (string wanted),") 212 | return false, "bad params" 213 | end 214 | 215 | -- Push the undo: 216 | local State = GetPlayerState(a_Player) 217 | return State.UndoStack:PushUndoFromCuboid(a_World, a_Cuboid, a_Description) 218 | end 219 | 220 | 221 | 222 | 223 | 224 | --- Pushes an undo from the current contents of the area 225 | -- a_World is the world where to read the area 226 | -- a_Cuboid is the area's coords (both points are inclusive) 227 | -- a_Description holds the user-visible description of the undo savepoint 228 | -- Will load all chunks required for the operation asynchronously 229 | -- When the undo gets pushed (or an error is detected preventing the push), 230 | -- the a_CallbackFunctionName is called in a_CallbackPluginName 231 | -- The callback receives two params, IsSuccess (bool) and FailureMessage (string or nil) 232 | -- Returns true on success, false and optional message on early failure 233 | function WEPushUndoAsync(a_Player, a_World, a_Cuboid, a_Description, a_CallbackPluginName, a_CallbackFunctionName) 234 | -- Check the params: 235 | if ( 236 | (tolua.type(a_Player) ~= "cPlayer") or 237 | (tolua.type(a_World) ~= "cWorld") or 238 | (tolua.type(a_Cuboid) ~= "cCuboid") or 239 | (type(a_Description) ~= "string") or 240 | (type(a_CallbackPluginName) ~= "string") or 241 | (type(a_CallbackFunctionName) ~= "string") 242 | ) then 243 | LOGWARNING("Invalid WEPushUndoAsync() API function parameters.") 244 | LOGWARNING(" WePushUndo() was called with these param types:") 245 | LOGWARNING(" " .. tolua.type(a_Player) .. " (cPlayer wanted),") 246 | LOGWARNING(" " .. tolua.type(a_World) .. " (cWorld wanted),") 247 | LOGWARNING(" " .. tolua.type(a_Cuboid) .. " (cCuboid wanted),") 248 | LOGWARNING(" " .. type(a_Description) .. " (string wanted),") 249 | LOGWARNING(" " .. type(a_CallbackPluginName) .. " (string wanted),") 250 | LOGWARNING(" " .. type(a_CallbackFunctionName) .. " (string wanted),") 251 | return false, "bad params" 252 | end 253 | 254 | -- if the input cuboid isn't sorted, create a sorted copy: 255 | if not(a_Cuboid:IsSorted()) then 256 | a_Cuboid = cCuboid(a_Cuboid) 257 | a_Cuboid:Sort() 258 | end 259 | 260 | -- Create a callback for the ChunkStay: 261 | local State = GetPlayerState(a_Player) -- a_Player may be deleted in the meantime, but the State table won't 262 | local OnAllChunksAvailable = function() 263 | local IsSuccess, Msg = State.UndoStack:PushUndoFromCuboid(a_World, a_Cuboid, a_Description) 264 | cPluginManager:CallPlugin(a_CallbackPluginName, a_CallbackFunctionName, IsSuccess, Msg) 265 | end 266 | 267 | -- Get a list of chunks that need to be present: 268 | local Chunks = ListChunksForCuboid(a_Cuboid) 269 | 270 | -- Initiate a ChunkStay operation, pushing the undo when all the chunks are available 271 | a_World:ChunkStay(Chunks, nil, OnAllChunksAvailable) 272 | return true 273 | end 274 | 275 | 276 | 277 | 278 | 279 | --- Loads and executes a string 280 | -- a_String is the function to load. This can be a normal string that is manualy composed, or a dumped string using string.dump 281 | -- All other arguments are passed to the function when called. 282 | -- Returns false + error when failed, and returns true + all return values when succesfull. 283 | function ExecuteString(a_String, ...) 284 | local Function, Error = loadstring(a_String) 285 | if (not Function) then 286 | return false, Error 287 | end 288 | 289 | return pcall(Function, ...) 290 | end 291 | -------------------------------------------------------------------------------- /Classes/Clipboard.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Clipboard.lua 3 | 4 | -- Implements the cClipboard clas representing a player's clipboard 5 | 6 | 7 | 8 | 9 | 10 | --- Class for storing the player's clipboard 11 | cClipboard = {} 12 | 13 | 14 | 15 | 16 | 17 | function cClipboard:new(a_Obj) 18 | a_Obj = a_Obj or {} 19 | setmetatable(a_Obj, cClipboard) 20 | self.__index = self 21 | 22 | -- Initialize the object members: 23 | a_Obj.Area = cBlockArea() 24 | 25 | return a_Obj 26 | end 27 | 28 | 29 | 30 | 31 | 32 | --- Copies the blocks in the specified sorted cuboid into clipboard 33 | function cClipboard:Copy(a_World, a_Cuboid, a_Offset) 34 | assert(tolua.type(a_World) == "cWorld") 35 | assert(tolua.type(a_Cuboid) == "cCuboid") 36 | 37 | local Offset = a_Offset or Vector3i() 38 | self.Area:Read(a_World, 39 | a_Cuboid.p1.x, a_Cuboid.p2.x, 40 | a_Cuboid.p1.y, a_Cuboid.p2.y, 41 | a_Cuboid.p1.z, a_Cuboid.p2.z 42 | ) 43 | self.Area:SetWEOffset(Offset) 44 | 45 | return self.Area:GetVolume() 46 | end 47 | 48 | 49 | 50 | 51 | 52 | --- Cuts the blocks from the specified sorted cuboid into clipboard 53 | -- Replaces the cuboid with air blocks 54 | function cClipboard:Cut(a_World, a_Cuboid, a_Offset) 55 | self:Copy(a_World, a_Cuboid, a_Offset) 56 | 57 | -- Replace everything with air: 58 | local Area = cBlockArea() 59 | Area:Create(a_Cuboid:DifX() + 1, a_Cuboid:DifY() + 1, a_Cuboid:DifZ() + 1) 60 | Area:Write(a_World, a_Cuboid.p1.x, a_Cuboid.p1.y, a_Cuboid.p1.z) 61 | 62 | -- Wake up the simulators in the area: 63 | a_World:WakeUpSimulatorsInArea(cCuboid( 64 | Vector3i(a_Cuboid.p1.x - 1, a_Cuboid.p1.y - 1, a_Cuboid.p1.z - 1), 65 | Vector3i(a_Cuboid.p2.x + 1, a_Cuboid.p2.y + 1, a_Cuboid.p2.z + 1) 66 | )) 67 | 68 | return self.Area:GetVolume() 69 | end 70 | 71 | 72 | 73 | 74 | 75 | --- Returns the cuboid that holds the area which would be affected by a paste operation 76 | function cClipboard:GetPasteDestCuboid(a_Player, a_UseOffset) 77 | assert(tolua.type(a_Player) == "cPlayer") 78 | assert(self:IsValid()) 79 | 80 | local MinX, MinY, MinZ = math.floor(a_Player:GetPosX()), math.floor(a_Player:GetPosY()), math.floor(a_Player:GetPosZ()) 81 | if (a_UseOffset) then 82 | local Offset = self.Area:GetWEOffset() 83 | MinX = MinX + Offset.x 84 | MinY = MinY + Offset.y 85 | MinZ = MinZ + Offset.z 86 | end 87 | 88 | local XSize, YSize, ZSize = self.Area:GetSize() 89 | return cCuboid(Vector3i(MinX, MinY, MinZ), Vector3i(MinX + XSize, MinY + YSize, MinZ + ZSize)) 90 | end 91 | 92 | 93 | 94 | 95 | 96 | --- Returns a string describing the clipboard size 97 | -- Format: "X * Y * Z (volume: N blocks)" 98 | -- If the clipboard isn't valid, returns a placeholder text 99 | function cClipboard:GetSizeDesc() 100 | if not(self:IsValid()) then 101 | return "no clipboard data" 102 | end 103 | 104 | local XSize, YSize, ZSize = self.Area:GetSize() 105 | local Volume = XSize * YSize * ZSize 106 | local Dimensions = XSize .. " * " .. YSize .. " * " .. ZSize .. " (volume: " 107 | if (Volume == 1) then 108 | return Dimensions .. "1 block)" 109 | else 110 | return Dimensions .. Volume .. " blocks)" 111 | end 112 | end 113 | 114 | 115 | 116 | 117 | 118 | --- Returns true if there's any content in the clipboard 119 | function cClipboard:IsValid() 120 | return (self.Area:GetDataTypes() ~= 0) 121 | end 122 | 123 | 124 | 125 | 126 | 127 | --- Loads the specified schematic file into the clipboard 128 | -- Returns true if successful, false if not 129 | function cClipboard:LoadFromSchematicFile(a_FileName) 130 | return self.Area:LoadFromSchematicFile(a_FileName) 131 | end 132 | 133 | 134 | 135 | 136 | 137 | --- Pastes the clipboard contents into the world relative to the player 138 | -- a_DstPoint is the optional min-coord Vector3i where to paste; if not specified, the default is used 139 | -- Returns the number of blocks pasted 140 | function cClipboard:Paste(a_Player, a_DstPoint) 141 | local World = a_Player:GetWorld() 142 | 143 | -- Write the area: 144 | self.Area:Write(World, a_DstPoint.x, a_DstPoint.y, a_DstPoint.z) 145 | 146 | -- Wake up simulators in the area: 147 | local XSize, YSize, ZSize = self.Area:GetSize() 148 | World:WakeUpSimulatorsInArea(cCuboid( 149 | Vector3i(a_DstPoint.x - 1, a_DstPoint.y, a_DstPoint.z - 1), 150 | Vector3i(a_DstPoint.x + XSize + 1, a_DstPoint.y + YSize + 1, a_DstPoint.z + ZSize + 1) 151 | )) 152 | 153 | return XSize * YSize * ZSize 154 | end 155 | 156 | 157 | 158 | 159 | 160 | --- Rotates the clipboard around the Y axis the specified number of quarter-rotations (90 degrees) 161 | -- Positive number of rotations turn CCW, negative number of rotations turn CW 162 | -- Intelligent rotating - does CW instead of 3 CCW rotations etc. 163 | -- TODO: Also rotates the player-relative offsets 164 | function cClipboard:Rotate(a_NumCCWQuarterRotations) 165 | local NumRots = math.fmod(a_NumCCWQuarterRotations, 4) 166 | if ((NumRots == -3) or (NumRots == 1)) then 167 | -- 3 CW rotations = 1 CCW rotation 168 | self.Area:RotateCCW() 169 | elseif ((NumRots == -2) or (NumRots == 2)) then 170 | -- -2 or 2 rotations is the same, use any rotation function twice 171 | self.Area:RotateCCW() 172 | self.Area:RotateCCW() 173 | elseif ((NumRots == -1) or (NumRots == 3)) then 174 | -- 3 CCW rotation = 1 CW rotation 175 | self.Area:RotateCW() 176 | elseif (NumRots == 0) then 177 | -- No rotation needed 178 | else 179 | error("Bad fmod result: " .. NumRots) 180 | end 181 | end 182 | 183 | 184 | 185 | 186 | 187 | --- Saves the clipboard to the specified .schematic file 188 | -- Assumes the clipboard is valid 189 | -- Returns true on success, false on failure 190 | function cClipboard:SaveToSchematicFile(a_FileName) 191 | assert(self:IsValid()) 192 | 193 | return self.Area:SaveToSchematicFile(a_FileName) 194 | end 195 | -------------------------------------------------------------------------------- /Classes/ClipboardBlockTypeSource.lua: -------------------------------------------------------------------------------- 1 | 2 | -- BlockDstClipboard.lua 3 | 4 | -- Implements the cClipboardBlockTypeSource class that allows doing actions using the player's clipboard 5 | -- If used in for example //set you'll see the clipboard in a repeating pattern. 6 | 7 | 8 | 9 | 10 | 11 | cClipboardBlockTypeSource = {} 12 | 13 | 14 | 15 | 16 | 17 | function cClipboardBlockTypeSource:new(a_Player) 18 | local State = GetPlayerState(a_Player) 19 | if (not State.Clipboard:IsValid()) then 20 | return false, "no clipboard data" 21 | end 22 | 23 | local Area = State.Clipboard.Area 24 | local Size = Vector3i(Area:GetSize()) 25 | 26 | local Obj = {} 27 | 28 | setmetatable(Obj, cClipboardBlockTypeSource) 29 | self.__index = self 30 | 31 | Obj.m_Area = Area 32 | Obj.m_Size = Size 33 | 34 | return Obj 35 | end 36 | 37 | 38 | 39 | 40 | 41 | -- Returns a block from the clipboard. 42 | function cClipboardBlockTypeSource:Get(a_X, a_Y, a_Z) 43 | local PosX = math.floor(a_X % self.m_Size.x) 44 | local PosY = math.floor(a_Y % self.m_Size.y) 45 | local PosZ = math.floor(a_Z % self.m_Size.z) 46 | 47 | return self.m_Area:GetRelBlockTypeMeta(PosX, PosY, PosZ) 48 | end 49 | 50 | 51 | 52 | 53 | 54 | -- Returns true if one of the blocks in the given table is in the clipboard. 55 | function cClipboardBlockTypeSource:Contains(a_BlockTypeList) 56 | local SizeX, SizeY, SizeZ = self.m_Area:GetCoordRange() 57 | 58 | for X = 0, SizeX do 59 | for Y = 0, SizeY do 60 | for Z = 0, SizeZ do 61 | local BlockType = self.m_Area:GetRelBlockType(X, Y, Z) 62 | if (a_BlockTypeList[BlockType]) then 63 | return true, BlockType 64 | end 65 | end 66 | end 67 | end 68 | 69 | return false 70 | end 71 | -------------------------------------------------------------------------------- /Classes/ClipboardStorage.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Schematic.lua 3 | 4 | -- Contains classes for saving and loading a clipboard depending on the requested format. 5 | 6 | 7 | 8 | 9 | 10 | --- Default values for cubeset metadata. 11 | local g_CubesetDefaultValues = 12 | { 13 | Metadata = 14 | { 15 | CubesetFormatVersion = 1, 16 | IntendedUse = "SinglePieceStructures", 17 | ["AllowedBiomes"] = "", 18 | ["GridSizeX"] = "750", 19 | ["GridSizeZ"] = "750", 20 | ["MaxOffsetX"] = "100", 21 | ["MaxOffsetZ"] = "100", 22 | }, 23 | PiecesMetadata = 24 | { 25 | IsStarting = "1", 26 | MergeStrategy = "msSpongePrint", 27 | MoveToGround = "0", 28 | ExpandFloorStrategy = "RepeatBottomTillNonAir", 29 | VerticalStrategy = "TerrainOrOceanTop|-2", 30 | DefaultWeight = "100", 31 | AllowedRotations = "7", 32 | } 33 | } 34 | 35 | 36 | 37 | 38 | local g_FormatExtensionMap = { 39 | ["mcedit"] = ".schematic", 40 | ["cubeset"] = ".cubeset" 41 | } 42 | 43 | 44 | 45 | 46 | cClipboardStorage = {} 47 | 48 | 49 | 50 | 51 | 52 | function cClipboardStorage:new(a_Obj, a_Clipboard) 53 | local obj = a_Obj or {} 54 | setmetatable(obj, cClipboardStorage) 55 | self.__index = self 56 | 57 | self.Clipboard = a_Clipboard 58 | return obj 59 | end 60 | 61 | 62 | 63 | 64 | 65 | -- Returns a sorted list containing all schematic and cubeset files in the schematics folder. 66 | -- If there are files with the same name, but different extensions then those are shown explicitly with their extensions. 67 | function cClipboardStorage:ListFiles() 68 | local dict = {} 69 | local files = cFile:GetFolderContents("schematics") 70 | 71 | local validExtensions = table.todictionary({".schematic", ".cubeset"}) 72 | for _, file in ipairs(files) do 73 | local filename, extension = file:match("^(%w+)(%.%w+)$") 74 | if (validExtensions[extension]) then 75 | dict[filename] = dict[filename] or {} 76 | table.insert(dict[filename], {name = filename, fullname = file}) 77 | end 78 | end 79 | local output = {} 80 | for fileWithoutExtension, variants in pairs(dict) do 81 | if (#variants == 1) then 82 | table.insert(output, fileWithoutExtension) 83 | else 84 | for _, variant in ipairs(variants) do 85 | table.insert(output, variant.fullname) 86 | end 87 | end 88 | end 89 | table.sort(output, 90 | function(f1, f2) 91 | return (string.lower(f1) < string.lower(f2)) 92 | end 93 | ) 94 | return output 95 | end 96 | 97 | 98 | 99 | 100 | 101 | --- Maps the requested format to the right file extension. 102 | function cClipboardStorage:GetExtension(a_Format) 103 | return g_FormatExtensionMap[a_Format] or false 104 | end 105 | 106 | 107 | 108 | 109 | 110 | --- Returns a path for the provided file. The extension depends on the requested format. 111 | function cClipboardStorage:FormatPath(a_FileName, a_Format) 112 | local Extension = "" 113 | if (a_Format) then 114 | Extension = cClipboardStorage:GetExtension(a_Format) 115 | if (not Extension) then 116 | return false, "Format not recognized." 117 | end 118 | end 119 | return "schematics/" .. a_FileName .. Extension 120 | end 121 | 122 | 123 | 124 | 125 | 126 | --- Searches for the requested file and loads it into the user's clipboard. 127 | -- Can load mcedit files and cubeset files. 128 | function cClipboardStorage:Load(a_FileName, a_Options) 129 | if (not a_FileName:match("^([%w%.]+)$")) then 130 | return false, "Filename must only contain letters with an extension being optional." 131 | end 132 | local foundPath, foundFormat 133 | for format, extension in pairs(g_FormatExtensionMap) do 134 | local path = cClipboardStorage:FormatPath(a_FileName, not a_FileName:endswith(extension) and format or nil) 135 | if (cFile:IsFile(path)) then 136 | foundPath = path 137 | foundFormat = format 138 | break 139 | end 140 | end 141 | 142 | if (not foundPath) then 143 | return false, "File not found." 144 | end 145 | 146 | if (foundFormat == "mcedit") then 147 | return self:LoadMCEdit(foundPath) 148 | elseif (foundFormat == "cubeset") then 149 | return self:LoadCubeset(foundPath, a_Options) 150 | end 151 | end 152 | 153 | 154 | 155 | 156 | 157 | --- Loads the provided file as a schematic file. 158 | function cClipboardStorage:LoadMCEdit(a_Path) 159 | self.Clipboard.Area:LoadFromSchematicFile(a_Path) 160 | return true 161 | end 162 | 163 | 164 | 165 | 166 | 167 | --- Tries to load the provided file as a cubeset file. 168 | function cClipboardStorage:LoadCubeset(a_Path, a_Options) 169 | local loader, err = loadfile(a_Path) 170 | if (not loader) then 171 | return false, "Unable to parse cubeset file." 172 | end 173 | local env = {} 174 | setfenv(loader, env) 175 | local success, err = pcall(loader) 176 | if (not success) then 177 | return false, "Unable to load cubeset file." 178 | end 179 | 180 | if (not env.Cubeset or not env.Cubeset.Metadata) then 181 | return false, "Cubeset file doesn't contain any metadata." 182 | end 183 | if (env.Cubeset.Metadata.CubesetFormatVersion == 1) then 184 | return self:LoadCubesetV1(env, a_Options) 185 | else 186 | return false, "Unknown Cubeset version." 187 | end 188 | end 189 | 190 | 191 | 192 | 193 | 194 | --- Loads the provided cubeset environment into the users clipboard. 195 | function cClipboardStorage:LoadCubesetV1(a_Env, a_Options) 196 | local options, err = ParseOptionsToDictionary(a_Options) 197 | if (not options) then 198 | return false, err 199 | end 200 | local pieceIdx = options.pieceIdx or 1 201 | local piece = a_Env.Cubeset.Pieces[pieceIdx] 202 | if (not piece) then 203 | return false, "The requested piece in the Cubeset file does not exist." 204 | end 205 | 206 | -- Create a dictionary symbol -> block type/meta based on the blockdefinitions. 207 | local blockDefinitions = {} 208 | for idx, blockDefinition in ipairs(piece.BlockDefinitions) do 209 | local split = StringSplitAndTrim(blockDefinition, ":") 210 | if (#split ~= 3) then 211 | return false, "Block Definitions in cubeset is corrupt at definition nr. " .. idx 212 | end 213 | blockDefinitions[split[1]] = {type = split[2], meta = split[3]} 214 | end 215 | 216 | -- Set all blocks in the cubeset blockdata to a new blockarea. 217 | -- A new blockarea is used so the user's current clipboard isn't corrupted if something is wrong in the cubeset's blockdata. 218 | local area = cBlockArea() 219 | area:Create(piece.Size.x, piece.Size.y, piece.Size.z) 220 | local blockdata = table.concat(piece.BlockData) 221 | for y = 0, piece.Size.y - 1 do 222 | for z = 0, piece.Size.z - 1 do 223 | for x = 0, piece.Size.x - 1 do 224 | local index = (x + z * piece.Size.x + y * piece.Size.x * piece.Size.z) + 1 -- Lua is 1-based. 225 | local symbol = blockdata:sub(index, index) 226 | local definition = blockDefinitions[symbol] 227 | if (not definition) then 228 | return false, ('Unknown block definition "%s" at (%s, %s, %s) in cubeset file.'):format(symbol, x, y, z) 229 | end 230 | area:SetRelBlockTypeMeta(x, y, z, definition.type, definition.meta) 231 | end 232 | end 233 | end 234 | 235 | -- Move the new blockarea to the user's clipboard. 236 | area:CopyTo(self.Clipboard.Area) 237 | return true 238 | end 239 | 240 | 241 | 242 | 243 | 244 | function cClipboardStorage:Save(a_Name, a_Format, a_Options) 245 | if (not a_Name:match("^(%w+)$")) then 246 | return false, "Filename must only contain letters." 247 | end 248 | 249 | local Format = a_Format:lower() 250 | local Path, Error = self:FormatPath(a_Name, Format) 251 | if (not Path) then 252 | return false, Error 253 | end 254 | 255 | -- Check if there already is a schematic with that name, and if so if we are allowed to override it. 256 | if (not g_Config.Schematics.OverrideExistingFiles and cFile:IsFile(Path)) then 257 | return false, "There already is a schematic with that name." 258 | end 259 | 260 | if (Format == "mcedit") then 261 | return self:SaveMCEdit(Path) 262 | elseif (Format == "cubeset") then 263 | return self:SaveCubesetV1(Path, a_Name, a_Options) 264 | end 265 | end 266 | 267 | 268 | 269 | 270 | 271 | function cClipboardStorage:SaveMCEdit(a_FileName) 272 | self.Clipboard.Area:SaveToSchematicFile(a_FileName) 273 | return true 274 | end 275 | 276 | 277 | 278 | 279 | 280 | --- Parses an array of key=value strings into an array with objects containing the key and value. 281 | -- If a name is prefixed with "piece." it's used as metadata of the piece itself, not as metadata of the cubeset file. 282 | function cClipboardStorage:ParseOptions(a_Options) 283 | local output = {} 284 | local isXZOption = table.todictionary({"MaxOffset", "GridSize"}) 285 | for _, option in ipairs(a_Options) do 286 | local optionName, value = option:match("^(.-)%=(.-)$") 287 | local isPieceOption, name = optionName:match("^(piece%.)(.-)$") 288 | name = name or optionName 289 | if (isXZOption[name]) then 290 | table.insert(output, {isPieceOption = isPieceOption ~= nil, name = name .. "X", value = value}) 291 | table.insert(output, {isPieceOption = isPieceOption ~= nil, name = name .. "Z", value = value}) 292 | else 293 | table.insert(output, {isPieceOption = isPieceOption ~= nil, name = name, value = value}) 294 | end 295 | end 296 | return output 297 | end 298 | 299 | 300 | 301 | 302 | 303 | --- Saves the current clipboard into a v1 cubeset format in the provided file. 304 | function cClipboardStorage:SaveCubesetV1(a_FileName, a_StructureName, a_Options) 305 | local area = self.Clipboard.Area 306 | local blockdata, definitions = self:GetBlockDefinitions(area) 307 | local output = { 308 | Metadata = table.merge({}, g_CubesetDefaultValues.Metadata), 309 | Pieces = 310 | { 311 | { 312 | Metadata = table.merge({}, g_CubesetDefaultValues.PiecesMetadata), 313 | OriginData = 314 | { 315 | ExportName = a_StructureName, 316 | Name = a_StructureName, 317 | }, 318 | Size = 319 | { 320 | x = area:GetSizeX(), 321 | y = area:GetSizeY(), 322 | z = area:GetSizeZ(), 323 | }, 324 | Hitbox = 325 | { 326 | MinX = 0, 327 | MinY = 0, 328 | MinZ = 0, 329 | MaxX = area:GetSizeX() - 1, 330 | MaxY = area:GetSizeY() - 1, 331 | MaxZ = area:GetSizeZ() - 1, 332 | }, 333 | StructureBox = 334 | { 335 | MinX = 0, 336 | MinY = 0, 337 | MinZ = 0, 338 | MaxX = area:GetSizeX() - 1, 339 | MaxY = area:GetSizeY() - 1, 340 | MaxZ = area:GetSizeZ() - 1, 341 | }, 342 | Connectors = {}, 343 | BlockDefinitions = 344 | { 345 | unpack(definitions) 346 | }, 347 | BlockData = 348 | { 349 | unpack(blockdata) 350 | } 351 | } 352 | } 353 | } 354 | 355 | -- Allow the user to override the default metadata. 356 | for _, option in ipairs(self:ParseOptions(a_Options)) do 357 | if (option.isPieceOption) then 358 | output.Pieces[1].Metadata[option.name] = option.value 359 | else 360 | output.Metadata[option.name] = option.value 361 | end 362 | end 363 | 364 | local file = io.open(a_FileName, "w") 365 | if (not file) then 366 | return false, "Could not open file" 367 | end 368 | file:write("Cubeset = \n{\n") 369 | 370 | -- Recursive function to dump the Lua table to the output file. 371 | local function write(a_Tabs, a_Obj) 372 | local prefix = string.rep("\t", a_Tabs) 373 | -- Prioritize the metadata table. 374 | -- Cuberite checks the first 8KiB for the Cubeset version when loading prefabs. 375 | -- If the file is too large and the metadata is at the end Cuberite won't be able to find it. 376 | if (a_Obj["Metadata"]) then 377 | file:write(prefix, '["Metadata"] =') 378 | file:write("\n", prefix, "{\n") 379 | write(a_Tabs + 1, a_Obj["Metadata"]) 380 | file:write("\n", prefix, "},\n") 381 | end 382 | for k, v in pairs(a_Obj) do 383 | if (k ~= "Metadata") then 384 | if (k == "CubesetFormatVersion") then 385 | file:write(prefix, k, ' =') 386 | elseif (type(k) == "string") then 387 | file:write(prefix, '["', k, '"] =') 388 | elseif (type(k) == "number") then 389 | file:write(prefix, '[', k, '] =') 390 | end 391 | if (type(v) == "table") then 392 | file:write("\n", prefix, "{\n") 393 | write(a_Tabs + 1, v) 394 | file:write("\n", prefix, "},\n") 395 | elseif (type(v) == "string") then 396 | file:write(' "', v, '",\n') 397 | elseif (type(v) == "number") then 398 | file:write(" ", v, ",\n") 399 | end 400 | end 401 | end 402 | end 403 | write(1, output) 404 | file:write("}") 405 | file:close() 406 | return true 407 | end 408 | 409 | 410 | 411 | 412 | 413 | --- Parses the provided cBlockArea into an array of blockdata and an array of blockdefinitions. 414 | function cClipboardStorage:GetBlockDefinitions(a_Area) 415 | local symbols = "abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*,<>/?;[{]}|_-=+~" 416 | local currentSymbol = 1 417 | 418 | -- In order to be similar to the GalExport plugin air always uses '.' and sponges always use 'm' 419 | local definitionDictionary = {["0:0"] = ".", ["19:0"] = "m"} 420 | local definitions = {".:0:0", "m:19:0"} 421 | local blockdata = {} 422 | local sizeX, sizeY, sizeZ = a_Area:GetCoordRange() 423 | 424 | for y = 0, sizeY do 425 | for z = 0, sizeZ do 426 | local blockline = "" 427 | for x = 0, sizeX do 428 | local blocktype, blockmeta = a_Area:GetRelBlockTypeMeta(x, y, z) 429 | local key = blocktype .. ":" .. blockmeta 430 | local symbol = definitionDictionary[key] 431 | if (not symbol) then 432 | symbol = symbols:sub(currentSymbol, currentSymbol) 433 | currentSymbol = currentSymbol + 1 434 | definitionDictionary[key] = symbol 435 | table.insert(definitions, symbol .. ":" .. key) 436 | end 437 | blockline = blockline .. symbol 438 | end 439 | table.insert(blockdata, blockline) 440 | end 441 | end 442 | return blockdata, definitions 443 | end 444 | 445 | 446 | 447 | 448 | 449 | -------------------------------------------------------------------------------- /Classes/ConstantBlockTypeSource.lua: -------------------------------------------------------------------------------- 1 | 2 | -- BlockDstConstant.lua 3 | 4 | -- Implements the cConstantBlockTypeSource class that always returns the same block 5 | 6 | 7 | 8 | 9 | 10 | cConstantBlockTypeSource = {} 11 | 12 | 13 | 14 | 15 | 16 | function cConstantBlockTypeSource:new(a_BlockString) 17 | local BlockType, BlockMeta = GetBlockTypeMeta(a_BlockString) 18 | if (not BlockType) then 19 | return false, a_BlockString -- On error the blockmeta is the block that isn't valid 20 | end 21 | 22 | local Obj = {} 23 | 24 | setmetatable(Obj, cConstantBlockTypeSource) 25 | self.__index = self 26 | 27 | Obj.m_BlockType = BlockType 28 | Obj.m_BlockMeta = BlockMeta 29 | 30 | return Obj 31 | end 32 | 33 | 34 | 35 | 36 | 37 | -- Always return the same blocktype and blockmeta. 38 | function cConstantBlockTypeSource:Get(a_X, a_Y, a_Z) 39 | return self.m_BlockType, self.m_BlockMeta 40 | end 41 | 42 | 43 | 44 | 45 | 46 | -- Returns true + blocktype if the block is in the given table as a key. Returns false otherwise. 47 | function cConstantBlockTypeSource:Contains(a_BlockTypeList) 48 | if (a_BlockTypeList[self.m_BlockType]) then 49 | return true, self.m_BlockType 50 | end 51 | return false 52 | end 53 | -------------------------------------------------------------------------------- /Classes/CraftScripts.lua: -------------------------------------------------------------------------------- 1 | 2 | -- CraftScripts.lua 3 | 4 | -- Implements the cCraftScript class representing a script for a single player. 5 | 6 | 7 | 8 | 9 | 10 | -- Only logs when debugging for craftscripts is enabled 11 | local function LOGSCRIPTERROR(a_Msg) 12 | if (not g_Config.Scripting.Debug) then 13 | return 14 | end 15 | 16 | LOGERROR(a_Msg) 17 | end 18 | 19 | 20 | 21 | 22 | 23 | -- All the variables in _G that a craftscript isn't allowed to use. 24 | local g_BlockedFunctions = table.todictionary{ 25 | "rawset", 26 | "rawget", 27 | "setfenv", 28 | "io", 29 | "os", 30 | "debug", 31 | "cFile", 32 | "loadstring", 33 | "loadfile", 34 | "load", 35 | "dofile", 36 | "ExecuteString", 37 | "_G", 38 | "cPluginManager", 39 | } 40 | 41 | 42 | 43 | 44 | 45 | local g_CraftScriptEnvironment = setmetatable({}, { 46 | __index = function(_, a_Key) 47 | if (g_BlockedFunctions[a_Key]) then 48 | local ScriptInfo = debug.getinfo(2) 49 | error("Craftscript tried to use blocked variable at line " .. ScriptInfo.currentline .. " in file " .. ScriptInfo.short_src) 50 | return nil 51 | end 52 | return _G[a_Key] 53 | end 54 | } 55 | ) 56 | 57 | 58 | 59 | 60 | 61 | --- Class for storing a players selected script 62 | cCraftScript = {} 63 | 64 | 65 | 66 | 67 | 68 | function cCraftScript:new(a_Obj) 69 | a_Obj = a_Obj or {} 70 | setmetatable(a_Obj, cCraftScript) 71 | self.__index = self 72 | 73 | -- Initialize the object members: 74 | a_Obj.SelectedScript = nil 75 | 76 | return a_Obj; 77 | end 78 | 79 | 80 | 81 | 82 | 83 | function cCraftScript:SelectScript(a_ScriptName) 84 | local Path = cPluginManager:GetCurrentPlugin():GetLocalFolder() .. "/craftscripts/" .. a_ScriptName .. ".lua" 85 | if (not cFile:IsFile(Path)) then 86 | return false, "The script does not exist." 87 | end 88 | 89 | local Function, Err = loadfile(Path) 90 | if (not Function) then 91 | LOGSCRIPTERROR(Err) 92 | return false, "There is an issue in the scripts code." 93 | end 94 | 95 | -- Make sure the craftscript can't break code by overlapping our global variables and functions 96 | setfenv(Function, g_CraftScriptEnvironment) 97 | 98 | self.SelectedScript = Function 99 | return true 100 | end 101 | 102 | 103 | 104 | 105 | 106 | function cCraftScript:Execute(a_Player, a_Split) 107 | if (not self.SelectedScript) then 108 | return false, "There is no script selected." 109 | end 110 | 111 | -- Limit the execution time of the script if configured 112 | if (g_Config.Scripting.MaxExecutionTime > 0) then 113 | local TimeLimit = os.clock() + g_Config.Scripting.MaxExecutionTime 114 | debug.sethook(function() 115 | if (TimeLimit < os.clock()) then 116 | debug.sethook() 117 | error("Time limit exceeded. Max time is: " .. g_Config.Scripting.MaxExecutionTime .. " seconds") 118 | end 119 | end, "", 100000) 120 | end 121 | 122 | -- Execute the craftscript 123 | local Succes, Err = pcall(self.SelectedScript, a_Player, a_Split) 124 | 125 | -- Remove the timelimit. 126 | debug.sethook() 127 | 128 | if (not Succes) then 129 | LOGSCRIPTERROR(Err) 130 | return false, "Something went wrong while running the script." 131 | end 132 | 133 | return true 134 | end 135 | -------------------------------------------------------------------------------- /Classes/Expression.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Expression.lua 3 | 4 | -- Contains the cExpression class. This allows formulas to be executed safely in an empty environment. 5 | --[[ Usage example: 6 | local FormulaString = "data=4; x > y" 7 | local Expression = cExpression:new(FormulaString) 8 | 9 | Expression:AddReturnValue("Comp1") -- Return the first known comparisons 10 | :AddParameter("x") -- Add x and y as a parameter 11 | :AddParameter("y") 12 | :AddParameter("type"):AddReturnValue("type") -- Add type and data as a parameter and return it 13 | :AddParameter("data"):AddReturnValue("data") 14 | 15 | local Formula = Expression:Compile() 16 | 17 | for X = 1, 5 do 18 | for Y = 1, 5 do 19 | local PlaceBlock, BlockType, BlockMeta = Formula(X, Y, E_BLOCK_AIR, 0) 20 | if (PlaceBlock) then 21 | -- Place the block 22 | end 23 | end 24 | end 25 | ]] 26 | 27 | 28 | 29 | 30 | 31 | cExpression = {} 32 | 33 | 34 | 35 | 36 | 37 | cExpression.m_ExpressionTemplate = 38 | [[ 39 | local assert, pairs = assert, pairs 40 | local abs, acos, asin, atan, atan2, 41 | ceil, cos, cosh, exp, floor, ln, 42 | log, log10, max, min, round, sin, 43 | sinh, sqrt, tan, tanh, random, pi, e 44 | = 45 | math.abs, math.acos, math.asin, math.atan, math.atan2, 46 | math.ceil, math.cos, math.cosh, math.exp, math.floor, math.log, 47 | math.log, math.log10, math.max, math.min, math.round, math.sin, 48 | math.sinh, math.sqrt, math.tan, math.tanh, math.random, math.pi, math.exp(1) 49 | 50 | -- These functions are not build into Lua: 51 | local cbrt = function(x) return sqrt(x^(1/3)) end 52 | local randint = function(max) return random(0, max) end 53 | local rint = function(num) local Number, Decimal = math.modf(num); return (Decimal <= 0.5) and Number or (Number + 1) end 54 | 55 | %s 56 | 57 | local validators = {...} 58 | 59 | return function(%s) 60 | %s 61 | for _, validator in pairs(validators) do 62 | assert(validator(%s)) 63 | end 64 | return %s 65 | end]] 66 | 67 | 68 | 69 | 70 | 71 | -- The envoronment of the loader. 72 | -- It can currently only use the functions from the math library. 73 | cExpression.m_LoaderEnv = 74 | { 75 | math = math, 76 | assert = assert, 77 | pairs = pairs, 78 | } 79 | 80 | 81 | 82 | 83 | 84 | -- All the assignment operator 85 | -- Since Lua only supports the simple = assignments we need to give the others special handling. 86 | -- The = assignment is special because it can also be used in comparisons >=, == etc 87 | cExpression.m_Assignments = 88 | { 89 | "%+=", 90 | "%-=", 91 | "%*=", 92 | "%%=", 93 | "%^=", 94 | "/=", 95 | } 96 | 97 | 98 | 99 | 100 | 101 | -- A list of all the comparison operators. This is used to see if an action is an assignment or a comparison. 102 | -- For example if "x=5;y where is how many comparisons there were starting from 1. 166 | -- For example in the formula "xz" xz is Comp2 167 | function cExpression:AddReturnValue(a_Name) 168 | table.insert(self.m_ReturnValues, a_Name) 169 | return self 170 | end 171 | 172 | 173 | 174 | 175 | 176 | -- Adds a validator to check if the return value is allowed. 177 | -- a_Validator is a function. If it returns false the expression will assert 178 | function cExpression:AddReturnValidator(a_Validator) 179 | table.insert(self.m_ReturnValidators, a_Validator) 180 | return self 181 | end 182 | 183 | 184 | 185 | 186 | 187 | -- Adds a new constant. The formula will be able to use this in it's calculation. 188 | -- a_VarName is a string that will be the name of the constant. 189 | -- a_Value can only be a string or a number, since the environment blocks all other functions and tables. 190 | function cExpression:PredefineConstant(a_VarName, a_Value) 191 | table.insert(self.m_PredefinedConstants, {name = a_VarName, value = a_Value}) 192 | return self 193 | end 194 | 195 | 196 | 197 | 198 | 199 | -- Creates a safe function from the formula string. 200 | -- The returned function takes the previously-bound parameters (AddParameter()), does the calculations using any predefined constants (PredefineConstant()) and returns the named values (AddReturnValue()) 201 | -- Comparisons can be returned by adding a return value called "Comp" where is the ID of the comparison starting from 1. For example in the formula "xz" xz is Comp2 202 | function cExpression:Compile() 203 | local Arguments = table.concat(self.m_Parameters, ", ") 204 | local ReturnValues = table.concat(self.m_ReturnValues, ", ") 205 | 206 | local PredefinedVariables = "" 207 | for _, Variable in ipairs(self.m_PredefinedConstants) do 208 | local Value = Variable.value 209 | if (type(Value) == "string") then 210 | Value = "\"" .. Value .. "\"" 211 | end 212 | 213 | PredefinedVariables = PredefinedVariables .. "local " .. Variable.name .. " = " .. Value .. "\n" 214 | end 215 | 216 | -- The number of comparisons. This will be used to give each comparison a name (Comp) 217 | local NumComparison = 1 218 | 219 | -- Split the formula into actions (For example in "data=5; x" 224 | for Idx, Action in ipairs(Actions) do 225 | -- Check if the = operator is found 226 | local IsAssignment = Action:match("[%a%d%s]=[%(%a%d%s]") ~= nil 227 | 228 | -- Check if one of the assignment operators is found. If one is found it's certain that the action is an assignment. 229 | for Idx, Assignment in pairs(cExpression.m_Assignments) do 230 | if (Action:match(Assignment)) then 231 | IsAssignment = true 232 | end 233 | end 234 | 235 | -- Make all the comparisons work properly. For example != is used instead of ~=, while ~= is used to see if 2 numbers are near each other. 236 | for _, Comparison in ipairs(cExpression.m_Comparisons) do 237 | if (Action:match(Comparison.Usage)) then 238 | Action = Comparison.Result:format(Action:match(Comparison.Usage)) 239 | end 240 | end 241 | 242 | if (IsAssignment) then 243 | -- The action is an assignment. Since Lua only supports the simple = assignments we got to do some special handling for the assign assignments like += and *=. 244 | for Idx, Assignment in pairs(cExpression.m_Assignments) do 245 | -- Get what type of assignment it is (multiply, divide etc) 246 | local Operator = Assignment:match(".="):sub(1, 1) 247 | 248 | -- This pattern will get the name of the variable to assign, and everything to add/devide/multiply etc 249 | local Pattern = "(.*)" .. Assignment .. "(.*)" 250 | Action:gsub(Pattern, 251 | function(a_Variable, a_Val2) 252 | Action = a_Variable .. " = " .. a_Variable .. Operator .. a_Val2 253 | end 254 | ) 255 | end 256 | 257 | -- Add the assignment in the formula function 258 | Actions[Idx] = "local " .. Action 259 | else 260 | -- Add the comparison. The name will be Comp where nr is how many comparison's there currently are. 261 | Actions[Idx] = "local Comp" .. NumComparison .. " = " .. Action 262 | NumComparison = NumComparison + 1 263 | end 264 | end 265 | 266 | local formulaLoaderSrc = cExpression.m_ExpressionTemplate:format(PredefinedVariables, Arguments, table.concat(Actions, "\n\t"), ReturnValues, ReturnValues) 267 | local FormulaLoader = loadstring(formulaLoaderSrc) 268 | if (not FormulaLoader) then 269 | return false, "Invalid formula" 270 | end 271 | 272 | -- Only allow the FormulaLoader to use the math library and the Round function 273 | setfenv(FormulaLoader, cExpression.m_LoaderEnv) 274 | 275 | -- Try to get the formula checker 276 | local Succes, Formula = pcall(FormulaLoader, unpack(self.m_ReturnValidators)) 277 | if (not Succes) then 278 | return false, "Invalid formula" 279 | end 280 | 281 | -- Don't allow Formula to interact with the rest of the server except the local variables it already has. 282 | setfenv(Formula, {}) 283 | 284 | return Formula 285 | end 286 | -------------------------------------------------------------------------------- /Classes/Mask.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Mask.lua 3 | 4 | -- Contains the cMask class representing blocks that can be replaced. Used for example in masks and the replace command. 5 | 6 | 7 | 8 | 9 | 10 | cMask = {} 11 | 12 | 13 | 14 | 15 | local function ParseBlockArray(a_BlockArray) 16 | -- Make from the array a table with the blocktypes as keys. 17 | -- In there create a table a boolean value called TypeOnly, and a table containing blockmetas 18 | -- If TypeOnly is set to true the Contains function will only check if the blocktype exists. Else it will look if the blockmeta exists in the BlockMetas table. 19 | local BlockTable = {} 20 | for Idx, Block in ipairs(a_BlockArray) do 21 | local BlockInfo = BlockTable[Block.BlockType] or {TypeOnly = false, BlockMetas = {}} 22 | BlockInfo.TypeOnly = BlockInfo.TypeOnly or Block.TypeOnly 23 | if (not BlockInfo.TypeOnly) then 24 | BlockInfo.BlockMetas[Block.BlockMeta] = true 25 | end 26 | 27 | -- Save the blockinfo in the table with the blocktype as key. 28 | BlockTable[Block.BlockType] = BlockInfo 29 | end 30 | return BlockTable; 31 | end 32 | 33 | 34 | 35 | 36 | 37 | local function Contains(a_BlockTable, a_BlockType, a_BlockMeta) 38 | local BlockInfo = a_BlockTable[a_BlockType] 39 | if (not BlockInfo) then 40 | return false 41 | end 42 | 43 | if (BlockInfo.TypeOnly) then 44 | -- The block is marked to only check the blocktype, so we don't have to check the meta. 45 | -- Since the block exists in the blocktable we can just return true 46 | return true 47 | end 48 | 49 | -- Check if the meta exists. If it exists it has the value true, so we either return that or return false. 50 | return BlockInfo.BlockMetas[a_BlockMeta] or false 51 | end 52 | 53 | 54 | 55 | 56 | 57 | function cMask:new(a_PositiveBlocks, a_NegativeBlocks) 58 | -- Get all the different blocks from the string 59 | local Obj = {} 60 | 61 | setmetatable(Obj, cMask) 62 | self.__index = self 63 | 64 | Obj.m_PositiveBlockTable = {} 65 | Obj.m_NegativeBlockTable = nil 66 | 67 | if (a_PositiveBlocks ~= nil) then 68 | local BlockArray, ErrorBlock = RetrieveBlockTypes(a_PositiveBlocks) 69 | if (not BlockArray) then 70 | return false, ErrorBlock 71 | end 72 | Obj.m_PositiveBlockTable = ParseBlockArray(BlockArray) 73 | end 74 | 75 | if (a_NegativeBlocks ~= nil) then 76 | local BlockArray, ErrorBlock = RetrieveBlockTypes(a_NegativeBlocks) 77 | if (not BlockArray) then 78 | return false, ErrorBlock 79 | end 80 | Obj.m_NegativeBlockTable = ParseBlockArray(BlockArray) 81 | end 82 | 83 | return Obj 84 | end 85 | 86 | 87 | 88 | 89 | 90 | -- Checks if the given blocktype exists in the blocktable. 91 | function cMask:Contains(a_BlockType, a_BlockMeta) 92 | if (self.m_NegativeBlockTable ~= nil and not Contains(self.m_NegativeBlockTable, a_BlockType, a_BlockMeta)) then 93 | return true; 94 | end 95 | 96 | if (not Contains(self.m_PositiveBlockTable, a_BlockType, a_BlockMeta)) then 97 | return false; 98 | end 99 | 100 | return true; 101 | end 102 | -------------------------------------------------------------------------------- /Classes/PlayerSelection.lua: -------------------------------------------------------------------------------- 1 | 2 | -- PlayerSelection.lua 3 | 4 | -- Implements the cPlayerSelection class representing a selection for a single player 5 | 6 | 7 | 8 | 9 | 10 | --- Class for storing the player's selection 11 | cPlayerSelection = {} 12 | 13 | 14 | 15 | 16 | 17 | --- Creates a new instance of the class 18 | function cPlayerSelection:new(a_Obj, a_PlayerState) 19 | a_Obj = a_Obj or {} 20 | setmetatable(a_Obj, cPlayerSelection) 21 | self.__index = self 22 | 23 | -- Initialize the object members: 24 | a_Obj.Cuboid = cCuboid() 25 | a_Obj.IsFirstPointSet = false 26 | a_Obj.IsSecondPointSet = false 27 | a_Obj.PlayerState = a_PlayerState 28 | a_Obj.OnChangeCallbacks = {} 29 | 30 | return a_Obj 31 | end 32 | 33 | 34 | 35 | 36 | 37 | function cPlayerSelection:Deselect() 38 | self.IsFirstPointSet = false 39 | self.IsSecondPointSet = false 40 | self.Cuboid = cCuboid() 41 | 42 | -- Remove the selection from the database 43 | local DB = cSQLStorage:Get() 44 | DB:ExecuteCommand("remove_playerselection", 45 | { 46 | playeruuid = self.PlayerState:GetUUID() 47 | } 48 | ) 49 | 50 | -- Set the player's WECUI, if present: 51 | if (not self.PlayerState.IsWECUIActivated) then 52 | return 53 | end 54 | 55 | self.PlayerState:DoWithPlayer( 56 | function(a_Player) 57 | a_Player:GetClientHandle():SendPluginMessage("WECUI", "s|cuboid") 58 | end 59 | ) 60 | end 61 | 62 | 63 | 64 | 65 | 66 | -- Saves or overwrites a selection to the database 67 | function cPlayerSelection:SaveSelection(a_SelectionName) 68 | if (not self:IsValid()) then 69 | return false, "No region selected" 70 | end 71 | 72 | local Success = cSQLStorage:Get():ExecuteCommand("set_namedplayerselection", 73 | { 74 | playeruuid = self.PlayerState:GetUUID(), 75 | selname = a_SelectionName, 76 | MinX = self.Cuboid.p1.x, 77 | MinY = self.Cuboid.p1.y, 78 | MinZ = self.Cuboid.p1.z, 79 | MaxX = self.Cuboid.p2.x, 80 | MaxY = self.Cuboid.p2.y, 81 | MaxZ = self.Cuboid.p2.z, 82 | } 83 | ) 84 | 85 | if (not Success) then 86 | return false, "An error occurred while saving the selection" 87 | end 88 | 89 | return true 90 | end 91 | 92 | 93 | 94 | 95 | 96 | -- Loads a player's selection from the database 97 | function cPlayerSelection:LoadSelection(a_SelectionName) 98 | local FoundSelection = false 99 | local DatabaseSuccess = cSQLStorage:Get():ExecuteCommand("get_namedplayerselection", 100 | { 101 | playeruuid = self.PlayerState:GetUUID(), 102 | selname = a_SelectionName 103 | }, 104 | function(a_Data) 105 | self:SetFirstPoint(a_Data.MinX, a_Data.MinY, a_Data.MinZ) 106 | self:SetSecondPoint(a_Data.MaxX, a_Data.MaxY, a_Data.MaxZ) 107 | FoundSelection = true 108 | end 109 | ) 110 | 111 | if (not DatabaseSuccess) then 112 | return false, "An error occurred while saving the selection" 113 | end 114 | 115 | if (not FoundSelection) then 116 | return false, "The selection doesn't exist" 117 | end 118 | 119 | self:NotifySelectionChanged() 120 | return true 121 | end 122 | 123 | 124 | 125 | 126 | 127 | -- Expands the selection in each direction by the specified amount of blocks 128 | function cPlayerSelection:Expand(a_SubMinX, a_SubMinY, a_SubMinZ, a_AddMaxX, a_AddMaxY, a_AddMaxZ) 129 | -- Check the params: 130 | assert(a_SubMinX ~= nil) 131 | assert(a_SubMinY ~= nil) 132 | assert(a_SubMinZ ~= nil) 133 | assert(a_AddMaxX ~= nil) 134 | assert(a_AddMaxY ~= nil) 135 | assert(a_AddMaxZ ~= nil) 136 | 137 | if (self.Cuboid.p1.x < self.Cuboid.p2.x) then 138 | self.Cuboid.p1.x = self.Cuboid.p1.x - a_SubMinX 139 | else 140 | self.Cuboid.p2.x = self.Cuboid.p2.x - a_SubMinX 141 | end 142 | 143 | if (self.Cuboid.p1.y < self.Cuboid.p2.y) then 144 | self.Cuboid.p1.y = self.Cuboid.p1.y - a_SubMinY 145 | else 146 | self.Cuboid.p2.y = self.Cuboid.p2.y - a_SubMinY 147 | end 148 | 149 | if (self.Cuboid.p1.z < self.Cuboid.p2.z) then 150 | self.Cuboid.p1.z = self.Cuboid.p1.z - a_SubMinZ 151 | else 152 | self.Cuboid.p2.z = self.Cuboid.p2.z - a_SubMinZ 153 | end 154 | 155 | if (self.Cuboid.p1.x > self.Cuboid.p2.x) then 156 | self.Cuboid.p1.x = self.Cuboid.p1.x + a_AddMaxX 157 | else 158 | self.Cuboid.p2.x = self.Cuboid.p2.x + a_AddMaxX 159 | end 160 | 161 | if (self.Cuboid.p1.y > self.Cuboid.p2.y) then 162 | self.Cuboid.p1.y = self.Cuboid.p1.y + a_AddMaxY 163 | else 164 | self.Cuboid.p2.y = self.Cuboid.p2.y + a_AddMaxY 165 | end 166 | 167 | if (self.Cuboid.p1.z > self.Cuboid.p2.z) then 168 | self.Cuboid.p1.z = self.Cuboid.p1.z + a_AddMaxZ 169 | else 170 | self.Cuboid.p2.z = self.Cuboid.p2.z + a_AddMaxZ 171 | end 172 | 173 | self:NotifySelectionChanged() -- Notify the changes to the client. 174 | end 175 | 176 | 177 | 178 | 179 | 180 | --- Returns the absolute differences in each coord, as three numbers 181 | function cPlayerSelection:GetCoordDiffs() 182 | assert(self:IsValid()) 183 | 184 | local DifX = self.Cuboid.p2.x - self.Cuboid.p1.x 185 | local DifY = self.Cuboid.p2.y - self.Cuboid.p1.y 186 | local DifZ = self.Cuboid.p2.z - self.Cuboid.p1.z 187 | if (DifX < 0) then 188 | DifX = -DifX 189 | end 190 | if (DifY < 0) then 191 | DifY = -DifY 192 | end 193 | if (DifZ < 0) then 194 | DifZ = -DifZ 195 | end 196 | return DifX, DifY, DifZ 197 | end 198 | 199 | 200 | 201 | 202 | 203 | --- Returns the smaller of each coord-pair 204 | function cPlayerSelection:GetMinCoords() 205 | local MinX, MinY, MinZ 206 | if (self.Cuboid.p1.x < self.Cuboid.p2.x) then 207 | MinX = self.Cuboid.p1.x 208 | else 209 | MinX = self.Cuboid.p2.x 210 | end 211 | if (self.Cuboid.p1.y < self.Cuboid.p2.y) then 212 | MinX = self.Cuboid.p1.y 213 | else 214 | MinX = self.Cuboid.p2.y 215 | end 216 | if (self.Cuboid.p1.z < self.Cuboid.p2.z) then 217 | MinX = self.Cuboid.p1.z 218 | else 219 | MinX = self.Cuboid.p2.z 220 | end 221 | return MinX, MinY, MinZ 222 | end 223 | 224 | 225 | 226 | 227 | 228 | --- Returns a string describing the selection size ("X * Y * Z, volume V blocks") 229 | function cPlayerSelection:GetSizeDesc() 230 | assert(self:IsValid()) 231 | 232 | local DifX, DifY, DifZ = self:GetCoordDiffs() 233 | DifX = DifX + 1 234 | DifY = DifY + 1 235 | DifZ = DifZ + 1 236 | local Volume = DifX * DifY * DifZ 237 | local Dimensions = tostring(DifX) .. " * " .. DifY .. " * " .. DifZ 238 | if (Volume == 1) then 239 | return Dimensions .. ", volume 1 block" 240 | else 241 | return Dimensions .. ", volume " .. Volume .. " blocks" 242 | end 243 | end 244 | 245 | 246 | 247 | 248 | 249 | --- Returns a new cuboid with the selection's bounds, sorted 250 | function cPlayerSelection:GetSortedCuboid() 251 | assert(self:IsValid()) 252 | 253 | local SCuboid = cCuboid(self.Cuboid) 254 | SCuboid:Sort() 255 | return SCuboid; 256 | end 257 | 258 | 259 | 260 | 261 | 262 | --- Returns the 3D volume of the selection 263 | function cPlayerSelection:GetVolume() 264 | assert(self:IsValid()) 265 | 266 | local Volume = self.Cuboid.p2.x - self.Cuboid.p1.x 267 | Volume = Volume * (self.Cuboid.p2.y - self.Cuboid.p1.y) 268 | Volume = Volume * (self.Cuboid.p2.z - self.Cuboid.p1.z) 269 | if (Volume < 0) then 270 | return -Volume 271 | end 272 | return Volume 273 | end 274 | 275 | 276 | 277 | 278 | 279 | --- Returns the two X coords, smaller first 280 | function cPlayerSelection:GetXCoordsSorted() 281 | assert(self:IsValid()) 282 | 283 | if (self.Cuboid.p1.x < self.Cuboid.p2.x) then 284 | return self.Cuboid.p1.x, self.Cuboid.p2.x 285 | else 286 | return self.Cuboid.p2.x, self.Cuboid.p1.x 287 | end 288 | end 289 | 290 | 291 | 292 | 293 | 294 | --- Returns the two Y coords, smaller first 295 | function cPlayerSelection:GetYCoordsSorted() 296 | assert(self:IsValid()) 297 | 298 | if (self.Cuboid.p1.y < self.Cuboid.p2.y) then 299 | return self.Cuboid.p1.y, self.Cuboid.p2.y 300 | else 301 | return self.Cuboid.p2.y, self.Cuboid.p1.y 302 | end 303 | end 304 | 305 | 306 | 307 | 308 | 309 | --- Returns the two Z coords, smaller first 310 | function cPlayerSelection:GetZCoordsSorted() 311 | assert(self:IsValid()) 312 | 313 | if (self.Cuboid.p1.z < self.Cuboid.p2.z) then 314 | return self.Cuboid.p1.z, self.Cuboid.p2.z 315 | else 316 | return self.Cuboid.p2.z, self.Cuboid.p1.z 317 | end 318 | end 319 | 320 | 321 | 322 | 323 | 324 | --- Returns true if the selection is valid - both points are set 325 | function cPlayerSelection:IsValid() 326 | return (self.IsFirstPointSet and self.IsSecondPointSet) 327 | end 328 | 329 | 330 | 331 | 332 | 333 | --- Notifies all registered callbacks that the selection has changed 334 | -- a_PointChanged is optional, assigned to the point that has just changed 335 | -- If nil, the entire selection is assumed changed 336 | function cPlayerSelection:NotifySelectionChanged(a_PointChanged) 337 | -- TODO: Call the OnPlayerSelectionChangING callback to prevent selection changes. 338 | 339 | -- Call the player selection changed callback to notify other plugins of the selection change. 340 | self.PlayerState:DoWithPlayer(function(a_Player) 341 | local cuboid = cCuboid() 342 | cuboid:Assign(self.Cuboid) 343 | CallHook("OnPlayerSelectionChanged", a_Player, cuboid, a_PointChanged) 344 | end); 345 | 346 | -- Set the player's WECUI, if present: 347 | if (self.PlayerState.IsWECUIActivated) then 348 | local Volume = -1 349 | if (self:IsValid()) then 350 | Volume = self:GetVolume() 351 | end 352 | local c = self.Cuboid 353 | if (self.IsFirstPointSet and ((a_PointChanged == nil) or (a_PointChanged == 1))) then 354 | self.PlayerState:DoWithPlayer( 355 | function(a_Player) 356 | a_Player:GetClientHandle():SendPluginMessage("WECUI", string.format( 357 | "p|0|%i|%i|%i|%i", 358 | c.p1.x, c.p1.y, c.p1.z, Volume 359 | )) 360 | end 361 | ) 362 | end 363 | if (self.IsSecondPointSet and ((a_PointChanged == nil) or (a_PointChanged == 2))) then 364 | self.PlayerState:DoWithPlayer( 365 | function(a_Player) 366 | a_Player:GetClientHandle():SendPluginMessage("WECUI", string.format( 367 | "p|1|%i|%i|%i|%i", 368 | c.p2.x, c.p2.y, c.p2.z, Volume 369 | )) 370 | end 371 | ) 372 | end 373 | end 374 | end 375 | 376 | 377 | 378 | 379 | 380 | --- Moves the entire cuboid to the given Offset 381 | function cPlayerSelection:Move(a_OffsetX, a_OffsetY, a_OffsetZ) 382 | -- Check the parameters 383 | assert(a_OffsetX ~= nil) 384 | assert(a_OffsetY ~= nil) 385 | assert(a_OffsetZ ~= nil) 386 | 387 | -- Move the cuboid 388 | self.Cuboid:Move(Vector3i(a_OffsetX, a_OffsetY, a_OffsetZ)) 389 | 390 | -- Notify the client 391 | self:NotifySelectionChanged() 392 | end 393 | 394 | 395 | 396 | 397 | 398 | --- Sets the first point in the selection 399 | function cPlayerSelection:SetFirstPoint(a_BlockX, a_BlockY, a_BlockZ) 400 | -- Check the params: 401 | local BlockX = tonumber(a_BlockX) 402 | local BlockY = tonumber(a_BlockY) 403 | local BlockZ = tonumber(a_BlockZ) 404 | assert(BlockX ~= nil) 405 | assert(BlockY ~= nil) 406 | assert(BlockZ ~= nil) 407 | 408 | -- Set the point: 409 | self.Cuboid.p1:Set(BlockX, BlockY, BlockZ) 410 | self.IsFirstPointSet = true 411 | self:NotifySelectionChanged(1) 412 | end 413 | 414 | 415 | 416 | 417 | 418 | --- Sets the second point in the selection 419 | function cPlayerSelection:SetSecondPoint(a_BlockX, a_BlockY, a_BlockZ) 420 | -- Check the params: 421 | local BlockX = tonumber(a_BlockX) 422 | local BlockY = tonumber(a_BlockY) 423 | local BlockZ = tonumber(a_BlockZ) 424 | assert(BlockX ~= nil) 425 | assert(BlockY ~= nil) 426 | assert(BlockZ ~= nil) 427 | 428 | -- Set the point: 429 | self.Cuboid.p2:Set(BlockX, BlockY, BlockZ) 430 | self.IsSecondPointSet = true 431 | self:NotifySelectionChanged(2) 432 | end 433 | 434 | 435 | 436 | 437 | 438 | --- Common code to set selection position based on player clicking somewhere 439 | function cPlayerSelection:SetPos(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_PosName, a_ForceSet) 440 | -- Check if a valid click: 441 | if (a_BlockFace == BLOCK_FACE_NONE) then 442 | return false 443 | end 444 | 445 | -- Check the wand activation state: 446 | if (not self.PlayerState.WandActivated and not a_ForceSet) then 447 | return false 448 | end 449 | 450 | local Abort = false 451 | self.PlayerState:DoWithPlayer( 452 | function(a_Player) 453 | -- Check the WE permission: 454 | if not(a_Player:HasPermission("worldedit.selection.pos")) then 455 | Abort = true 456 | return true 457 | end 458 | 459 | -- When shift is pressed, use the air block instead of the clicked block: 460 | if (a_Player:IsCrouched()) then 461 | a_BlockX, a_BlockY, a_BlockZ = AddFaceDirection(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 462 | end 463 | end 464 | ) 465 | 466 | -- The callback decided to abort. 467 | if (Abort) then 468 | return false 469 | end 470 | 471 | -- Determine what point we're trying to set and get the proper set function for it. 472 | local SetFunc = (a_PosName == "First") and cPlayerSelection.SetFirstPoint or cPlayerSelection.SetSecondPoint 473 | 474 | -- Set the position in the internal representation: 475 | SetFunc(self, a_BlockX, a_BlockY, a_BlockZ) 476 | 477 | -- Return a success message: 478 | return true, cChatColor.LightPurple .. a_PosName .. " position set to {" .. a_BlockX .. ", " .. a_BlockY .. ", " .. a_BlockZ .. "}." 479 | end 480 | 481 | 482 | 483 | 484 | 485 | -- Loads the player selection from the database 486 | function cPlayerSelection:Load(a_PlayerUUID) 487 | local DB = cSQLStorage:Get() 488 | DB:ExecuteCommand("get_playerselection", 489 | { 490 | playeruuid = a_PlayerUUID 491 | }, 492 | function(a_Data) 493 | self:SetFirstPoint(a_Data.MinX, a_Data.MinY, a_Data.MinZ) 494 | self:SetSecondPoint(a_Data.MaxX, a_Data.MaxY, a_Data.MaxZ) 495 | end 496 | ) 497 | 498 | self:NotifySelectionChanged() 499 | end 500 | 501 | 502 | 503 | 504 | 505 | -- Saves the player's selection points in the database 506 | function cPlayerSelection:Save(a_PlayerUUID) 507 | if (not self:IsValid()) then 508 | return 509 | end 510 | 511 | local SrcCuboid = self:GetSortedCuboid() 512 | local DB = cSQLStorage:Get() 513 | DB:ExecuteCommand("set_playerselection", 514 | { 515 | playeruuid = a_PlayerUUID, 516 | MinX = SrcCuboid.p1.x, 517 | MinY = SrcCuboid.p1.y, 518 | MinZ = SrcCuboid.p1.z, 519 | MaxX = SrcCuboid.p2.x, 520 | MaxY = SrcCuboid.p2.y, 521 | MaxZ = SrcCuboid.p2.z, 522 | } 523 | ) 524 | end 525 | -------------------------------------------------------------------------------- /Classes/PlayerState.lua: -------------------------------------------------------------------------------- 1 | 2 | -- PlayerState.lua 3 | 4 | -- Implements the cPlayerState object, representing the full information that is remembered per player 5 | -- Also implements the GetPlayerState() function for retrieving / initializing the player state 6 | 7 | 8 | 9 | 10 | 11 | --- The dict-table of player states. 12 | --[[ 13 | Each player has an entry in this dictionary, indexed by the player's Key. 14 | The player name has been chosen as the Key, this means that multiple players of the same name 15 | share their state and the state is global for all worlds. 16 | Each entry is a cPlayerState class instance 17 | --]] 18 | local g_PlayerStates = {} 19 | 20 | 21 | 22 | 23 | 24 | --- Class for storing the player's state 25 | local cPlayerState = {} 26 | 27 | 28 | 29 | 30 | 31 | --- Creates a new PlayerState object 32 | function cPlayerState:new(a_Obj, a_PlayerKey, a_Player) 33 | assert(a_PlayerKey ~= nil) 34 | assert(a_Player ~= nil) 35 | 36 | a_Obj = a_Obj or {} 37 | setmetatable(a_Obj, cPlayerState) 38 | self.__index = self 39 | 40 | -- Initialize the object members to their defaults: 41 | local ClientHandle = a_Player:GetClientHandle() 42 | if (ClientHandle ~= nil) then 43 | a_Obj.IsWECUIActivated = ClientHandle:HasPluginChannel("WECUI") 44 | end 45 | a_Obj.Clipboard = cClipboard:new() 46 | a_Obj.ClipboardStorage = cClipboardStorage:new({}, a_Obj.Clipboard) 47 | a_Obj.PlayerKey = a_PlayerKey 48 | a_Obj.Selection = cPlayerSelection:new({}, a_Obj) 49 | a_Obj.UndoStack = cUndoStack:new({}, 10, a_Obj) -- TODO: Settable Undo depth (2nd param) 50 | a_Obj.WandActivated = true 51 | a_Obj.ToolRegistrator = cToolRegistrator:new({}) 52 | a_Obj.CraftScript = cCraftScript:new({}) 53 | 54 | return a_Obj 55 | end 56 | 57 | 58 | 59 | 60 | 61 | --- Calls the specified callback with the cPlayer instance of the player to whom this state belongs 62 | -- Returns true if the callback has been called, false otherwise 63 | function cPlayerState:DoWithPlayer(a_Callback) 64 | local HasCalled = false 65 | cRoot:Get():ForEachPlayer( 66 | function(a_Player) 67 | if (a_Player:GetName() == self.PlayerKey) then 68 | HasCalled = true 69 | a_Callback(a_Player) 70 | return true 71 | end 72 | end 73 | ) 74 | return HasCalled 75 | end 76 | 77 | 78 | 79 | 80 | 81 | function cPlayerState:GetUUID() 82 | local UUID = nil 83 | self:DoWithPlayer( 84 | function(a_Player) 85 | UUID = a_Player:GetUUID() 86 | end 87 | ) 88 | return UUID 89 | end 90 | 91 | 92 | 93 | 94 | 95 | --- Loads the state from persistent storage (if so configured) 96 | function cPlayerState:Load() 97 | local UUID = self:GetUUID() 98 | 99 | if (g_Config.Storage.RememberPlayerSelection) then 100 | self.Selection:Load(UUID) 101 | end 102 | end 103 | 104 | 105 | 106 | 107 | 108 | --- Pushes one level of Undo onto the Undo stack, by cloning the BlockArea within the Selection 109 | -- a_World is the cWorld where the selection is being copied, a_UndoName is a user-visible name that can be listed 110 | function cPlayerState:PushUndoInSelection(a_World, a_UndoName) 111 | -- Read the BlockArea: 112 | local Area = cBlockArea() 113 | local MinX, MaxX = self.Selection:GetXCoordsSorted() 114 | local MinY, MaxY = self.Selection:GetYCoordsSorted() 115 | local MinZ, MaxZ = self.Selection:GetZCoordsSorted() 116 | Area:Read(a_World, MinX, MaxX, MinY, MaxY, MinZ, MaxZ) 117 | 118 | -- Push the Undo onto the stack: 119 | self.UndoStack:PushUndo(a_World, Area, a_UndoName) 120 | end 121 | 122 | 123 | 124 | 125 | 126 | --- Saves the state to persistent storage (if so configured) 127 | function cPlayerState:Save(a_PlayerUUID) 128 | if (g_Config.Storage.RememberPlayerSelection) then 129 | self.Selection:Save(a_PlayerUUID) 130 | end 131 | end 132 | 133 | 134 | 135 | 136 | 137 | --- Returns a PlayerState object for the specified Player 138 | -- Creates one if it doesn't exist yet 139 | function GetPlayerState(a_Player) 140 | assert(tolua.type(a_Player) == "cPlayer") 141 | 142 | local Key = a_Player:GetName() 143 | local res = g_PlayerStates[Key] 144 | if (res ~= nil) then 145 | return res 146 | end 147 | 148 | -- The player state doesn't exist yet, create a new one: 149 | res = cPlayerState:new({}, Key, a_Player) 150 | g_PlayerStates[Key] = res 151 | res:Load() 152 | 153 | return res 154 | end 155 | 156 | 157 | 158 | 159 | 160 | local function OnPlayerDestroyed(a_Player) 161 | -- Allow the player state to be saved to a persistent storage: 162 | local State = g_PlayerStates[a_Player:GetName()] 163 | if (State == nil) then 164 | return false 165 | end 166 | State:Save(a_Player:GetUUID()) 167 | 168 | -- Remove the player state altogether: 169 | g_PlayerStates[a_Player:GetName()] = nil 170 | end 171 | 172 | 173 | 174 | 175 | 176 | function ForEachPlayerState(a_Callback) 177 | assert(type(a_Callback) == "function") 178 | 179 | for _, State in pairs(g_PlayerStates) do 180 | if (a_Callback(State)) then 181 | break 182 | end 183 | end 184 | end 185 | 186 | 187 | 188 | 189 | 190 | local function OnPluginMessage(a_Client, a_Channel, a_Message) 191 | if (a_Channel ~= "REGISTER") then 192 | return 193 | end 194 | 195 | -- The client has registered for some channels, if they did register for WECUI, send the selection to them: 196 | -- Find the cPlayer object for this client, if available: 197 | local Player = a_Client:GetPlayer() 198 | if (Player == nil) then 199 | return 200 | end 201 | 202 | -- Send the selection (if there is one): 203 | local State = GetPlayerState(Player) 204 | State.IsWECUIActivated = a_Client:HasPluginChannel("WECUI") 205 | State.Selection:NotifySelectionChanged() 206 | end 207 | 208 | 209 | 210 | 211 | 212 | -- Register the hooks needed: 213 | cPluginManager:AddHook(cPluginManager.HOOK_PLAYER_DESTROYED, OnPlayerDestroyed) 214 | cPluginManager:AddHook(cPluginManager.HOOK_PLUGIN_MESSAGE, OnPluginMessage) 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /Classes/RandomBlockTypeSource.lua: -------------------------------------------------------------------------------- 1 | 2 | -- BlockDstRandom.lua 3 | 4 | -- Implements the cRandomBlockTypeSource class to return random blocks from a string. 5 | 6 | 7 | 8 | 9 | 10 | cRandomBlockTypeSource = {} 11 | 12 | 13 | 14 | 15 | 16 | function cRandomBlockTypeSource:new(a_BlockString) 17 | local BlockTable, ErrorBlock = cRandomBlockTypeSource.RetrieveBlockTypes(a_BlockString) 18 | if (not BlockTable) then 19 | return false, ErrorBlock 20 | end 21 | 22 | local Obj = {} 23 | 24 | setmetatable(Obj, cRandomBlockTypeSource) 25 | self.__index = self 26 | 27 | Obj.m_BlockTable = BlockTable 28 | 29 | return Obj 30 | end 31 | 32 | 33 | 34 | 35 | 36 | -- (STATIC) Returns a table from a string containing all the blocks with weighted chances 37 | function cRandomBlockTypeSource.RetrieveBlockTypes(a_Input) 38 | local BlockTable, ErrBlock = RetrieveBlockTypes(a_Input) 39 | if (not BlockTable) then 40 | return false, ErrBlock 41 | end 42 | 43 | -- Count all the chances. This is used to calculate the chances off the blocks, since the chance from BlockTable are either raw from the player or 1. 44 | local ChanceSum = 0 45 | for Idx, BlockInfo in ipairs(BlockTable) do 46 | ChanceSum = ChanceSum + BlockInfo.Chance 47 | end 48 | 49 | local CalculatedBlockTable = {} 50 | local Temp = 0 51 | for Idx, BlockInfo in ipairs(BlockTable) do 52 | Temp = Temp + BlockInfo.Chance / ChanceSum 53 | table.insert(CalculatedBlockTable, {BlockType = BlockInfo.BlockType, BlockMeta = BlockInfo.BlockMeta, Chance = Temp}) 54 | end 55 | 56 | return CalculatedBlockTable 57 | end 58 | 59 | 60 | 61 | 62 | 63 | -- Returns a random block from self.m_BlockTable 64 | function cRandomBlockTypeSource:Get(a_X, a_Y, a_Z) 65 | local RandomNumber = math.random() 66 | for Idx, BlockInfo in ipairs(self.m_BlockTable) do 67 | if (RandomNumber <= BlockInfo.Chance) then 68 | return BlockInfo.BlockType, BlockInfo.BlockMeta 69 | end 70 | end 71 | end 72 | 73 | 74 | 75 | 76 | 77 | -- Returns if one of the blocktypes in the given table is in the block table as a key. 78 | function cRandomBlockTypeSource:Contains(a_BlockTypeList) 79 | for Idx, BlockInfo in ipairs(self.m_BlockTable) do 80 | if (a_BlockTypeList[BlockInfo.BlockType]) then 81 | return true, BlockInfo.BlockType 82 | end 83 | end 84 | return false 85 | end 86 | -------------------------------------------------------------------------------- /Classes/ShapeGenerator.lua: -------------------------------------------------------------------------------- 1 | 2 | -- ShapeGenerator.lua 3 | 4 | -- Capable of generating shapes in BlockAreas. Either a predefined shape like a cylinder, or a shape from a mathematical formula. 5 | -- Only for a shape made using a mathematical formula the constructor has to be used. For other shapes there are static functions to create the shape in a blockarea. 6 | 7 | 8 | 9 | 10 | 11 | cShapeGenerator = {} 12 | 13 | 14 | 15 | 16 | 17 | -- The coordinates around a single point 18 | cShapeGenerator.m_Coords = 19 | { 20 | Vector3f(1, 0, 0), Vector3f(-1, 0, 0), -- X coords 21 | Vector3f(0, 1, 0), Vector3f(0, -1, 0), -- Y coords 22 | Vector3f(0, 0, 1), Vector3f(0, 0, -1), -- Z coords 23 | } 24 | 25 | 26 | 27 | 28 | 29 | -- Handler that makes the structure hollow 30 | cShapeGenerator.m_HollowHandler = function(a_ShapeGenerator, a_BlockArea, a_BlockPos) 31 | -- Check around the block coordinates if it has at least one single position that doesn't get set 32 | for Idx, Coord in ipairs(cShapeGenerator.m_Coords) do 33 | local CoordAround = a_BlockPos + Coord 34 | local DoSet = a_ShapeGenerator:GetBlockInfoFromFormula(CoordAround) 35 | if (not DoSet) then 36 | -- Empty spot around the block. 37 | return true 38 | end 39 | end -- /for Coords 40 | return false 41 | end 42 | 43 | 44 | 45 | 46 | 47 | -- Handler that makes the structure solid. 48 | cShapeGenerator.m_SolidHandler = function(a_ShapeGenerator, a_BlockArea, a_BlockPos) 49 | return true 50 | end 51 | 52 | 53 | 54 | 55 | 56 | -- Creates a new cShapeGenerator object. 57 | -- a_Zero and a_Unit are Vector3f vectors used to calculate a scaled vector3f. The formula will use the scaled vector as x, y and z values. 58 | -- a_BlockTable are the blocks to make the shape out of. 59 | -- a_Expression is a cExpression object that ShapeGenerator will compile. The ShapeGenerator will bind all the parameters and return values in the constructor. 60 | function cShapeGenerator:new(a_Zero, a_Unit, a_BlockTable, a_Expression, a_CanUseAllBlocks) 61 | local Obj = {} 62 | 63 | setmetatable(Obj, cShapeGenerator) 64 | self.__index = self 65 | 66 | -- Bind the parameters that will be used in the expression. We want the data and type returned again with the first comparison 67 | a_Expression:AddReturnValue("Comp1") 68 | :AddParameter("x") 69 | :AddParameter("y") 70 | :AddParameter("z") 71 | :AddParameter("type"):AddReturnValue("type") 72 | :AddParameter("data"):AddReturnValue("data") 73 | 74 | if (not a_CanUseAllBlocks) then 75 | a_Expression:AddReturnValidator( 76 | function(shouldPlace, blocktype, blockdata) 77 | if (g_Config.Limits.DisallowedBlocks[math.floor(blocktype)]) then 78 | return false, ItemTypeToString(blocktype) .. ' is not allowed' 79 | else 80 | return true 81 | end 82 | end 83 | ) 84 | end 85 | 86 | local Formula, Error = a_Expression:Compile() 87 | if (not Formula) then 88 | return false, "Invalid formula" 89 | end 90 | 91 | -- Test the formula to check if it is a comparison. 92 | local Succes, TestResult = pcall(Formula, 1, 1, 1, 1, 1) 93 | if (not Succes or (type(TestResult) ~= "boolean")) then 94 | if ((type(TestResult) == 'string') and TestResult:match(" is not allowed$")) then 95 | return false, TestResult:match(':%d-: (.+)') 96 | end 97 | return false, "The formula isn't a comparison" 98 | end 99 | 100 | -- A cache with blocktypes by x, y, z coordinates. 101 | -- It's only used when the shape is hollow 102 | Obj.m_Cache = {} 103 | 104 | -- A table containing all the blocks to use. The block chances are already calculated 105 | Obj.m_BlockTable = a_BlockTable 106 | 107 | -- A function that will calculate the shape 108 | Obj.m_Formula = Formula 109 | 110 | Obj.m_Unit = a_Unit 111 | Obj.m_Zero = a_Zero 112 | 113 | -- The size of the blockarea we're going to work in. 114 | Obj.m_Size = Vector3f() 115 | 116 | return Obj 117 | end 118 | 119 | 120 | 121 | 122 | 123 | -- Checks the cache if there already is a block calculated for the coordinates. 124 | -- Generates a new one if it doesn't exist in the cache 125 | -- a_BlockPos is a Vector3f with the coordinates to get the block for 126 | function cShapeGenerator:GetBlockInfoFromFormula(a_BlockPos) 127 | local Index = a_BlockPos.x + (a_BlockPos.z * self.m_Size.x) + (a_BlockPos.y * self.m_Size.x * self.m_Size.z) 128 | local BlockInfo = self.m_Cache[Index] 129 | 130 | -- The block already exists in the cache. Return the info from that. 131 | if (BlockInfo) then 132 | return BlockInfo.DoSet, BlockInfo.BlockType, BlockInfo.BlockMeta 133 | end 134 | 135 | local scaled = (a_BlockPos - self.m_Zero) / self.m_Unit 136 | local BlockType, BlockMeta = self.m_BlockTable:Get(a_BlockPos.x, a_BlockPos.y, a_BlockPos.z) 137 | 138 | -- Execute the formula to get the info from it. 139 | local DoSet, BlockType, BlockMeta = self.m_Formula(scaled.x, scaled.y, scaled.z, BlockType, BlockMeta) 140 | 141 | -- Save the block info in the cache 142 | self.m_Cache[Index] = {DoSet = DoSet, BlockType = BlockType, BlockMeta = BlockMeta} 143 | 144 | return DoSet, BlockType, BlockMeta 145 | end 146 | 147 | 148 | 149 | 150 | 151 | -- Generates a shape from m_Formula 152 | -- a_BlockArea is a cBlockArea to build the shape in 153 | -- a_MinVector and a_MaxVector are Vector3f classes. The shape will be build inside those coordinates 154 | -- a_IsHollow is a boolean value. If true the the shape will be made hollow 155 | -- a_Mask is a table or nil. If it's a table it will only change blocks if the block that is going to change is in the table. 156 | function cShapeGenerator:MakeShape(a_BlockArea, a_MinVector, a_MaxVector, a_IsHollow, a_Mask) 157 | local DoCheckMask = a_Mask ~= nil 158 | local Handler = a_IsHollow and cShapeGenerator.m_HollowHandler or cShapeGenerator.m_SolidHandler 159 | local NumAffectedBlocks = 0 160 | self.m_Size = Vector3f(a_BlockArea:GetSize()) 161 | 162 | local CurrentBlock = Vector3f(a_MinVector) 163 | for X = a_MinVector.x, a_MaxVector.x do 164 | CurrentBlock.x = X 165 | for Y = a_MinVector.y, a_MaxVector.y do 166 | CurrentBlock.y = Y 167 | for Z = a_MinVector.z, a_MaxVector.z do 168 | CurrentBlock.z = Z 169 | local DoSet, BlockType, BlockMeta = self:GetBlockInfoFromFormula(CurrentBlock) 170 | 171 | -- Check for the mask. 172 | if (DoSet and DoCheckMask) then 173 | local CurrentType, CurrentMeta = a_BlockArea:GetRelBlockTypeMeta(X, Y, Z) 174 | 175 | if (not a_Mask:Contains(CurrentType, CurrentMeta)) then 176 | -- The block does not exist in the mask, or the meta isn't set/is different. 177 | -- Don't change the block. 178 | DoSet = false 179 | end 180 | end 181 | 182 | if (DoSet and Handler(self, a_BlockArea, CurrentBlock)) then 183 | a_BlockArea:SetRelBlockTypeMeta(X, Y, Z, BlockType, BlockMeta) 184 | NumAffectedBlocks = NumAffectedBlocks + 1 185 | end 186 | end -- /for Z 187 | end -- /for Y 188 | end -- /for X 189 | 190 | return NumAffectedBlocks 191 | end 192 | 193 | 194 | 195 | 196 | 197 | -- (STATIC) Creates a cylinder in the given blockarea 198 | -- a_BlockArea is the cBlockArea to build the cylinder in 199 | -- a_BlockTable are the blocks to make the cylinder out of 200 | -- a_IsHollow is a boolean value. If true the cylinder will be made hollow. 201 | -- a_Mask is a table or nil. If it's a table it will only change blocks if the block that is going to change is in the table. 202 | function cShapeGenerator.MakeCylinder(a_BlockArea, a_BlockTable, a_IsHollow, a_Mask) 203 | local DoCheckMask = a_Mask ~= nil 204 | local SizeX, SizeY, SizeZ = a_BlockArea:GetCoordRange() 205 | local HalfX, HalfZ = SizeX / 2, SizeZ / 2 206 | local SqHalfX, SqHalfZ = HalfX ^ 2, HalfZ ^ 2 207 | 208 | local Expression = cExpression:new("x -= HalfX; z -= HalfZ; ((x * x) / SqHalfX) + ((z * z) / SqHalfZ) <= 1") 209 | :AddReturnValue("Comp1") 210 | :AddParameter("x") 211 | :AddParameter("z") 212 | :PredefineConstant("SqHalfX", SqHalfX) 213 | :PredefineConstant("SqHalfZ", SqHalfZ) 214 | :PredefineConstant("HalfX", HalfX) 215 | :PredefineConstant("HalfZ", HalfZ) 216 | 217 | local NumAffectedBlocks = 0 218 | local Formula = Expression:Compile() 219 | 220 | -- Sets the block in the blockarea. If the mask was not nil it checks the mask first. 221 | local function SetBlock(a_RelX, a_RelY, a_RelZ) 222 | if (DoCheckMask) then 223 | local CurrentBlock, CurrentMeta = a_BlockArea:GetRelBlockTypeMeta(a_RelX, a_RelY, a_RelZ) 224 | if (not a_Mask:Contains(CurrentBlock, CurrentMeta)) then 225 | -- The block does not exist in the mask, or the meta isn't set/is different. 226 | -- Don't change the block. 227 | return 228 | end 229 | end 230 | 231 | a_BlockArea:SetRelBlockTypeMeta(a_RelX, a_RelY, a_RelZ, a_BlockTable:Get(a_RelX, a_RelY, a_RelZ)) 232 | NumAffectedBlocks = NumAffectedBlocks + 1 233 | end 234 | 235 | for X = 0, HalfX, 1 do 236 | for Z = 0, HalfZ, 1 do 237 | local PlaceColumn = Formula(X, Z) 238 | if (a_IsHollow and PlaceColumn) then 239 | -- Check if there is at least one empty space around the current block. 240 | if (Formula(X - 1, Z) and Formula(X, Z - 1) and Formula(X + 1, Z) and Formula(X, Z + 1)) then 241 | PlaceColumn = false 242 | end 243 | end 244 | 245 | if (PlaceColumn) then 246 | for Y = 0, SizeY, 1 do 247 | SetBlock(X, Y, Z) 248 | SetBlock(SizeX - X, Y, Z) 249 | SetBlock(X, Y, SizeZ - Z) 250 | SetBlock(SizeX - X, Y, SizeZ - Z) 251 | end 252 | end 253 | end 254 | end 255 | 256 | return NumAffectedBlocks 257 | end 258 | 259 | 260 | 261 | 262 | 263 | -- (STATIC) Creates a sphere in the given blockarea. 264 | -- a_BlockArea is the cBlockArea to build the sphere in 265 | -- a_BlockTable are the blocks to make the sphere out of 266 | -- a_IsHollow is a boolean value. If true the sphere will be made hollow. 267 | -- a_Mask is a table or nil. If it's a table it will only change blocks if the block that is going to change is in the table. 268 | function cShapeGenerator.MakeSphere(a_BlockArea, a_BlockTable, a_IsHollow, a_Mask) 269 | local DoCheckMask = a_Mask ~= nil 270 | local SizeX, SizeY, SizeZ = a_BlockArea:GetCoordRange() 271 | local HalfX, HalfY, HalfZ = SizeX / 2, SizeY / 2, SizeZ / 2 272 | local SqHalfX, SqHalfY, SqHalfZ = HalfX ^ 2, HalfY ^ 2, HalfZ ^ 2 273 | 274 | local Expression = cExpression:new("x -= HalfX; y -= HalfY; z -= HalfZ; ((x * x) / SqHalfX) + ((y * y) / SqHalfY) + ((z * z) / SqHalfZ) <= 1") 275 | :AddReturnValue("Comp1") 276 | :AddParameter("x") 277 | :AddParameter("y") 278 | :AddParameter("z") 279 | :PredefineConstant("SqHalfX", SqHalfX) 280 | :PredefineConstant("SqHalfY", SqHalfY) 281 | :PredefineConstant("SqHalfZ", SqHalfZ) 282 | :PredefineConstant("HalfX", HalfX) 283 | :PredefineConstant("HalfY", HalfY) 284 | :PredefineConstant("HalfZ", HalfZ) 285 | 286 | local NumAffectedBlocks = 0 287 | local Formula = Expression:Compile() 288 | 289 | -- Sets the block in the blockarea. If the mask was not nil it checks the mask first. 290 | local function SetBlock(a_RelX, a_RelY, a_RelZ) 291 | if (DoCheckMask) then 292 | local CurrentBlock, CurrentMeta = a_BlockArea:GetRelBlockTypeMeta(a_RelX, a_RelY, a_RelZ) 293 | if (not a_Mask:Contains(CurrentBlock, CurrentMeta)) then 294 | -- The block does not exist in the mask, or the meta isn't set/is different. 295 | -- Don't change the block. 296 | return 297 | end 298 | end 299 | 300 | a_BlockArea:SetRelBlockTypeMeta(a_RelX, a_RelY, a_RelZ, a_BlockTable:Get(a_RelX, a_RelY, a_RelZ)) 301 | NumAffectedBlocks = NumAffectedBlocks + 1 302 | end 303 | 304 | for X = 0, HalfX, 1 do 305 | for Y = 0, HalfY, 1 do 306 | for Z = 0, HalfZ do 307 | local PlaceBlocks = Formula(X, Y, Z) 308 | if (a_IsHollow and PlaceBlocks) then 309 | -- Check if there is at least one empty space around the current block. 310 | if (Formula(X - 1, Y, Z) and Formula(X, Y - 1, Z) and Formula(X, Y, Z - 1) and Formula(X + 1, Y, Z) and Formula(X, Y + 1, Z) and Formula(X, Y, Z + 1)) then 311 | PlaceBlocks = false 312 | end 313 | end 314 | 315 | if (PlaceBlocks) then 316 | -- Lower half of the sphere 317 | SetBlock(X, Y, Z) 318 | SetBlock(SizeX - X, Y, Z) 319 | SetBlock(X, Y, SizeZ - Z) 320 | SetBlock(SizeX - X, Y, SizeZ - Z) 321 | 322 | -- topper part of the sphere 323 | SetBlock(X, SizeY - Y, Z) 324 | SetBlock(SizeX - X, SizeY - Y, Z) 325 | SetBlock(X, SizeY - Y, SizeZ - Z) 326 | SetBlock(SizeX - X, SizeY - Y, SizeZ - Z) 327 | end 328 | end 329 | end 330 | end 331 | 332 | return NumAffectedBlocks 333 | end 334 | 335 | 336 | 337 | 338 | 339 | -- (STATIC) Creates a pyramid in the given BlockArea. 340 | -- a_BlockArea is the cBlockArea to build the pyramid in 341 | -- a_BlockTable are the blocks to make the pyramid out of 342 | -- a_IsHollow is a boolean value. If true the pyramid will be made hollow. 343 | -- a_Mask is a table or nil. If it's a table it will only change blocks if the block that is going to change is in the table. 344 | function cShapeGenerator.MakePyramid(a_BlockArea, a_BlockTable, a_IsHollow, a_Mask) 345 | local DoCheckMask = a_Mask ~= nil 346 | local SizeX, SizeY, SizeZ = a_BlockArea:GetCoordRange() 347 | local NumAffectedBlocks = 0 348 | 349 | -- Sets the block in the blockarea. If the mask was not nil it checks the mask first. 350 | local function SetBlock(a_RelX, a_RelY, a_RelZ) 351 | if (DoCheckMask) then 352 | local CurrentBlock, CurrentMeta = a_BlockArea:GetRelBlockTypeMeta(a_RelX, a_RelY, a_RelZ) 353 | if (not a_Mask:Contains(CurrentBlock, CurrentMeta)) then 354 | -- The block does not exist in the mask, or the meta isn't set/is different. 355 | -- Don't change the block. 356 | return 357 | end 358 | end 359 | 360 | a_BlockArea:SetRelBlockTypeMeta(a_RelX, a_RelY, a_RelZ, a_BlockTable:Get(a_RelX, a_RelY, a_RelZ)) 361 | NumAffectedBlocks = NumAffectedBlocks + 1 362 | end 363 | 364 | local StepSizeX = SizeX / SizeY / 2 365 | local StepSizeZ = SizeZ / SizeY / 2 366 | 367 | -- Makes a hollow layer 368 | local HollowLayer = function(a_Y) 369 | local MinX = math.floor(a_Y * StepSizeX) 370 | local MaxX = math.ceil(SizeX - MinX) 371 | local MinZ = math.floor(a_Y * StepSizeZ) 372 | local MaxZ = math.ceil(SizeZ - MinZ) 373 | for X = MinX, MaxX do 374 | SetBlock(X, a_Y, MinZ) 375 | SetBlock(X, a_Y, MaxZ) 376 | end 377 | 378 | for Z = MinZ + 1, MaxZ - 1 do 379 | SetBlock(MinX, a_Y, Z) 380 | SetBlock(MaxX, a_Y, Z) 381 | end 382 | end 383 | 384 | -- Makes a solid layer 385 | local SolidLayer = function(a_Y) 386 | local MinX = math.floor(a_Y * StepSizeX) 387 | local MaxX = math.ceil(SizeX - MinX) 388 | local MinZ = math.floor(a_Y * StepSizeZ) 389 | local MaxZ = math.ceil(SizeZ - MinZ) 390 | for X = MinX, MaxX do 391 | for Z = MinZ, MaxZ do 392 | SetBlock(X, a_Y, Z) 393 | end 394 | end 395 | end 396 | 397 | -- Choose the layer handler 398 | local LayerHandler = (a_IsHollow and HollowLayer) or SolidLayer; 399 | 400 | -- Call the layer handler on each layer. 401 | for Y = 0, SizeY do 402 | LayerHandler(Y) 403 | end 404 | 405 | -- Return the number of changed blocks 406 | return NumAffectedBlocks; 407 | end 408 | -------------------------------------------------------------------------------- /Classes/ToolRegistrator.lua: -------------------------------------------------------------------------------- 1 | 2 | -- ToolRegistrator.lua 3 | 4 | -- Implements the cToolRegistrator class representing the tools used by a player. 5 | 6 | 7 | 8 | 9 | 10 | cToolRegistrator = {} 11 | 12 | 13 | 14 | 15 | 16 | function cToolRegistrator:new(a_Obj) 17 | -- Create the class instance: 18 | a_Obj = a_Obj or {} 19 | setmetatable(a_Obj, cToolRegistrator) 20 | self.__index = self 21 | 22 | -- Initialize the object members: 23 | a_Obj.RightClickTools = {} 24 | a_Obj.LeftClickTools = {} 25 | a_Obj.Masks = {} 26 | 27 | -- Bind the tools like navigation. 28 | a_Obj:BindAbsoluteTools() 29 | 30 | return a_Obj 31 | end 32 | 33 | 34 | 35 | 36 | 37 | function cToolRegistrator:BindAbsoluteTools() 38 | local function RightClickCompassCallback(a_Player, _, _, _, a_BlockFace) 39 | -- The player can't use the navigation tool because he doesn't have permission use it. 40 | if (not a_Player:HasPermission("worldedit.navigation.thru.tool")) then 41 | return false 42 | end 43 | 44 | if (a_BlockFace ~= BLOCK_FACE_NONE) then 45 | return true 46 | end 47 | 48 | RightClickCompass(a_Player) 49 | return true 50 | end 51 | 52 | local LastLeftClick = -math.huge 53 | local function LeftClickCompassCallback(a_Player, _, _, _, a_BlockFace) 54 | -- The player can't use the navigation tool because he doesn't have permission use it. 55 | if (not a_Player:HasPermission("worldedit.navigation.jumpto.tool")) then 56 | return false 57 | end 58 | 59 | if (a_BlockFace ~= BLOCK_FACE_NONE) then 60 | return true 61 | end 62 | 63 | if ((os.clock() - LastLeftClick) < 0.20) then 64 | return true 65 | end 66 | 67 | LastLeftClick = os.clock() 68 | LeftClickCompass(a_Player) 69 | return true 70 | end 71 | 72 | local timeSinceLastRightWand = -math.huge 73 | local function RightClickWandItem(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 74 | local Succ, Message = GetPlayerState(a_Player).Selection:SetPos(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, "Second") 75 | if (not Succ) then 76 | return false 77 | end 78 | 79 | if ((os.clock() - timeSinceLastRightWand) < 0.2) then 80 | return; 81 | end 82 | 83 | timeSinceLastRightWand = os.clock() 84 | a_Player:SendMessage(Message) 85 | return true 86 | end 87 | 88 | local function LeftClickWandItem(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 89 | local Succ, Message = GetPlayerState(a_Player).Selection:SetPos(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, "First") 90 | if (not Succ) then 91 | return false 92 | end 93 | 94 | a_Player:SendMessage(Message) 95 | return true 96 | end 97 | 98 | self:BindRightClickTool(g_Config.NavigationWand.Item, RightClickCompassCallback, "thru tool", true) 99 | self:BindRightClickTool(g_Config.WandItem, RightClickWandItem, "selection", true) 100 | self:BindLeftClickTool(g_Config.NavigationWand.Item, LeftClickCompassCallback, "jumpto tool", true) 101 | self:BindLeftClickTool(g_Config.WandItem, LeftClickWandItem, "selection", true) 102 | end 103 | 104 | 105 | 106 | 107 | 108 | -- Get the blocks from the mask on this item. If the item hasn't a mask, it returns nil. 109 | function cToolRegistrator:GetMask(a_ItemType) 110 | if (self.Masks[a_ItemType] == nil) then 111 | return nil 112 | end 113 | 114 | return self.Masks[a_ItemType] 115 | end 116 | 117 | 118 | 119 | 120 | 121 | -- Binds a mask to a given item. Returns true on success and returns false + errormessage when it fails. 122 | function cToolRegistrator:BindMask(a_ItemType, a_Blocks) 123 | self.Masks[a_ItemType] = a_Blocks 124 | return true 125 | end 126 | 127 | 128 | 129 | 130 | 131 | function cToolRegistrator:UnbindMask(a_ItemType) 132 | if (self.Masks[a_ItemType] == nil) then 133 | return false, "The item didn't have any masks bound on it." 134 | end 135 | 136 | self.Masks[a_ItemType] = nil 137 | return true 138 | end 139 | 140 | 141 | 142 | 143 | 144 | -- Binds an callback to a given item. Returns true on succes and returns false + errormessage when it fails. 145 | -- If an array is given then the function will loop through it calling itself with the itemtypes in the array. The returned error will be a table with errors. 146 | -- The callback is called when the player right clicks with the tool in hand. 147 | -- The toolname is the name of the tool 148 | -- IsAbsolute is a bool value. If false no other tool can bind that tool, and UnbindTool won't unbind it. 149 | function cToolRegistrator:BindRightClickTool(a_ItemType, a_UsageCallback, a_ToolName, a_IsAbsolute) 150 | if (type(a_ItemType) == "table") then 151 | local Succes, Error = nil, {} 152 | for Idx, ItemType in ipairs(a_ItemType) do 153 | local Suc, Err = self:BindRightClickTool(ItemType, a_UsageCallback, a_ToolName) 154 | Succes = Succes and Suc, not Suc and table.insert(Error, Err) 155 | end 156 | 157 | return Succes, Error 158 | end 159 | 160 | if ((self.RightClickTools[a_ItemType] ~= nil) and self.RightClickTools[a_ItemType].IsAbsolute) then 161 | return false, "Can't bind tool to \"" .. ItemTypeToString(a_ItemType) .. "\": Already used for the " .. self.RightClickTools[a_ItemType].ToolName 162 | end 163 | 164 | self.RightClickTools[a_ItemType] = {Callback = a_UsageCallback, ToolName = a_ToolName, IsAbsolute = a_IsAbsolute} 165 | return true 166 | end 167 | 168 | 169 | 170 | 171 | 172 | -- Uses the right click tool. Returns false when it fails. Else it returns what the callback returns. 173 | function cToolRegistrator:UseRightClickTool(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_ItemType) 174 | if (self.RightClickTools[a_ItemType] == nil) then 175 | return false 176 | end 177 | 178 | -- Let the handler decide if the callback from Cuberite should return true or false. 179 | return self.RightClickTools[a_ItemType].Callback(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 180 | end 181 | 182 | 183 | 184 | 185 | 186 | -- Returns the info about a left click registered tool. This is a table with the callback, and the name of the tool. If there was no tool for the item type it returns false 187 | function cToolRegistrator:GetRightClickCallbackInfo(a_ItemType) 188 | return self.RightClickTools[a_ItemType] or false 189 | end 190 | 191 | 192 | 193 | 194 | 195 | -- Binds an callback to a given item. Returns true on succes and returns false + errormessage when it fails. 196 | -- If an array is given then the function will loop through it calling itself with the itemtypes in the array. The returned error will be a table with errors. 197 | -- The callback is called when the player left clicks with the tool in hand. 198 | -- The toolname is the name of the tool 199 | -- IsAbsolute is a bool value. If false no other tool can bind that tool, and UnbindTool won't unbind it. 200 | function cToolRegistrator:BindLeftClickTool(a_ItemType, a_UsageCallback, a_ToolName, a_IsAbsolute) 201 | if (type(a_ItemType) == "table") then 202 | local Succes, Error = nil, {} 203 | for Idx, ItemType in ipairs(a_ItemType) do 204 | local Suc, Err = self:BindLeftClickTool(ItemType, a_UsageCallback, a_ToolName) 205 | Succes = Succes and Suc, not Suc and table.insert(Error, Err) 206 | end 207 | 208 | return Succes, Error 209 | end 210 | 211 | if ((self.LeftClickTools[a_ItemType] ~= nil) and self.LeftClickTools[a_ItemType].IsAbsolute) then 212 | return false, "Can't bind tool to \"" .. ItemTypeToString(a_ItemType) .. "\": Already used for the " .. self.LeftClickTools[a_ItemType].ToolName 213 | end 214 | 215 | self.LeftClickTools[a_ItemType] = {Callback = a_UsageCallback, ToolName = a_ToolName, IsAbsolute = a_IsAbsolute} 216 | return true 217 | end 218 | 219 | 220 | 221 | 222 | 223 | -- Uses the left click tool. Returns false when it fails. Else it returns what the callback returns. 224 | function cToolRegistrator:UseLeftClickTool(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_ItemType) 225 | if (self.LeftClickTools[a_ItemType] == nil) then 226 | return false 227 | end 228 | return self.LeftClickTools[a_ItemType].Callback(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 229 | end 230 | 231 | 232 | 233 | 234 | 235 | -- Returns the info about a left click registered tool. This is a table with the callback, and the name of the tool. If there was no tool for the item type it returns false 236 | function cToolRegistrator:GetLeftClickCallbackInfo(a_ItemType) 237 | return self.LeftClickTools[a_ItemType] or false 238 | end 239 | 240 | 241 | 242 | 243 | 244 | -- Unbinds a tool from it's callback. Returns true on succes and returns false + errormessage when it fails. 245 | -- The itemtype can also be an array with itemtypes. If this is the case then the function will loop through the array and call itself with the itemtypes in the array. 246 | -- The result will in that case be a boolean + an array with errors. 247 | -- a_ToolName is a string or nil. If it's a string then the tool will only be unbound if the current tool is a_ToolName 248 | function cToolRegistrator:UnbindTool(a_ItemType, a_ToolName) 249 | if (type(a_ItemType) == "table") then 250 | local Succes, Errors = nil, {} 251 | for Idx, ItemType in ipairs(a_ItemType) do 252 | local Suc, Err = self:UnbindTool(ItemType, a_ToolName) 253 | Succes = Succes and Suc, not Suc and table.insert(Errors, Err) 254 | end 255 | 256 | return Succes, Errors 257 | end 258 | 259 | if ((self.RightClickTools[a_ItemType] == nil) and (self.LeftClickTools[a_ItemType] == nil)) then 260 | return false, "The item didn't have any tools bound to it." 261 | end 262 | 263 | if (a_ToolName) then 264 | if ((self.RightClickTools[a_ItemType] or {}).ToolName == a_ToolName) then 265 | self.RightClickTools[a_ItemType] = nil 266 | end 267 | if ((self.LeftClickTools[a_ItemType] or {}).ToolName == a_ToolName) then 268 | self.LeftClickTools[a_ItemType] = nil 269 | end 270 | 271 | return true 272 | end 273 | 274 | -- Only unbind the tool if it isn't absolute 275 | if ((self.LeftClickTools[a_ItemType] ~= nil) and (not self.LeftClickTools[a_ItemType].IsAbsolute)) then 276 | self.LeftClickTools[a_ItemType] = nil 277 | end 278 | 279 | if ((self.RightClickTools[a_ItemType] ~= nil) and (not self.RightClickTools[a_ItemType].IsAbsolute)) then 280 | self.RightClickTools[a_ItemType] = nil 281 | end 282 | return true 283 | end 284 | 285 | 286 | 287 | 288 | 289 | local function RightClickToolsHook(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ) 290 | local State = GetPlayerState(a_Player) 291 | 292 | return State.ToolRegistrator:UseRightClickTool(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_Player:GetEquippedItem().m_ItemType) 293 | end 294 | 295 | 296 | 297 | 298 | 299 | local function LeftClickToolsHook(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_Action) 300 | if (a_Action ~= 0) then 301 | -- Left click is also called for other things like throwing items 302 | return false 303 | end 304 | 305 | local State = GetPlayerState(a_Player) 306 | return State.ToolRegistrator:UseLeftClickTool(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_Player:GetEquippedItem().m_ItemType) 307 | end 308 | 309 | 310 | 311 | 312 | 313 | local function LeftClickToolsAnimationHook(a_Player, a_Animation) 314 | -- In 1.8.x the left click has a value of 0, while in 1.7.x it's 1 315 | local LeftClickAnimation = (a_Player:GetClientHandle():GetProtocolVersion() > 5) and 0 or 1 316 | if (a_Animation ~= LeftClickAnimation) then 317 | return false 318 | end 319 | 320 | local State = GetPlayerState(a_Player) 321 | return State.ToolRegistrator:UseLeftClickTool(a_Player, 0, 0, 0, BLOCK_FACE_NONE, a_Player:GetEquippedItem().m_ItemType) 322 | end 323 | 324 | 325 | 326 | 327 | 328 | -- Register the hooks needed: 329 | cPluginManager:AddHook(cPluginManager.HOOK_PLAYER_RIGHT_CLICK, RightClickToolsHook); 330 | cPluginManager:AddHook(cPluginManager.HOOK_PLAYER_LEFT_CLICK, LeftClickToolsHook); 331 | cPluginManager:AddHook(cPluginManager.HOOK_PLAYER_ANIMATION, LeftClickToolsAnimationHook); 332 | 333 | 334 | -------------------------------------------------------------------------------- /Classes/UndoStack.lua: -------------------------------------------------------------------------------- 1 | 2 | -- UndoStack.lua 3 | 4 | -- Implements the cUndoStack class representing a single stack of Undo / Redo operations 5 | 6 | 7 | 8 | 9 | 10 | cUndoStack = {} 11 | 12 | 13 | 14 | 15 | 16 | function cUndoStack:new(a_Obj, a_MaxDepth, a_PlayerState) 17 | assert(a_MaxDepth ~= nil) 18 | assert(a_PlayerState ~= nil) 19 | 20 | -- Create the class instance: 21 | a_Obj = a_Obj or {} 22 | setmetatable(a_Obj, cUndoStack) 23 | self.__index = self 24 | 25 | -- Initialize the object members: 26 | a_Obj.MaxDepth = a_MaxDepth 27 | a_Obj.UndoStack = {} 28 | a_Obj.RedoStack = {} 29 | 30 | return a_Obj 31 | end 32 | 33 | 34 | 35 | 36 | 37 | --- Applies a snapshot from src stack, saving the world contents into the dst stack first. 38 | -- This performs the actual undo or redo, based on what stacks get passed in. 39 | -- a_World is the world where the operation is performed. Only stack entries matching the world are considered. 40 | -- Returns true if successful, false + reason if not 41 | function cUndoStack:ApplySnapshot(a_SrcStack, a_DstStack, a_World) 42 | assert(type(a_SrcStack) == "table") 43 | assert(type(a_DstStack) == "table") 44 | assert(a_World ~= nil) 45 | 46 | -- Find the src snapshot to apply: 47 | local Src = self:PopLastSnapshotInWorld(a_SrcStack, a_World:GetName()) 48 | if (Src == nil) then 49 | -- There's no snapshot to apply 50 | return false, "No snapshot to apply" 51 | end 52 | 53 | -- Save a snapshot to dst stack: 54 | local MinX, MinY, MinZ = Src.Area:GetOrigin() 55 | local MaxX = MinX + Src.Area:GetSizeX() 56 | local MaxY = MinY + Src.Area:GetSizeY() 57 | local MaxZ = MinZ + Src.Area:GetSizeZ() 58 | local BackupArea = cBlockArea() 59 | if not(BackupArea:Read(a_World, MinX, MaxX, MinY, MaxY, MinZ, MaxZ)) then 60 | return false, "Cannot backup the destination" 61 | end 62 | table.insert(a_DstStack, {WorldName = Src.WorldName, Area = BackupArea, Name = Src.Name}) 63 | 64 | -- Write the src snapshot: 65 | Src.Area:Write(a_World, MinX, MinY, MinZ) 66 | a_World:WakeUpSimulatorsInArea(cCuboid( 67 | Vector3i(MinX - 1, MinY - 1, MinZ - 1), 68 | Vector3i(MaxX + 1, MaxY + 1, MaxZ + 1) 69 | )) 70 | 71 | -- Clean up memory used by the snapshot: 72 | Src.Area:Clear() 73 | return true 74 | end 75 | 76 | 77 | 78 | 79 | 80 | --- Removes all items from the Redo stack 81 | function cUndoStack:DropAllRedo() 82 | -- Clear all the areas now so that they don't keep their blocktypes in memory until GC kicks in 83 | for _, redo in ipairs(self.RedoStack) do 84 | redo.Area:Clear() 85 | end 86 | 87 | self.RedoStack = {} 88 | end 89 | 90 | 91 | 92 | 93 | 94 | --- Returns the last snapshot from the stack that matches the worldname 95 | -- Removes the snapshot from the stack. 96 | -- Returns nil if no matching snapshot 97 | function cUndoStack:PopLastSnapshotInWorld(a_Stack, a_WorldName) 98 | assert(type(a_Stack) == "table") 99 | assert(type(a_WorldName) == "string") 100 | 101 | -- Walk the snapshots most-recent-first, check worldname: 102 | for idx = #a_Stack, 1, -1 do 103 | if (a_Stack[idx].WorldName == a_WorldName) then 104 | -- Found a suitable snapshot, return it and remove it from the stack: 105 | local res = a_Stack[idx] 106 | table.remove(a_Stack, idx) 107 | return res 108 | end 109 | end 110 | 111 | -- No matching snapshot found: 112 | return nil 113 | end 114 | 115 | 116 | 117 | 118 | 119 | --- Pushes one level of Undo onto the Undo stack and clears the Redo stack 120 | -- a_World is the world where the area belongs 121 | -- a_Area is expected to have its origin set to where the Undo is located in the world 122 | -- a_Name is the optional display name for the Undo 123 | -- No return value 124 | function cUndoStack:PushUndo(a_World, a_Area, a_Name) 125 | assert(a_World ~= nil) 126 | assert(a_Area ~= nil) 127 | 128 | -- Drop all Redo from the Redo stack (they have just been invalidated): 129 | self:DropAllRedo() 130 | 131 | -- Push the new Undo onto the stack: 132 | table.insert(self.UndoStack, {WorldName = a_World:GetName(), Area = a_Area, Name = a_Name}) 133 | 134 | -- If the stack is too big, trim the oldest item: 135 | if (#self.UndoStack > self.MaxDepth) then 136 | -- Clear the area now so that it doesn't keep its blocktypes in memory until GC kicks in 137 | self.UndoStack[1].Area:Clear() 138 | table.remove(self.UndoStack, 1) 139 | end 140 | end 141 | 142 | 143 | 144 | 145 | 146 | --- Pushes one level of Undo onto the Undo stack and clears the Redo stack 147 | -- Reads the area for the undo from the specified world in the specified cuboid 148 | -- a_Name is the optional display name for the Undo 149 | -- Returns true on success, false and message on failure 150 | function cUndoStack:PushUndoFromCuboid(a_World, a_Cuboid, a_Name) 151 | assert(tolua.type(a_World) == "cWorld") 152 | assert(tolua.type(a_Cuboid) == "cCuboid") 153 | 154 | -- Read the area: 155 | local Area = cBlockArea() 156 | if not(Area:Read( 157 | a_World, 158 | a_Cuboid.p1.x, a_Cuboid.p2.x, 159 | a_Cuboid.p1.y, a_Cuboid.p2.y, 160 | a_Cuboid.p1.z, a_Cuboid.p2.z 161 | )) then 162 | return false, "cannot read block area" 163 | end 164 | 165 | -- Push the Undo: 166 | self:PushUndo(a_World, Area, a_Name) 167 | return true 168 | end 169 | 170 | 171 | 172 | 173 | 174 | --- Redoes one operation from the UndoStack (pushes previous to UndoStack) 175 | -- Returns true if successful, false + reason if not 176 | function cUndoStack:Redo(a_World) 177 | -- Apply one snapshot from RedoStack to UndoStack: 178 | return self:ApplySnapshot(self.RedoStack, self.UndoStack, a_World) 179 | end 180 | 181 | 182 | 183 | 184 | 185 | --- Undoes one operation from the UndoStack (pushes previous to RedoStack) 186 | -- Returns true if successful, false + reason if not 187 | function cUndoStack:Undo(a_World) 188 | -- Apply one snapshot from UndoStack to RedoStack: 189 | return self:ApplySnapshot(self.UndoStack, self.RedoStack, a_World) 190 | end 191 | -------------------------------------------------------------------------------- /Classes/Updater.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Updater.lua 3 | 4 | -- Contains the cUpdater class used to CheckForNewerVersion if there is a newer version of WorldEdit available 5 | 6 | 7 | 8 | 9 | cUpdater = {} 10 | 11 | 12 | 13 | 14 | cUpdater.s_CheckAttempts = 0 15 | cUpdater.s_DownloadAttempts = 0 16 | 17 | 18 | 19 | 20 | function cUpdater:CheckForNewerVersion() 21 | if (cUpdater.s_CheckAttempts > g_Config.Updates.NumAttempts) then 22 | LOGWARNING("Could not connect to Github to check for a newer version for WorldEdit") 23 | return; 24 | end 25 | cUpdater.s_CheckAttempts = cUpdater.s_CheckAttempts + 1; 26 | 27 | cUrlClient:Get("https://raw.githubusercontent.com/cuberite/WorldEdit/master/Info.lua", 28 | function(a_Body, a_Data) 29 | if (a_Body) then 30 | cUpdater:ParsePluginInfo(a_Body) 31 | else 32 | -- The request failed, schedule a retry 33 | cRoot:Get():GetDefaultWorld():QueueTask( 34 | function() 35 | cUpdater:CheckForNewerVersion() 36 | end 37 | ) 38 | end 39 | end 40 | ) 41 | end 42 | 43 | 44 | 45 | 46 | 47 | function cUpdater:DownloadLatestVersion(a_DisplayVersion) 48 | if (cUpdater.s_DownloadAttempts > g_Config.Updates.NumAttempts) then 49 | LOGWARNING("Error while downloading newer worldedit version") 50 | return 51 | end 52 | cUpdater.s_DownloadAttempts = cUpdater.s_DownloadAttempts + 1; 53 | 54 | cUrlClient:Get("https://raw.githubusercontent.com/cuberite/WorldEdit/zip/master", 55 | function (a_Body, a_Data) 56 | if (not a_Body or (a_Body:len() ~= tonumber(a_Data["Content-Length"]))) then 57 | -- Downloading failed, schedule a retry 58 | cRoot:Get():GetDefaultWorld():QueueTask( 59 | function() 60 | cUpdater:DownloadLatestVersion(a_DisplayVersion) 61 | end 62 | ) 63 | return 64 | end 65 | 66 | -- Write the ZIP data to the file. The filename looks like this: "WorldEdit v.zip" 67 | local ZipFile = assert(io.open("Plugins/WorldEdit v" .. a_DisplayVersion .. ".zip", "wb"), "Failed to open \"Plugins/WorldEdit v" .. (a_DisplayVersion or "_Unknown") .. ".zip\"") 68 | ZipFile:write(a_Body) 69 | ZipFile:close() 70 | 71 | LOGINFO(string.format("New WorldEdit version downloaded to %q", "Plugins/WorldEdit v" .. a_DisplayVersion .. ".zip")) 72 | end 73 | ) 74 | end 75 | 76 | 77 | 78 | 79 | 80 | function cUpdater:ParsePluginInfo(a_PluginInfo) 81 | local Func, ErrMsg = loadstring(a_PluginInfo) 82 | if (not Func) then 83 | LOGWARNING("Error while checking for newer WorldEdit version") 84 | return 85 | end 86 | 87 | local Env = {} 88 | 89 | -- Protect from malicious code (though this shouldn't be possible) 90 | setfenv(Func, Env) 91 | 92 | -- Execute the code 93 | Func() 94 | 95 | -- Extract the plugin version: 96 | if (not Env.g_PluginInfo) then 97 | LOGWARNING("Error while checking for newer WorldEdit version") 98 | return 99 | end 100 | 101 | if (type(Env.g_PluginInfo.Version) ~= "number") then 102 | LOGWARNING("Error while checking for newer WorldEdit version") 103 | return 104 | end 105 | 106 | if (Env.g_PluginInfo.Version <= g_PluginInfo.Version) then 107 | if (g_Config.Updates.ShowMessageWhenUpToDate) then 108 | LOGINFO("Your WorldEdit plugin is up-to-date") 109 | end 110 | return 111 | end 112 | 113 | LOGINFO("There is a newer WorldEdit version available: v" .. Env.g_PluginInfo.DisplayVersion) 114 | if (not g_Config.Updates.DownloadNewerVersion) then 115 | return 116 | end 117 | 118 | cUpdater:DownloadLatestVersion(Env.g_PluginInfo.DisplayVersion) 119 | end 120 | -------------------------------------------------------------------------------- /Commands/Biome.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | function HandleSetBiomeCommand(a_Split, a_Player) 7 | local function SendWrongArguments(Reason) 8 | a_Player:SendMessage(cChatColor.Rose .. Reason .. " arguments.") 9 | a_Player:SendMessage(cChatColor.Rose .. "//setbiome [-p] ") 10 | a_Player:SendMessage(cChatColor.Rose .. "") -- Extra space 11 | a_Player:SendMessage(cChatColor.Rose .. "Sets the biome of the region.") 12 | a_Player:SendMessage(cChatColor.Rose .. "By default sets the biome in your selected area.") 13 | a_Player:SendMessage(cChatColor.Rose .. "-p sets biome in the column you are currently standing in.") 14 | end 15 | 16 | if #a_Split == 1 then 17 | SendWrongArguments("Too few") 18 | return true 19 | end 20 | 21 | if #a_Split > 3 then 22 | SendWrongArguments("Too many") 23 | return true 24 | end 25 | 26 | local World = a_Player:GetWorld() 27 | local PosX = math.floor(a_Player:GetPosX()) 28 | local PosZ = math.floor(a_Player:GetPosZ()) 29 | 30 | if #a_Split == 3 then 31 | if a_Split[2] ~= "-p" then 32 | SendWrongArguments("Too many") 33 | return true 34 | end 35 | 36 | local NewBiome = StringToBiome(a_Split[3]) 37 | if NewBiome == biInvalidBiome then 38 | a_Player:SendMessage(cChatColor.Rose .. "Unknown biome type: '" .. a_Split[3] .. "'.") 39 | return true 40 | end 41 | 42 | World:SetAreaBiome(PosX, PosX, PosZ, PosZ, NewBiome) 43 | a_Player:SendMessage(cChatColor.LightPurple .. "Biome changed to " .. a_Split[3] .. " at your current location.") 44 | return true 45 | elseif #a_Split == 2 then 46 | local NewBiome = StringToBiome(a_Split[2]) 47 | if NewBiome == biInvalidBiome then 48 | a_Player:SendMessage(cChatColor.Rose .. "Unknown " .. a_Split[2] .. " biome type.") 49 | return true 50 | end 51 | 52 | local State = GetPlayerState(a_Player) 53 | if not(State.Selection:IsValid()) then 54 | a_Player:SendMessage(cChatColor.Rose .. "You need to select a region first.") 55 | return true 56 | end 57 | local MinX, MaxX = State.Selection:GetXCoordsSorted() 58 | local MinZ, MaxZ = State.Selection:GetZCoordsSorted() 59 | 60 | World:SetAreaBiome(MinX, MaxX, MinZ, MaxZ, NewBiome) 61 | a_Player:SendMessage(cChatColor.LightPurple .. "Biome changed to " .. a_Split[2] .. ". " .. (1 + MaxX - MinX) * (1 + MaxZ - MinZ) .. " columns affected.") 62 | return true 63 | end 64 | return true 65 | end 66 | 67 | 68 | 69 | 70 | 71 | function HandleBiomeListCommand(a_Split, a_Player) 72 | -- /biomelist 73 | 74 | local Page = a_Split[2] ~= nil and a_Split[2] or 1 75 | 76 | -- TODO: Load the biomes on start, not when the command is executed 77 | local Biomes = {} 78 | for Key, Value in pairs(_G) do 79 | if (Key:match("bi(.*)")) then 80 | table.insert(Biomes, BiomeToString(Value)) 81 | end 82 | end 83 | table.sort(Biomes) 84 | 85 | a_Player:SendMessage(cChatColor.Green .. "Page " .. Page .. "/" .. math.floor(#Biomes / 8)) 86 | 87 | local MinIndex = Page * 8 88 | local MaxIndex = MinIndex + 8 89 | for I = MinIndex, MaxIndex do 90 | local Biome = Biomes[I] 91 | if (not Biome) then 92 | break 93 | end 94 | 95 | a_Player:SendMessage(cChatColor.LightPurple .. Biome) 96 | end 97 | return true 98 | end 99 | 100 | 101 | 102 | 103 | 104 | function HandleBiomeInfoCommand(a_Split, a_Player) 105 | -- /biomeinfo 106 | 107 | -- If a "-p" param is present, report the biome at player's position: 108 | if (a_Split[2] == "-p") then 109 | local Biome = BiomeToString(a_Player:GetWorld():GetBiomeAt(math.floor(a_Player:GetPosX()), math.floor(a_Player:GetPosZ()))) 110 | a_Player:SendMessage(cChatColor.LightPurple .. "Biome: " .. Biome) 111 | return true 112 | end 113 | 114 | -- Get the player state: 115 | local State = GetPlayerState(a_Player) 116 | if not(State.Selection:IsValid()) then 117 | a_Player:SendMessage(cChatColor.Rose .. "Make a region selection first.") 118 | return true 119 | end 120 | 121 | -- Retrieve set of biomes in the selection: 122 | local BiomesSet = {} 123 | local MinX, MaxX = State.Selection:GetXCoordsSorted() 124 | local MinZ, MaxZ = State.Selection:GetZCoordsSorted() 125 | local World = a_Player:GetWorld() 126 | for X = MinX, MaxX do 127 | for Z = MinZ, MaxZ do 128 | BiomesSet[World:GetBiomeAt(X, Z)] = true 129 | end 130 | end 131 | 132 | -- Convert set to array of names: 133 | local BiomesArr = {} 134 | for b, val in pairs(BiomesSet) do 135 | if (val) then 136 | table.insert(BiomesArr, BiomeToString(b)) 137 | end 138 | end 139 | 140 | -- Send the list to the player: 141 | a_Player:SendMessage(cChatColor.LightPurple .. "Biomes: " .. table.concat(BiomesArr, ", ")) 142 | return true 143 | end 144 | -------------------------------------------------------------------------------- /Commands/Brush.lua: -------------------------------------------------------------------------------- 1 | 2 | -- cmd_Brush.lua 3 | 4 | -- Implements command handlers for the brush commands 5 | 6 | 7 | 8 | 9 | 10 | function HandleMaskCommand(a_Split, a_Player) 11 | -- /mask 12 | 13 | if (#a_Split == 1) then 14 | -- Remove mask 15 | local State = GetPlayerState(a_Player) 16 | local Succes, error = State.ToolRegistrator:UnbindMask(a_Player:GetEquippedItem().m_ItemType) 17 | 18 | if (not Succes) then 19 | a_Player:SendMessage(cChatColor.Rose .. error) 20 | return true 21 | end 22 | a_Player:SendMessage(cChatColor.LightPurple .. "Brush mask disabled.") 23 | return true 24 | end 25 | 26 | -- Retrieve the blocktypes from the params: 27 | local Mask, ErrBlock = cMask:new(a_Split[2]) 28 | if not(Mask) then 29 | a_Player:SendMessage(cChatColor.Rose .. "Unknown block type: '" .. ErrBlock .. "'.") 30 | return true 31 | end 32 | 33 | local State = GetPlayerState(a_Player) 34 | local Succes, error = State.ToolRegistrator:BindMask(a_Player:GetEquippedItem().m_ItemType, Mask) 35 | 36 | if (not Succes) then 37 | a_Player:SendMessage(cChatColor.Rose .. error) 38 | return true 39 | end 40 | a_Player:SendMessage(cChatColor.LightPurple .. "Brush mask set.") 41 | return true 42 | end 43 | 44 | 45 | 46 | 47 | 48 | function HandleSphereBrush(a_Split, a_Player) 49 | -- //brush sphere [-h] 50 | 51 | if (#a_Split < 4) then 52 | a_Player:SendMessage(cChatColor.Rose .. "Usage: /brush sphere [-h] ") 53 | return true 54 | end 55 | 56 | local Hollow = false 57 | if (a_Split[3] == "-h") then 58 | Hollow = true 59 | table.remove(a_Split, 3) 60 | end 61 | 62 | -- Retrieve the blocktypes from the params: 63 | local BlockTable, ErrBlock = GetBlockDst(a_Split[3], a_Player) 64 | if not(BlockTable) then 65 | a_Player:SendMessage(cChatColor.LightPurple .. "Unknown block type: '" .. ErrBlock .. "'.") 66 | return true 67 | end 68 | 69 | -- Convert the Radius param: 70 | local Radius = tonumber(a_Split[4]) 71 | if not(Radius) then 72 | a_Player:SendMessage(cChatColor.Rose .. "Cannot convert radius \"" .. a_Split[4] .. "\" to a number.") 73 | return true 74 | end 75 | 76 | -- The radius is too large 77 | if (Radius > g_Config.Limits.MaxBrushRadius) then 78 | a_Player:SendMessage(cChatColor.Rose .. "Maximum brush radius: " .. g_Config.Limits.MaxBrushRadius) 79 | return true 80 | end 81 | 82 | -- The player state is used to get the player's mask, and to bind the tool 83 | local State = GetPlayerState(a_Player) 84 | 85 | -- Initialize the handler. 86 | local function BrushHandler(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 87 | local Position = (a_BlockFace == BLOCK_FACE_NONE and GetTargetBlock(a_Player)) or Vector3i(a_BlockX, a_BlockY, a_BlockZ) 88 | 89 | if (not Position) then 90 | return true 91 | end 92 | 93 | local AffectedArea = cCuboid(Position, Position) 94 | AffectedArea:Expand(Radius, Radius, Radius, Radius, Radius, Radius) 95 | AffectedArea:Sort() 96 | 97 | -- Get the mask. We can't put this outside the brush handler, because the player might have changed it already. 98 | local Mask = State.ToolRegistrator:GetMask(a_Player:GetEquippedItem().m_ItemType) 99 | 100 | CreateSphereInCuboid(a_Player, AffectedArea, BlockTable, Hollow, Mask) 101 | return true 102 | end 103 | 104 | local Succes, error = State.ToolRegistrator:BindRightClickTool(a_Player:GetEquippedItem().m_ItemType, BrushHandler, "brush") 105 | if (not Succes) then 106 | a_Player:SendMessage(cChatColor.Rose .. error) 107 | return true 108 | end 109 | 110 | a_Player:SendMessage(cChatColor.LightPurple .. "Sphere brush shape equipped (" .. Radius .. ")") 111 | return true 112 | end 113 | 114 | 115 | 116 | 117 | 118 | function HandleCylinderBrush(a_Split, a_Player) 119 | -- //brush cyl [-h] 120 | 121 | if (#a_Split < 5) then 122 | a_Player:SendMessage(cChatColor.Rose .. "Usage: /brush cylinder [-h] ") 123 | return true 124 | end 125 | 126 | local Hollow = false 127 | if (a_Split[3] == "-h") then 128 | Hollow = true 129 | table.remove(a_Split, 3) 130 | end 131 | 132 | -- Retrieve the blocktypes from the params: 133 | local BlockTable, ErrBlock = GetBlockDst(a_Split[3], a_Player) 134 | if not(BlockTable) then 135 | a_Player:SendMessage(cChatColor.LightPurple .. "Unknown block type: '" .. ErrBlock .. "'.") 136 | return true 137 | end 138 | 139 | -- Convert the Radius param: 140 | local Radius = tonumber(a_Split[4]) 141 | if not(Radius) then 142 | a_Player:SendMessage(cChatColor.Rose .. "Cannot convert radius \"" .. a_Split[4] .. "\" to a number.") 143 | return true 144 | end 145 | 146 | -- The radius is too large 147 | if (Radius > g_Config.Limits.MaxBrushRadius) then 148 | a_Player:SendMessage(cChatColor.Rose .. "Maximum brush radius: " .. g_Config.Limits.MaxBrushRadius) 149 | return true 150 | end 151 | 152 | -- Convert the height param. 153 | local Height = tonumber(a_Split[5]) 154 | if not(Height) then 155 | a_Player:SendMessage(cChatColor.Rose .. "Cannot convert height \"" .. a_Split[5] .. "\" to a number.") 156 | return true 157 | end 158 | 159 | -- The height used in the brush handler. If Height is negative we add one, if positive we lower by one 160 | local UsedHeight = (Height > 0 and (Height - 1)) or (Height + 1) 161 | 162 | -- The player state is used to get the player's mask, and to bind the tool 163 | local State = GetPlayerState(a_Player) 164 | 165 | -- Initialize the handler. 166 | local function BrushHandler(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 167 | local Position = (a_BlockFace == BLOCK_FACE_NONE and GetTargetBlock(a_Player)) or Vector3i(a_BlockX, a_BlockY, a_BlockZ) 168 | 169 | if (not Position) then 170 | return true 171 | end 172 | 173 | local AffectedArea = cCuboid(Position, Position) 174 | AffectedArea:Expand(Radius, Radius, 0, UsedHeight, Radius, Radius) 175 | AffectedArea:Sort() 176 | 177 | -- Get the mask. We can't put this outside the brush handler, because the player might have changed it already. 178 | local Mask = State.ToolRegistrator:GetMask(a_Player:GetEquippedItem().m_ItemType) 179 | 180 | CreateCylinderInCuboid(a_Player, AffectedArea, BlockTable, Hollow, Mask) 181 | return true 182 | end 183 | 184 | local Succes, error = State.ToolRegistrator:BindRightClickTool(a_Player:GetEquippedItem().m_ItemType, BrushHandler, "brush") 185 | if (not Succes) then 186 | a_Player:SendMessage(cChatColor.Rose .. error) 187 | return true 188 | end 189 | 190 | a_Player:SendMessage(cChatColor.LightPurple .. "Cylinder brush shape equipped (" .. Radius .. " by " .. Height .. ")") 191 | return true 192 | end 193 | -------------------------------------------------------------------------------- /Commands/Clipboard.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Clipboard.lua 3 | 4 | -- Implements command handlers for the clipboard-related commands 5 | 6 | 7 | 8 | 9 | function HandleCopyCommand(a_Split, a_Player) 10 | -- //copy 11 | 12 | -- Get the player state: 13 | local State = GetPlayerState(a_Player) 14 | if not(State.Selection:IsValid()) then 15 | a_Player:SendMessage(cChatColor.Rose .. "Make a region selection first.") 16 | return true 17 | end 18 | 19 | -- Check with other plugins if the operation is okay: 20 | local SrcCuboid = State.Selection:GetSortedCuboid() 21 | local World = a_Player:GetWorld() 22 | if (CallHook("OnAreaCopying", a_Player, World, SrcCuboid)) then 23 | return 24 | end 25 | 26 | -- Cut into the clipboard: 27 | local NumBlocks = State.Clipboard:Copy(World, SrcCuboid, SrcCuboid.p1 - Vector3i(a_Player:GetPosition())) 28 | 29 | -- Call other plugins to notify that the player has copied the region 30 | CallHook("OnAreaCopied", a_Player, World, SrcCuboid) 31 | 32 | a_Player:SendMessage(cChatColor.LightPurple .. NumBlocks .. " block(s) copied.") 33 | a_Player:SendMessage(cChatColor.LightPurple .. "Clipboard size: " .. State.Clipboard:GetSizeDesc()) 34 | return true 35 | end 36 | 37 | 38 | 39 | 40 | 41 | function HandleCutCommand(a_Split, a_Player) 42 | -- //cut 43 | 44 | -- Get the player state: 45 | local State = GetPlayerState(a_Player) 46 | if not(State.Selection:IsValid()) then 47 | a_Player:SendMessage(cChatColor.Rose .. "Make a region selection first.") 48 | return true 49 | end 50 | 51 | -- Check with other plugins if the operation is okay: 52 | local SrcCuboid = State.Selection:GetSortedCuboid() 53 | local World = a_Player:GetWorld() 54 | if (CallHook("OnAreaChanging", SrcCuboid, a_Player, World, "cut")) then 55 | return 56 | end 57 | 58 | -- Push an undo snapshot: 59 | State.UndoStack:PushUndoFromCuboid(World, SrcCuboid, "cut") 60 | 61 | -- Cut into the clipboard: 62 | local NumBlocks = State.Clipboard:Cut(World, SrcCuboid, SrcCuboid.p1 - Vector3i(a_Player:GetPosition())) 63 | 64 | -- Notify the plugins that the cut was succesfull. 65 | CallHook("OnAreaChanged", SrcCuboid, a_Player, World, "cut") 66 | 67 | a_Player:SendMessage(cChatColor.LightPurple .. NumBlocks .. " block(s) cut.") 68 | a_Player:SendMessage(cChatColor.LightPurple .. "Clipboard size: " .. State.Clipboard:GetSizeDesc()) 69 | return true 70 | end 71 | 72 | 73 | 74 | 75 | 76 | function HandlePasteCommand(a_Split, a_Player) 77 | -- //paste 78 | 79 | -- Check if there's anything in the clipboard: 80 | local State = GetPlayerState(a_Player) 81 | if not(State.Clipboard:IsValid()) then 82 | a_Player:SendMessage(cChatColor.Rose .. "Your clipboard is empty. Use //copy or //cut first.") 83 | return true 84 | end 85 | 86 | -- Check for parameters 87 | local UseOffset = true 88 | 89 | for Idx, Parameter in ipairs(a_Split) do 90 | if (Parameter == "-no") then -- No offset 91 | UseOffset = false 92 | end 93 | end 94 | 95 | -- Check with other plugins if the operation is okay: 96 | local DstCuboid = State.Clipboard:GetPasteDestCuboid(a_Player, UseOffset) 97 | if (CallHook("OnAreaChanging", DstCuboid, a_Player, a_Player:GetWorld(), "paste")) then 98 | return 99 | end 100 | 101 | -- Paste: 102 | State.UndoStack:PushUndoFromCuboid(a_Player:GetWorld(), DstCuboid, "paste") 103 | local NumBlocks = State.Clipboard:Paste(a_Player, DstCuboid.p1) 104 | 105 | -- Notify other plugins that the clipboard is pasted in the world 106 | CallHook("OnAreaChanged", DstCuboid, a_Player, a_Player:GetWorld(), "paste") 107 | 108 | if (UseOffset) then 109 | a_Player:SendMessage(cChatColor.LightPurple .. NumBlocks .. " block(s) pasted relative to you.") 110 | else 111 | a_Player:SendMessage(cChatColor.LightPurple .. NumBlocks .. " block(s) pasted next to you.") 112 | end 113 | return true 114 | end 115 | 116 | 117 | 118 | 119 | 120 | function HandleRotateCommand(a_Split, a_Player) 121 | -- //rotate [NumDegrees] 122 | 123 | -- Check if the clipboard is valid: 124 | local State = GetPlayerState(a_Player) 125 | if not(State.Clipboard:IsValid()) then 126 | a_Player:SendMessage(cChatColor.Rose .. "Nothing in the clipboard. Use //copy or //cut first.") 127 | return true 128 | end 129 | 130 | -- Check if the player gave an angle: 131 | local Angle = tonumber(a_Split[2]) 132 | if (Angle == nil) then 133 | a_Player:SendMessage(cChatColor.Rose .. "Usage: //rotate [90, 180, 270, -90, -180, -270]") 134 | return true 135 | end 136 | 137 | -- Rotate the clipboard: 138 | local NumRots = math.floor(Angle / 90 + 0.5) -- round to nearest 90-degree step 139 | State.Clipboard:Rotate(NumRots) 140 | a_Player:SendMessage(cChatColor.LightPurple .. "Rotated the clipboard by " .. (NumRots * 90) .. " degrees CCW") 141 | a_Player:SendMessage(cChatColor.LightPurple .. "Clipboard size: " .. State.Clipboard:GetSizeDesc()) 142 | return true 143 | end 144 | -------------------------------------------------------------------------------- /Commands/Entities.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Entities.lua 3 | 4 | -- Contains commands that do things with entities. 5 | 6 | 7 | 8 | 9 | 10 | -- Remove all entities of a or multiple types 11 | function HandleRemoveCommand(a_Split, a_Player) 12 | -- /remove 13 | 14 | -- Collect all the entity names with their corresponding type 15 | local Types = {} 16 | for Key, Value in pairs(cEntity) do 17 | if ((type(Value) == "number") and (Key:sub(1, 2) == "et") and (Value ~= cEntity.etPlayer)) then 18 | Types[Key:sub(3, Key:len()):lower()] = Value 19 | end 20 | end 21 | 22 | -- Check if the player gave a parameter. If not show them a list of acceptable types. 23 | if (a_Split[2] == nil) then 24 | a_Player:SendMessage(cChatColor.Rose .. "Too few arguments.\n/remove ") 25 | local ListEntityNames = "" 26 | for EntityName, _ in pairs(Types) do 27 | ListEntityNames = ListEntityNames .. EntityName .. ", " 28 | end 29 | 30 | a_Player:SendMessage(cChatColor.Rose .. "Acceptable types: " .. ListEntityNames:sub(1, ListEntityNames:len() - 2)) 31 | return true 32 | end 33 | 34 | -- Used to check if there is at least one entity type to be removed. 35 | local NumEntityTypes = 0 36 | 37 | -- Check if the parameters given are valid. If not, then ignore them. 38 | local EntityTypes = {} 39 | local EntityNames = StringSplit(a_Split[2], ",") 40 | for Idx, EntityName in ipairs(EntityNames) do 41 | local LowerCasedEntityName = EntityName:lower() 42 | if (Types[LowerCasedEntityName]) then 43 | EntityTypes[Types[LowerCasedEntityName]] = true 44 | NumEntityTypes = NumEntityTypes + 1 45 | else 46 | a_Player:SendMessage(cChatColor.Rose .. "Unknown entity \"" .. EntityName .. "\". Ignoring it.") 47 | end 48 | end 49 | 50 | -- Bail out of not even one entity type will be removed. 51 | if (NumEntityTypes == 0) then 52 | return true 53 | end 54 | 55 | local NumRemovedEntities = 0 56 | 57 | -- Go through every entity and check if it should be removed 58 | a_Player:GetWorld():ForEachEntity( 59 | function(a_Entity) 60 | if (EntityTypes[a_Entity:GetEntityType()]) then 61 | a_Entity:Destroy() 62 | NumRemovedEntities = NumRemovedEntities + 1 63 | end 64 | end 65 | ) 66 | 67 | -- Send a message to the player. 68 | a_Player:SendMessage(cChatColor.LightPurple .. "Marked " .. NumRemovedEntities .. " entit(ies) for removal.") 69 | return true 70 | end 71 | 72 | 73 | 74 | 75 | 76 | -- Kill all or nearby mobs 77 | function HandleButcherCommand(a_Split, a_Player) 78 | -- /butcher [Radius] 79 | 80 | local Radius; 81 | if (a_Split[2] == nil) then -- if the player did not give a radius then the radius is the normal radius 82 | Radius = g_Config.Defaults.ButcherRadius 83 | elseif (tonumber(a_Split[2]) == nil) then -- if the player gave a string as radius then stop 84 | a_Player:SendMessage(cChatColor.Rose .. 'Number expected; string "' .. a_Split[2] .. '" given') 85 | return true 86 | else -- the radius is set to the given radius 87 | Radius = tonumber(a_Split[2]) 88 | end 89 | 90 | if ((g_Config.Limits.ButcherRadius > 0) and (Radius > g_Config.Limits.ButcherRadius)) then 91 | a_Player:SendMessage(cChatColor.Rose .. 'Maximum butcher radius exceeded.') 92 | return true 93 | end 94 | 95 | -- If this is true then the mob will be destroyed regardless of how far he is from the player. 96 | local ShouldRemoveAllMobs = Radius <= 0 97 | 98 | -- Number of mobs that were destroyed. 99 | local NumDestroyedMobs = 0 100 | 101 | -- Loop through all the entities and destroy all/nearby mobs 102 | a_Player:GetWorld():ForEachEntity( 103 | function(a_Entity) 104 | if (a_Entity:IsMob()) then 105 | if (ShouldRemoveAllMobs) then 106 | a_Entity:Destroy() 107 | NumDestroyedMobs = NumDestroyedMobs + 1 108 | else 109 | if ((a_Player:GetPosition() - a_Entity:GetPosition()):Length() <= Radius) then -- If the mob is inside the radius then destroy it. 110 | a_Entity:Destroy() 111 | NumDestroyedMobs = NumDestroyedMobs + 1 112 | end 113 | end 114 | end 115 | end 116 | ) 117 | 118 | -- Send a message to the player. 119 | a_Player:SendMessage(cChatColor.LightPurple .. "Killed " .. NumDestroyedMobs .. " mobs.") 120 | return true 121 | end 122 | -------------------------------------------------------------------------------- /Commands/Generation.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Generation.lua 3 | 4 | -- Implements the commands from the Generation category 5 | 6 | 7 | 8 | 9 | 10 | function HandleGenerationShapeCommand(a_Split, a_Player) 11 | -- //generate 12 | 13 | local IsHollow = false 14 | local UseRawCoords = false 15 | local Offset = false 16 | local OffsetCenter = false 17 | 18 | local NumFlags = 0 19 | for Idx, Value in ipairs(a_Split) do 20 | NumFlags = NumFlags + 1 21 | if (Value == "-h") then 22 | IsHollow = true 23 | elseif (Value == "-r") then 24 | UseRawCoords = true 25 | elseif (Value == "-o") then 26 | Offset = true 27 | elseif (Value == "-c") then 28 | OffsetCenter = true 29 | else 30 | NumFlags = NumFlags - 1 31 | end 32 | end 33 | 34 | if ((a_Split[2 + NumFlags] == nil) or (a_Split[3 + NumFlags] == nil)) then 35 | a_Player:SendMessage(cChatColor.Rose .. "Too few arguments.") 36 | a_Player:SendMessage(cChatColor.Rose .. "//generate [Flags] ") 37 | return true 38 | end 39 | 40 | -- Check the selection: 41 | local State = GetPlayerState(a_Player) 42 | if not(State.Selection:IsValid()) then 43 | a_Player:SendMessage(cChatColor.Rose .. "No region set") 44 | return true 45 | end 46 | 47 | local BlockTable, ErrBlock = GetBlockDst(a_Split[2 + NumFlags], a_Player) 48 | if not(BlockTable) then 49 | a_Player:SendMessage(cChatColor.LightPurple .. "Unknown block type: '" .. ErrBlock .. "'.") 50 | return true 51 | end 52 | 53 | -- Get the selected area 54 | local SrcCuboid = State.Selection:GetSortedCuboid() 55 | local World = a_Player:GetWorld() 56 | 57 | -- Expand the area in all directions. Otherwise we get some weird results at the sides 58 | SrcCuboid:Expand(1, 1, 1, 1, 1, 1) 59 | SrcCuboid:ClampY(0, 255) 60 | SrcCuboid:Sort() 61 | 62 | local FormulaString = table.concat(a_Split, " ", 3 + NumFlags) 63 | 64 | local zero, unit 65 | 66 | -- Read the selected cuboid into a cBlockArea: 67 | local BA = cBlockArea() 68 | BA:Read(World, SrcCuboid) 69 | 70 | local SizeX, SizeY, SizeZ = BA:GetCoordRange() 71 | SizeX = SizeX - 1 72 | SizeY = SizeY - 1 73 | SizeZ = SizeZ - 1 74 | 75 | -- Get the proper zero and unit values 76 | if (UseRawCoords) then 77 | zero = Vector3f(0, 0, 0) 78 | unit = Vector3f(1, 1, 1) 79 | elseif (Offset) then 80 | zero = Vector3f(SrcCuboid.p1) - Vector3f(a_Player:GetPosition()) 81 | unit = Vector3f(1, 1, 1) 82 | elseif (OffsetCenter) then 83 | -- The lowest coordinate in the region 84 | local Min = Vector3f(0, 0, 0) 85 | 86 | -- The highest coordinate in the region. 87 | local Max = Vector3f(SizeX, SizeY, SizeZ) 88 | 89 | zero = (Max + Min) * 0.5 90 | unit = Vector3f(1, 1, 1) 91 | else 92 | -- The lowest coordinate in the region 93 | local Min = Vector3f(0, 0, 0) 94 | 95 | -- The highest coordinate in the region. 96 | local Max = Vector3f(SizeX, SizeY, SizeZ) 97 | 98 | zero = (Max + Min) * 0.5 99 | unit = Max - zero 100 | end 101 | 102 | local Expression = cExpression:new(FormulaString) 103 | 104 | -- Create the shape generator 105 | local ShapeGenerator, Error = cShapeGenerator:new(zero, unit, BlockTable, Expression, a_Player:HasPermission('worldedit.anyblock')) 106 | if (not ShapeGenerator) then 107 | -- Something went wrong while constructing the ShapeGenerator. 108 | a_Player:SendMessage(cChatColor.Rose .. Error) 109 | return true 110 | end 111 | 112 | -- Check if other plugins want to block this action 113 | if (CallHook("OnAreaChanging", SrcCuboid, a_Player, World, "generate")) then 114 | return true 115 | end 116 | 117 | -- Push an undo snapshot: 118 | State.UndoStack:PushUndoFromCuboid(World, SrcCuboid, "generation") 119 | 120 | -- Get the mask for the equipped item 121 | local Mask = State.ToolRegistrator:GetMask(a_Player:GetEquippedItem().m_ItemType) 122 | 123 | -- Write the shape in the block area 124 | local Success, NumAffectedBlocks = pcall(cShapeGenerator.MakeShape, ShapeGenerator, BA, Vector3f(1, 1, 1), Vector3f(SizeX, SizeY, SizeZ), IsHollow, Mask) 125 | if (not Success) then 126 | a_Player:SendMessage(cChatColor.Rose .. NumAffectedBlocks:match(":%d-: (.+)")) 127 | return true; 128 | end 129 | 130 | -- Send a message to the player with the number of changed blocks 131 | a_Player:SendMessage(cChatColor.LightPurple .. NumAffectedBlocks .. " block(s) changed") 132 | 133 | -- Write the blockarea in the world 134 | BA:Write(World, SrcCuboid.p1) 135 | 136 | -- Notify other plugins that the shape is in the world 137 | CallHook("OnAreaChanged", SrcCuboid, a_Player, World, "generate") 138 | return true 139 | end 140 | 141 | 142 | 143 | 144 | 145 | function HandleCylCommand(a_Split, a_Player) 146 | -- //cyl [Height] 147 | -- //hcyl [Height] 148 | 149 | if ((a_Split[2] == nil) or (a_Split[3] == nil)) then 150 | a_Player:SendMessage(cChatColor.Rose .. "Too few arguments.") 151 | a_Player:SendMessage(cChatColor.Rose .. a_Split[1] .. " [,] [height]") 152 | return true 153 | end 154 | 155 | -- Retrieve the blocktypes from the params: 156 | local BlockTable, ErrBlock = GetBlockDst(a_Split[2], a_Player) 157 | if not(BlockTable) then 158 | a_Player:SendMessage(cChatColor.LightPurple .. "Unknown block type: '" .. ErrBlock .. "'.") 159 | return true 160 | end 161 | 162 | local RadiusX, RadiusZ 163 | local Radius = tonumber(a_Split[3]) 164 | if (Radius) then 165 | -- Same radius for all axis 166 | RadiusX, RadiusZ = Radius, Radius, Radius 167 | else 168 | -- The player might want to specify the radius for each axis. 169 | local Radius = StringSplit(a_Split[3], ",") 170 | if (#Radius == 1) then 171 | a_Player:SendMessage(cChatColor.Rose .. "Number expected; string \"" .. a_Split[3] .. "\" given.") 172 | return true 173 | end 174 | 175 | if (#Radius ~= 2) then 176 | a_Player:SendMessage(cChatColor.Rose .. "You must specify 1 or 2 radius values") 177 | return true 178 | end 179 | 180 | -- Check if the radius for all axis are numbers 181 | for Idx = 1, 2 do 182 | if (not tonumber(Radius[Idx])) then 183 | a_Player:SendMessage(cChatColor.Rose .. "Number expected; string \"" .. Radius[Idx] .. "\" given.") 184 | return true 185 | end 186 | end 187 | 188 | RadiusX, RadiusZ = tonumber(Radius[1]) + 1, tonumber(Radius[2]) + 1 189 | end 190 | 191 | local Height = tonumber(a_Split[4] or 1) - 1 192 | local Pos = a_Player:GetPosition():Floor() 193 | 194 | local Cuboid = cCuboid(Pos, Pos) 195 | Cuboid:Expand(RadiusX, RadiusX, 0, Height, RadiusZ, RadiusZ) 196 | Cuboid:Sort() 197 | 198 | -- Create the sphere in the world 199 | local NumAffectedBlocks = CreateCylinderInCuboid(a_Player, Cuboid, BlockTable, a_Split[1] == "//hcyl") 200 | 201 | -- Send a message to the player with the amount of affected blocks 202 | a_Player:SendMessage(cChatColor.LightPurple .. NumAffectedBlocks .. " block(s) have been created.") 203 | return true 204 | end 205 | 206 | 207 | 208 | 209 | 210 | function HandleSphereCommand(a_Split, a_Player) 211 | -- //sphere 212 | -- //hsphere 213 | 214 | if ((a_Split[2] == nil) or (a_Split[3] == nil)) then 215 | a_Player:SendMessage(cChatColor.Rose .. "Too few arguments.") 216 | a_Player:SendMessage(cChatColor.Rose .. a_Split[1] .. " [,,]") 217 | return true 218 | end 219 | 220 | -- Retrieve the blocktypes from the params: 221 | local BlockTable, ErrBlock = GetBlockDst(a_Split[2], a_Player) 222 | if not(BlockTable) then 223 | a_Player:SendMessage(cChatColor.LightPurple .. "Unknown block type: '" .. ErrBlock .. "'.") 224 | return true 225 | end 226 | 227 | local RadiusX, RadiusY, RadiusZ 228 | local Radius = tonumber(a_Split[3]) 229 | if (Radius) then 230 | -- Same radius for all axis 231 | RadiusX, RadiusY, RadiusZ = Radius, Radius, Radius 232 | else 233 | -- The player might want to specify the radius for each axis. 234 | local Radius = StringSplit(a_Split[3], ",") 235 | if (#Radius == 1) then 236 | a_Player:SendMessage(cChatColor.Rose .. "Number expected; string \"" .. a_Split[3] .. "\" given.") 237 | return true 238 | end 239 | 240 | if (#Radius ~= 3) then 241 | a_Player:SendMessage(cChatColor.Rose .. "You must specify 1 or 3 radius values") 242 | return true 243 | end 244 | 245 | -- Check if the radius for all axis are numbers 246 | for Idx = 1, 3 do 247 | if (not tonumber(Radius[Idx])) then 248 | a_Player:SendMessage(cChatColor.Rose .. "Number expected; string \"" .. Radius[Idx] .. "\" given.") 249 | return true 250 | end 251 | end 252 | 253 | RadiusX, RadiusY, RadiusZ = tonumber(Radius[1]) + 1, tonumber(Radius[2]) + 1, tonumber(Radius[3]) + 1 254 | end 255 | 256 | local Pos = a_Player:GetPosition():Floor() 257 | 258 | local Cuboid = cCuboid(Pos, Pos) 259 | Cuboid:Expand(RadiusX, RadiusX, RadiusY, RadiusY, RadiusZ, RadiusZ) 260 | Cuboid:Sort() 261 | 262 | -- Create the sphere in the world 263 | local NumAffectedBlocks = CreateSphereInCuboid(a_Player, Cuboid, BlockTable, a_Split[1] == "//hsphere") 264 | 265 | -- Send a message to the player with the amount of affected blocks 266 | a_Player:SendMessage(cChatColor.LightPurple .. NumAffectedBlocks .. " block(s) have been created.") 267 | return true 268 | end 269 | 270 | 271 | 272 | 273 | 274 | function HandlePyramidCommand(a_Split, a_Player) 275 | -- //pyramid 276 | -- //hpyramid 277 | 278 | if ((a_Split[2] == nil) or (a_Split[3] == nil)) then 279 | a_Player:SendMessage(cChatColor.Rose .. "Too few arguments.") 280 | a_Player:SendMessage(cChatColor.Rose .. a_Split[1] .. " [,,]") 281 | return true 282 | end 283 | 284 | -- Retrieve the blocktypes from the params: 285 | local BlockTable, ErrBlock = GetBlockDst(a_Split[2], a_Player) 286 | if not(BlockTable) then 287 | a_Player:SendMessage(cChatColor.LightPurple .. "Unknown block type: '" .. ErrBlock .. "'.") 288 | return true 289 | end 290 | 291 | local RadiusX, RadiusY, RadiusZ 292 | local Radius = tonumber(a_Split[3]) 293 | if (Radius) then 294 | -- Same size for all axis 295 | RadiusX, RadiusY, RadiusZ = Radius, Radius, Radius 296 | else 297 | -- The player might want to specify the size for each axis. 298 | local Radius = StringSplit(a_Split[3], ",") 299 | if (#Radius == 1) then 300 | a_Player:SendMessage(cChatColor.Rose .. "Number expected; string \"" .. a_Split[3] .. "\" given.") 301 | return true 302 | end 303 | 304 | if (#Radius ~= 3) then 305 | a_Player:SendMessage(cChatColor.Rose .. "You must specify 1 or 3 size values") 306 | return true 307 | end 308 | 309 | -- Check if the size for all axis are numbers 310 | for Idx = 1, 3 do 311 | if (not tonumber(Radius[Idx])) then 312 | a_Player:SendMessage(cChatColor.Rose .. "Number expected; string \"" .. Radius[Idx] .. "\" given.") 313 | return true 314 | end 315 | end 316 | 317 | RadiusX, RadiusY, RadiusZ = tonumber(Radius[1]) + 1, tonumber(Radius[2]) + 1, tonumber(Radius[3]) + 1 318 | end 319 | 320 | -- Get the position of the player as a Vector3i 321 | local Pos = a_Player:GetPosition():Floor() 322 | 323 | -- Create a cuboid with the points set to the player's position 324 | local Cuboid = cCuboid(Pos, Pos) 325 | Cuboid:Expand(RadiusX, RadiusX, 0, RadiusY, RadiusZ, RadiusZ) 326 | Cuboid:ClampY(0, 255) 327 | Cuboid:Sort() 328 | 329 | local World = a_Player:GetWorld() 330 | 331 | -- Check other plugins 332 | if (CallHook("OnAreaChanging", Cuboid, a_Player, World, a_Split[1]:sub(3, -1))) then 333 | return true 334 | end 335 | 336 | -- Push the area into an undo stack: 337 | local State = GetPlayerState(a_Player) 338 | State.UndoStack:PushUndoFromCuboid(World, Cuboid) 339 | 340 | local BlockArea = cBlockArea() 341 | 342 | -- Read the affected area from the world. 343 | BlockArea:Read(World, Cuboid) 344 | 345 | -- Get the mask for the equipped item 346 | local Mask = State.ToolRegistrator:GetMask(a_Player:GetEquippedItem().m_ItemType) 347 | 348 | -- Create the pyramid in the blockarea. 349 | local AffectedBlocks = cShapeGenerator.MakePyramid(BlockArea, BlockTable, a_Split[1] == "//hpyramid", Mask); 350 | 351 | -- Write the changes into the world 352 | BlockArea:Write(World, Cuboid.p1) 353 | 354 | -- Notify other plugins of the (h)pyramid 355 | CallHook("OnAreaChanged", Cuboid, a_Player, World, a_Split[1]:sub(3, -1)) 356 | 357 | -- Send a message to the player with the amount of changed blocks 358 | a_Player:SendMessage(cChatColor.LightPurple .. AffectedBlocks .. " block(s) have been created.") 359 | 360 | return true 361 | end 362 | -------------------------------------------------------------------------------- /Commands/History.lua: -------------------------------------------------------------------------------- 1 | 2 | -- History.lua 3 | 4 | -- Contains all the command handlers in the History category 5 | 6 | 7 | 8 | 9 | 10 | function HandleUndoCommand(a_Split, a_Player) 11 | -- //undo 12 | 13 | local State = GetPlayerState(a_Player) 14 | local IsSuccess, Msg = State.UndoStack:Undo(a_Player:GetWorld()) 15 | if (IsSuccess) then 16 | a_Player:SendMessage(cChatColor.LightPurple .. "Undo Successful.") 17 | else 18 | a_Player:SendMessage(cChatColor.Rose .. "Cannot undo: " .. (Msg or "")) 19 | end 20 | return true 21 | end 22 | 23 | 24 | 25 | 26 | 27 | function HandleRedoCommand(a_Split, a_Player) 28 | -- //redo 29 | 30 | local State = GetPlayerState(a_Player) 31 | local IsSuccess, Msg = State.UndoStack:Redo(a_Player:GetWorld()) 32 | if (IsSuccess) then 33 | a_Player:SendMessage(cChatColor.LightPurple .. "Redo Successful.") 34 | else 35 | a_Player:SendMessage(cChatColor.Rose .. "Cannot redo: " .. (Msg or "")) 36 | end 37 | return true 38 | end 39 | -------------------------------------------------------------------------------- /Commands/Navigation.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Navigation.lua 3 | 4 | -- Contains all the command handlers in the navigation category 5 | 6 | 7 | 8 | 9 | 10 | function HandleUpCommand(a_Split, a_Player) 11 | -- /up 12 | 13 | if #a_Split < 2 then 14 | a_Player:SendMessage(cChatColor.Rose .. "Too few arguments.") 15 | a_Player:SendMessage(cChatColor.Rose .. "/up ") 16 | return true 17 | elseif #a_Split > 2 then 18 | a_Player:SendMessage(cChatColor.Rose .. "Too many arguments.") 19 | a_Player:SendMessage(cChatColor.Rose .. "/up ") 20 | return true 21 | end 22 | 23 | local Height = tonumber(a_Split[2]) 24 | if (Height == nil) then -- The given string isn't a number. bail out. 25 | a_Player:SendMessage(cChatColor.Rose .. 'Number expected; string"' .. a_Split[2] .. '" given.') 26 | return true 27 | end 28 | 29 | local P1 = a_Player:GetPosition():Floor() 30 | local P2 = a_Player:GetPosition():Floor() 31 | P2.y = P2.y + Height 32 | 33 | local World = a_Player:GetWorld() 34 | for Y = P1.y, P2.y + 1 do 35 | if (World:GetBlock(Vector3i(P1.x, Y, P1.z)) ~= E_BLOCK_AIR) then 36 | a_Player:SendMessage(cChatColor.Rose .. "You would hit something above you.") 37 | return true 38 | end 39 | end 40 | 41 | local ChangeCuboid = cCuboid(P2, P2) 42 | if (CallHook("OnAreaChanging", ChangeCuboid, a_Player, World, "up")) then 43 | return true 44 | end 45 | 46 | World:SetBlock(Vector3i(P1.x, P2.y - 1, P1.z), E_BLOCK_GLASS, 0) 47 | a_Player:TeleportToCoords(P1.x + 0.5, P2.y, P1.z + 0.5) 48 | a_Player:SendMessage(cChatColor.LightPurple .. "Whoosh!") 49 | 50 | CallHook("OnAreaChanged", ChangeCuboid, a_Player, World, "up") 51 | return true 52 | end 53 | 54 | 55 | 56 | 57 | 58 | function HandleJumpToCommand(a_Split, a_Player) 59 | -- /jumpto 60 | -- /j 61 | 62 | if (#a_Split ~= 1) then 63 | a_Player:SendMessage(cChatColor.Rose .. "Too many arguments.") 64 | a_Player:SendMessage(cChatColor.Rose .. "/jumpto") 65 | return true 66 | end 67 | 68 | LeftClickCompass(a_Player, a_Player:GetWorld()) 69 | a_Player:SendMessage(cChatColor.LightPurple .. "Poof!!") 70 | return true 71 | end 72 | 73 | 74 | 75 | 76 | 77 | function HandleThruCommand(Split, a_Player) 78 | -- /thru 79 | 80 | if (#Split ~= 1) then 81 | a_Player:SendMessage(cChatColor.Rose .. "Too many arguments.") 82 | a_Player:SendMessage(cChatColor.Rose .. "/thru") 83 | return true 84 | end 85 | 86 | RightClickCompass(a_Player, a_Player:GetWorld()) 87 | a_Player:SendMessage(cChatColor.LightPurple .. "Whoosh!") 88 | return true 89 | end 90 | 91 | 92 | 93 | 94 | 95 | function HandleDescendCommand(a_Split, a_Player) 96 | -- /descend 97 | -- /desc 98 | 99 | local World = a_Player:GetWorld() 100 | if a_Player:GetPosY() < 1 then 101 | a_Player:SendMessage(cChatColor.LightPurple .. "Descended a level.") 102 | return true 103 | end 104 | 105 | local FoundYCoordinate = false 106 | local WentThroughBlock = false 107 | local XPos = math.floor(a_Player:GetPosX()) 108 | local YPos = a_Player:GetPosY() 109 | local ZPos = math.floor(a_Player:GetPosZ()) 110 | 111 | for Y = math.floor(YPos), 1, -1 do 112 | if (World:GetBlock(Vector3i(XPos, Y, ZPos)) ~= E_BLOCK_AIR) then 113 | WentThroughBlock = true 114 | else 115 | if WentThroughBlock then 116 | for y = Y, 1, -1 do 117 | if cBlockInfo:IsSolid(World:GetBlock(Vector3i(XPos, y, ZPos))) then 118 | YPos = y 119 | FoundYCoordinate = true 120 | break 121 | end 122 | end 123 | 124 | if FoundYCoordinate then 125 | break 126 | end 127 | end 128 | end 129 | end 130 | 131 | if FoundYCoordinate then 132 | a_Player:TeleportToCoords(a_Player:GetPosX(), YPos + 1, a_Player:GetPosZ()) 133 | end 134 | 135 | a_Player:SendMessage(cChatColor.LightPurple .. "Descended a level.") 136 | return true 137 | end 138 | 139 | 140 | 141 | 142 | 143 | function HandleAscendCommand(a_Split, a_Player) 144 | -- /ascend 145 | -- /asc 146 | 147 | local World = a_Player:GetWorld() 148 | local XPos = math.floor(a_Player:GetPosX()) 149 | local YPos = a_Player:GetPosY() 150 | local ZPos = math.floor(a_Player:GetPosZ()) 151 | 152 | local IsValid, WorldHeight = World:TryGetHeight(XPos, ZPos) 153 | 154 | if not IsValid then 155 | a_Player:SendMessage(cChatColor.LightPurple .. "Ascended a level.") 156 | return true 157 | end 158 | 159 | if a_Player:GetPosY() == WorldHeight then 160 | a_Player:SendMessage(cChatColor.LightPurple .. "Ascended a level.") 161 | return true 162 | end 163 | 164 | 165 | local WentThroughBlock = false 166 | 167 | for Y = math.floor(a_Player:GetPosY()), WorldHeight + 1 do 168 | if (World:GetBlock(Vector3i(XPos, Y, ZPos)) == E_BLOCK_AIR) then 169 | if WentThroughBlock then 170 | YPos = Y 171 | break 172 | end 173 | else 174 | WentThroughBlock = true 175 | end 176 | end 177 | 178 | if WentThroughBlock then 179 | a_Player:TeleportToCoords(a_Player:GetPosX(), YPos, a_Player:GetPosZ()) 180 | end 181 | 182 | a_Player:SendMessage(cChatColor.LightPurple .. "Ascended a level.") 183 | return true 184 | end 185 | 186 | 187 | 188 | 189 | 190 | function HandleCeilCommand(a_Split, a_Player) 191 | -- ceil 192 | 193 | if (#a_Split > 2) then 194 | a_Player:SendMessage(cChatColor.Rose .. "Too many arguments.") 195 | a_Player:SendMessage(cChatColor.Rose .. "/ceil [cleurance]") 196 | return true 197 | end 198 | 199 | local BlockFromCeil 200 | if a_Split[2] == nil then 201 | BlockFromCeil = 0 202 | else 203 | BlockFromCeil = tonumber(a_Split[2]) 204 | end 205 | 206 | if BlockFromCeil == nil then 207 | a_Player:SendMessage(cChatColor.Rose .. 'Number expected; string "' .. a_Split[2] .. '" given.') 208 | return true 209 | end 210 | local World = a_Player:GetWorld() 211 | local X = math.floor(a_Player:GetPosX()) 212 | local Y = math.floor(a_Player:GetPosY()) 213 | local Z = math.floor(a_Player:GetPosZ()) 214 | local IsValid, WorldHeight = World:TryGetHeight(X, Z) 215 | 216 | if not IsValid then 217 | a_Player:SendMessage(cChatColor.LightPurple .. "Whoosh!") 218 | return true 219 | end 220 | 221 | if Y >= WorldHeight + 1 then 222 | a_Player:SendMessage(cChatColor.Rose .. "No free spot above you found.") 223 | return true 224 | end 225 | 226 | for y = Y, WorldHeight do 227 | if (World:GetBlock(Vector3i(X, y, Z)) ~= E_BLOCK_AIR) then 228 | -- Check with other plugins if the operation is okay: 229 | if not(CallHook("OnAreaChanging", cCuboid(X, y - BlockFromCeil - 3, Z), a_Player, a_Player:GetWorld(), "ceil")) then 230 | World:SetBlock(Vector3i(X, y - BlockFromCeil - 3, Z), E_BLOCK_GLASS, 0) 231 | CallHook("OnAreaChanged", cCuboid(X, y - BlockFromCeil - 3, Z), a_Player, a_Player:GetWorld(), "ceil") 232 | end 233 | local I = y - BlockFromCeil - 2 234 | if I == Y then 235 | a_Player:SendMessage(cChatColor.Rose .. "No free spot above you found.") 236 | return true 237 | end 238 | a_Player:TeleportToCoords(X + 0.5, I, Z + 0.5) 239 | break 240 | end 241 | end 242 | 243 | a_Player:SendMessage(cChatColor.LightPurple .. "Whoosh!") 244 | return true 245 | end 246 | -------------------------------------------------------------------------------- /Commands/Schematic.lua: -------------------------------------------------------------------------------- 1 | 2 | -- cmd_schematic.lua 3 | 4 | -- Command handlers for the "//schematic" subcommands 5 | 6 | 7 | 8 | 9 | function HandleSchematicFormatsCommand(a_Split, a_Player) 10 | -- //schematic listformats 11 | 12 | -- We support only one format, MCEdit: 13 | a_Player:SendMessage(cChatColor.LightPurple .. 'Available formats: "MCEdit", "Cubeset"') 14 | return true 15 | end 16 | 17 | 18 | 19 | 20 | 21 | function HandleSchematicListCommand(a_Split, a_Player) 22 | -- //schematic list 23 | 24 | local State = GetPlayerState(a_Player); 25 | local FileList = State.ClipboardStorage:ListFiles(); 26 | 27 | a_Player:SendMessage(cChatColor.LightPurple .. "Available schematics: " .. table.concat(FileList, ", ")) 28 | return true 29 | end 30 | 31 | 32 | 33 | 34 | 35 | function HandleSchematicLoadCommand(a_Split, a_Player) 36 | -- //schematic load [options] 37 | 38 | -- Check the FileName parameter: 39 | if (#a_Split < 3) then 40 | a_Player:SendMessage(cChatColor.Rose .. "Usage: /schematic load [options]") 41 | return true 42 | end 43 | local FileName = a_Split[3] 44 | local Options = {unpack(a_Split, 4)} 45 | 46 | -- Load the file into clipboard: 47 | local State = GetPlayerState(a_Player) 48 | local success, err = State.ClipboardStorage:Load(FileName, Options); 49 | if (success) then 50 | a_Player:SendMessage(cChatColor.LightPurple .. FileName .. " schematic was loaded into your clipboard.") 51 | a_Player:SendMessage(cChatColor.LightPurple .. "Clipboard size: " .. State.Clipboard:GetSizeDesc()) 52 | else 53 | a_Player:SendMessage(cChatColor.Rose .. err) 54 | end 55 | return true 56 | end 57 | 58 | 59 | 60 | 61 | 62 | function HandleSchematicSaveCommand(a_Split, a_Player) 63 | -- //schematic save [] 64 | 65 | -- Get the parameters from the command arguments: 66 | local FileName 67 | local Format = "mcedit" 68 | local Options = {} 69 | -- ToDo: Currently it's not possible to have additional options for the mcedit format. 70 | if (#a_Split >= 4) then 71 | Format = a_Split[3] 72 | FileName = a_Split[4] 73 | Options = {unpack(a_Split, 5)} 74 | elseif (#a_Split == 3) then 75 | FileName = a_Split[3] 76 | else 77 | a_Player:SendMessage(cChatColor.Rose .. "Usage: //schematic save [format] [options]") 78 | return true 79 | end 80 | 81 | -- Check that there's data in the clipboard: 82 | local State = GetPlayerState(a_Player) 83 | if not(State.Clipboard:IsValid()) then 84 | a_Player:SendMessage(cChatColor.Rose .. "There's no data in the clipboard. Use //copy or //cut first.") 85 | return true 86 | end 87 | 88 | -- Save the clipboard: 89 | local success, err = State.ClipboardStorage:Save(FileName, Format, Options); 90 | if (success) then 91 | a_Player:SendMessage(cChatColor.LightPurple .. "Clipboard saved to " .. FileName .. ".") 92 | else 93 | a_Player:SendMessage(cChatColor.Rose .. err) 94 | end 95 | return true 96 | end 97 | -------------------------------------------------------------------------------- /Commands/Scripting.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Scripting.lua 3 | 4 | -- Contains command handlers that are in the Scripting category. 5 | 6 | 7 | 8 | 9 | 10 | -- Loads and executes a craftscript 11 | function HandleCraftScriptCommand(a_Split, a_Player) 12 | -- /cs 13 | 14 | local PlayerState = GetPlayerState(a_Player) 15 | 16 | if (not a_Split[2]) then 17 | a_Player:SendMessage(cChatColor.Rose .. "Usage: /cs ") 18 | return true 19 | end 20 | 21 | local Succes, Err = PlayerState.CraftScript:SelectScript(a_Split[2]) 22 | if (not Succes) then 23 | a_Player:SendMessage(cChatColor.Rose .. Err) 24 | return true 25 | end 26 | 27 | local Arguments = a_Split 28 | table.remove(Arguments, 1); table.remove(Arguments, 1) 29 | 30 | local Succes, Err = PlayerState.CraftScript:Execute(a_Player, Arguments) 31 | if (not Succes) then 32 | a_Player:SendMessage(cChatColor.Rose .. Err) 33 | return true 34 | end 35 | 36 | a_Player:SendMessage(cChatColor.LightPurple .. "Script executed.") 37 | return true 38 | end 39 | 40 | 41 | 42 | 43 | 44 | -- Executes the last used craftscript. 45 | function HandleLastCraftScriptCommand(a_Split, a_Player) 46 | -- /.s 47 | 48 | local PlayerState = GetPlayerState(a_Player) 49 | 50 | local Arguments = a_Split 51 | table.remove(Arguments, 1) 52 | 53 | local Succes, Err = PlayerState.CraftScript:Execute(a_Player, Arguments) 54 | if (not Succes) then 55 | a_Player:SendMessage(cChatColor.Rose .. Err) 56 | return true 57 | end 58 | 59 | a_Player:SendMessage(cChatColor.Rose .. "Script Executed") 60 | return true 61 | end 62 | -------------------------------------------------------------------------------- /Commands/Selection.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Selection.lua 3 | 4 | -- Implements handlers for the selection-related commands 5 | 6 | 7 | 8 | 9 | 10 | function HandleChunkCommand(a_Split, a_Player) 11 | -- //chunk 12 | 13 | -- Find the chunk boundaries. 14 | local ChunkX = a_Player:GetChunkX() 15 | local ChunkZ = a_Player:GetChunkZ() 16 | local MinX = ChunkX * 16 17 | local MinZ = ChunkZ * 16 18 | local MaxX = MinX + 15 19 | local MaxZ = MinZ + 15 20 | 21 | -- Update selection. 22 | local State = GetPlayerState(a_Player) 23 | State.Selection:SetFirstPoint(MinX, 0, MinZ) 24 | State.Selection:SetSecondPoint(MaxX, 255, MaxZ) 25 | 26 | -- Notify the player about the selection. 27 | State.Selection:NotifySelectionChanged() 28 | a_Player:SendMessage(cChatColor.LightPurple .. "Chunk selected: " .. ChunkX .. ", " .. ChunkZ) 29 | 30 | return true 31 | end 32 | 33 | 34 | 35 | 36 | 37 | function HandleCountCommand(a_Split, a_Player) 38 | -- //count 39 | 40 | local State = GetPlayerState(a_Player) 41 | 42 | -- Check the selection: 43 | if not(State.Selection:IsValid()) then 44 | a_Player:SendMessage(cChatColor.Rose .. "No region set") 45 | return true 46 | end 47 | 48 | -- Check the params: 49 | if (a_Split[2] == nil) then 50 | a_Player:SendMessage(cChatColor.Rose .. "Usage: //count ") 51 | return true 52 | end 53 | 54 | -- Retrieve the blocktypes from the params: 55 | local Mask, ErrBlock = cMask:new(a_Split[2]) 56 | if not(Mask) then 57 | a_Player:SendMessage(cChatColor.Rose .. "Unknown block type: '" .. ErrBlock .. "'.") 58 | return true 59 | end 60 | 61 | -- Count the blocks: 62 | local NumBlocks = CountBlocksInCuboid(a_Player:GetWorld(), State.Selection:GetSortedCuboid(), Mask) 63 | 64 | a_Player:SendMessage(cChatColor.LightPurple .. "Counted: " .. NumBlocks) 65 | return true 66 | end 67 | 68 | 69 | 70 | 71 | 72 | function HandleDeselectCommand(a_Split, a_Player) 73 | -- //desel 74 | 75 | local State = GetPlayerState(a_Player) 76 | State.Selection:Deselect() 77 | 78 | a_Player:SendMessage(cChatColor.LightPurple .. "Selection cleared.") 79 | return true 80 | end 81 | 82 | 83 | 84 | 85 | 86 | function HandleDistrCommand(a_Split, a_Player) 87 | -- //distr 88 | 89 | -- TODO: -d option that separates data values. 90 | 91 | -- Check the selection: 92 | local State = GetPlayerState(a_Player) 93 | if not(State.Selection:IsValid()) then 94 | a_Player:SendMessage(cChatColor.Rose .. "No selection set") 95 | return true 96 | end 97 | 98 | -- Get selection information. 99 | local World = a_Player:GetWorld() 100 | local Area = cBlockArea() 101 | Area:Read(World, State.Selection:GetSortedCuboid()) 102 | local SizeX, SizeY, SizeZ = Area:GetCoordRange() 103 | 104 | -- Count the blocks. 105 | local TotalCount = Area:GetVolume() 106 | local BlockCounts = {} 107 | 108 | for X = 0, SizeX do 109 | for Y = 0, SizeY do 110 | for Z = 0, SizeZ do 111 | local BlockType = Area:GetRelBlockType(X, Y, Z) 112 | BlockCounts[BlockType] = (BlockCounts[BlockType] or 0) + 1 113 | end 114 | end 115 | end 116 | 117 | -- Generate the output. 118 | -- Sort records by count. 119 | local SortedBlockCounts, Index = {}, 1 120 | for BlockType, BlockCount in pairs(BlockCounts) do 121 | SortedBlockCounts[Index] = {Type = BlockType, Count = BlockCount} 122 | Index = Index + 1 123 | end 124 | table.sort(SortedBlockCounts, function(Block1, Block2) return Block1.Count < Block2.Count end) 125 | 126 | -- Display them. 127 | a_Player:SendMessage(cChatColor.LightPurple .. "# total blocks: " .. TotalCount) 128 | for _, Block in ipairs(SortedBlockCounts) do 129 | local BlockName = ItemTypeToString(Block.Type) 130 | local Perc = 100 * Block.Count / TotalCount 131 | local Line = string.format("% 7d (%.3f%%) %s #%d", Block.Count, Perc, BlockName, Block.Type) 132 | a_Player:SendMessage(cChatColor.LightPurple .. Line) 133 | end 134 | 135 | return true 136 | end 137 | 138 | 139 | 140 | 141 | 142 | function HandleExpandContractCommand(a_Split, a_Player) 143 | -- //expand [Amount] [Direction] 144 | -- //contract [Amount] [Direction] 145 | 146 | -- Check the selection: 147 | local State = GetPlayerState(a_Player) 148 | if not(State.Selection:IsValid()) then 149 | a_Player:SendMessage(cChatColor.Rose .. "No region set") 150 | return true 151 | end 152 | 153 | if ((a_Split[1] == "//expand") and (a_Split[2] == "vert")) then 154 | State.Selection.Cuboid.p1.y = 0 155 | State.Selection.Cuboid.p2.y = 255 156 | State.Selection:NotifySelectionChanged() 157 | 158 | a_Player:SendMessage(cChatColor.LightPurple .. "Expanded the selection from top to bottom.") 159 | a_Player:SendMessage(cChatColor.LightPurple .. "Selection is now " .. State.Selection:GetSizeDesc()) 160 | return true 161 | end 162 | 163 | if (a_Split[2] ~= nil) and (tonumber(a_Split[2]) == nil) then 164 | a_Player:SendMessage(cChatColor.Rose .. "Usage: " .. a_Split[1] .. " [Blocks] [Direction]") 165 | return true 166 | end 167 | 168 | local NumBlocks = a_Split[2] or 1 -- Use the given amount or 1 if nil 169 | local Direction = string.lower(a_Split[3] or ((a_Player:GetPitch() > 70) and "down") or ((a_Player:GetPitch() < -70) and "up") or "forward") 170 | local SubMinX, SubMinY, SubMinZ, AddMaxX, AddMaxY, AddMaxZ = 0, 0, 0, 0, 0, 0 171 | local LookDirection = math.round((a_Player:GetYaw() + 180) / 90) 172 | 173 | if ((Direction == "up") or (Direction == "u")) then 174 | AddMaxY = NumBlocks 175 | elseif ((Direction == "down") or (Direction == "d")) then 176 | SubMinY = NumBlocks 177 | elseif (Direction == "left") then 178 | if (LookDirection == E_DIRECTION_SOUTH) then 179 | AddMaxX = NumBlocks 180 | elseif (LookDirection == E_DIRECTION_EAST) then 181 | SubMinZ = NumBlocks 182 | elseif (LookDirection == E_DIRECTION_NORTH1) or (LookDirection == E_DIRECTION_NORTH2) then 183 | SubMinX = NumBlocks 184 | elseif (LookDirection == E_DIRECTION_WEST) then 185 | AddMaxZ = NumBlocks 186 | end 187 | elseif (Direction == "right") then 188 | if (LookDirection == E_DIRECTION_SOUTH) then 189 | SubMinX = NumBlocks 190 | elseif (LookDirection == E_DIRECTION_EAST) then 191 | AddMaxZ = NumBlocks 192 | elseif (LookDirection == E_DIRECTION_NORTH1) or (LookDirection == E_DIRECTION_NORTH2) then 193 | AddMaxX = NumBlocks 194 | elseif (LookDirection == E_DIRECTION_WEST) then 195 | SubMinZ = NumBlocks 196 | end 197 | elseif (Direction == "south") then 198 | AddMaxZ = NumBlocks 199 | elseif (Direction == "east") then 200 | AddMaxX = NumBlocks 201 | elseif (Direction == "north") then 202 | SubMinZ = NumBlocks 203 | elseif (Direction == "west") then 204 | SubMinX = NumBlocks 205 | elseif ((Direction == "forward") or (Direction == "me")) then 206 | if (LookDirection == E_DIRECTION_SOUTH) then 207 | AddMaxZ = NumBlocks 208 | elseif (LookDirection == E_DIRECTION_EAST) then 209 | AddMaxX = NumBlocks 210 | elseif ((LookDirection == E_DIRECTION_NORTH1) or (LookDirection == E_DIRECTION_NORTH2)) then 211 | SubMinZ = NumBlocks 212 | elseif (LookDirection == E_DIRECTION_WEST) then 213 | SubMinX = NumBlocks 214 | end 215 | elseif ((Direction == "backwards") or (Direction == "back")) then 216 | if (LookDirection == E_DIRECTION_SOUTH) then 217 | SubMinZ = NumBlocks 218 | elseif (LookDirection == E_DIRECTION_EAST) then 219 | SubMinX = NumBlocks 220 | elseif ((LookDirection == E_DIRECTION_NORTH1) or (LookDirection == E_DIRECTION_NORTH2)) then 221 | AddMaxZ = NumBlocks 222 | elseif (LookDirection == E_DIRECTION_WEST) then 223 | AddMaxX = NumBlocks 224 | end 225 | elseif (Direction == "walls") then 226 | AddMaxX = NumBlocks 227 | AddMaxZ = NumBlocks 228 | SubMinX = NumBlocks 229 | SubMinZ = NumBlocks 230 | elseif ((Direction == "all") or (Direction == "faces")) then 231 | AddMaxX = NumBlocks 232 | AddMaxY = NumBlocks 233 | AddMaxZ = NumBlocks 234 | SubMinX = NumBlocks 235 | SubMinY = NumBlocks 236 | SubMinZ = NumBlocks 237 | else 238 | a_Player:SendMessage(cChatColor.Rose .. "Unknown direction \"" .. Direction .. "\".") 239 | return true 240 | end 241 | 242 | if (a_Split[1] == "//contract") then 243 | SubMinX, AddMaxX = -AddMaxX, -SubMinX 244 | SubMinY, AddMaxY = -AddMaxY, -SubMinY 245 | SubMinZ, AddMaxZ = -AddMaxZ, -SubMinZ 246 | end 247 | 248 | -- Expand or contract the region 249 | State.Selection:Expand(SubMinX, SubMinY, SubMinZ, AddMaxX, AddMaxY, AddMaxZ) 250 | a_Player:SendMessage(cChatColor.LightPurple .. a_Split[1]:sub(3, -1):ucfirst() .. "ed the selection.") 251 | a_Player:SendMessage(cChatColor.LightPurple .. "Selection is now " .. State.Selection:GetSizeDesc()) 252 | return true 253 | end 254 | 255 | 256 | 257 | 258 | 259 | function HandleHPosCommand(a_Split, a_Player) 260 | -- //hpos1 261 | -- //hpos2 262 | 263 | -- Get the block the player is looking at 264 | local TargetBlock, BlockFace = GetTargetBlock(a_Player) 265 | if (not TargetBlock) then 266 | return true 267 | end 268 | 269 | -- Determine the name of the point. If the command is //pos1 then "First", otherwise it's the second point 270 | local PointName = (a_Split[1] == "//hpos1") and "First" or "Second" 271 | 272 | local State = GetPlayerState(a_Player) 273 | 274 | -- Select the block: 275 | local Succes, Msg = State.Selection:SetPos(TargetBlock.x, TargetBlock.y, TargetBlock.z, BlockFace, PointName) 276 | a_Player:SendMessage(Msg) 277 | return true 278 | end 279 | 280 | 281 | 282 | 283 | 284 | function HandlePosCommand(a_Split, a_Player) 285 | -- //pos1 286 | -- //pos2 287 | 288 | -- Determine the name of the point. If the command is //pos1 then "First", otherwise it's the second point 289 | local PointName = (a_Split[1] == "//pos1") and "First" or "Second" 290 | local State = GetPlayerState(a_Player) 291 | local Pos = a_Player:GetPosition():Floor() 292 | local Succes, Msg = State.Selection:SetPos(Pos.x, Pos.y, Pos.z, BLOCK_FACE_TOP, PointName, true) 293 | 294 | -- We can assume that the action was a succes, since all the given parameters are known to be valid. 295 | a_Player:SendMessage(cChatColor.LightPurple .. Msg) 296 | return true 297 | end 298 | 299 | 300 | 301 | 302 | 303 | function HandleSaveLoadSelectionCommand(a_Split, a_Player) 304 | -- //savesel 305 | -- //loadsel 306 | 307 | if (not a_Split[2]) then 308 | a_Player:SendMessage(cChatColor.Rose .. "Usage: " .. a_Split[1] .. " ") 309 | return true 310 | end 311 | 312 | local State = GetPlayerState(a_Player) 313 | local SelectionName = table.concat(a_Split, " ", 2) 314 | 315 | local Success, ErrMsg 316 | if (a_Split[1] == "//savesel") then 317 | Success, ErrMsg = State.Selection:SaveSelection(SelectionName) 318 | else 319 | Success, ErrMsg = State.Selection:LoadSelection(SelectionName) 320 | end 321 | 322 | if (not Success) then 323 | a_Player:SendMessage(cChatColor.Rose .. ErrMsg) 324 | return true 325 | end 326 | 327 | a_Player:SendMessage(cChatColor.LightPurple .. "Selection " .. ((a_Split[1] == "//loadsel") and "loaded" or "saved")) 328 | return true 329 | end 330 | 331 | 332 | 333 | 334 | 335 | function HandleShiftCommand(a_Split, a_Player) 336 | -- //shift [Amount] [Direction] 337 | 338 | -- Check the selection: 339 | local State = GetPlayerState(a_Player) 340 | if not(State.Selection:IsValid()) then 341 | a_Player:SendMessage(cChatColor.Rose .. "No region set") 342 | return true 343 | end 344 | 345 | if (a_Split[2] ~= nil) and (tonumber(a_Split[2]) == nil) then 346 | a_Player:SendMessage(cChatColor.Rose .. "Usage: //shift [Blocks] [Direction]") 347 | return true 348 | end 349 | 350 | local NumBlocks = a_Split[2] or 1 -- Use the given amount or 1 if nil 351 | local Direction = string.lower(a_Split[3] or ((a_Player:GetPitch() > 70) and "down") or ((a_Player:GetPitch() < -70) and "up") or "forward") 352 | local X, Y, Z = 0, 0, 0 353 | local LookDirection = math.round((a_Player:GetYaw() + 180) / 90) 354 | 355 | if (Direction == "up") then 356 | Y = NumBlocks 357 | elseif (Direction == "down") then 358 | Y = -NumBlocks 359 | elseif (Direction == "left") then 360 | if (LookDirection == E_DIRECTION_SOUTH) then 361 | X = NumBlocks 362 | elseif (LookDirection == E_DIRECTION_EAST) then 363 | Z = -NumBlocks 364 | elseif (LookDirection == E_DIRECTION_NORTH1) or (LookDirection == E_DIRECTION_NORTH2) then 365 | X = -NumBlocks 366 | elseif (LookDirection == E_DIRECTION_WEST) then 367 | Z = NumBlocks 368 | end 369 | elseif (Direction == "right") then 370 | if (LookDirection == E_DIRECTION_SOUTH) then 371 | X = -NumBlocks 372 | elseif (LookDirection == E_DIRECTION_EAST) then 373 | Z = NumBlocks 374 | elseif (LookDirection == E_DIRECTION_NORTH1) or (LookDirection == E_DIRECTION_NORTH2) then 375 | X = NumBlocks 376 | elseif (LookDirection == E_DIRECTION_WEST) then 377 | Z = -NumBlocks 378 | end 379 | elseif (Direction == "south") then 380 | Z = NumBlocks 381 | elseif (Direction == "east") then 382 | X = NumBlocks 383 | elseif (Direction == "north") then 384 | Z = -NumBlocks 385 | elseif (Direction == "west") then 386 | X = -NumBlocks 387 | elseif ((Direction == "forward") or (Direction == "me")) then 388 | if (LookDirection == E_DIRECTION_SOUTH) then 389 | Z = NumBlocks 390 | elseif (LookDirection == E_DIRECTION_EAST) then 391 | X = NumBlocks 392 | elseif ((LookDirection == E_DIRECTION_NORTH1) or (LookDirection == E_DIRECTION_NORTH2)) then 393 | Z = -NumBlocks 394 | elseif (LookDirection == E_DIRECTION_WEST) then 395 | X = -NumBlocks 396 | end 397 | elseif ((Direction == "backwards") or (Direction == "back")) then 398 | if (LookDirection == E_DIRECTION_SOUTH) then 399 | Z = -NumBlocks 400 | elseif (LookDirection == E_DIRECTION_EAST) then 401 | X = -NumBlocks 402 | elseif ((LookDirection == E_DIRECTION_NORTH1) or (LookDirection == E_DIRECTION_NORTH2)) then 403 | Z = NumBlocks 404 | elseif (LookDirection == E_DIRECTION_WEST) then 405 | X = NumBlocks 406 | end 407 | else 408 | a_Player:SendMessage(cChatColor.Rose .. "Unknown direction \"" .. Direction .. "\".") 409 | return true 410 | end 411 | 412 | State.Selection:Move(X, Y, Z) 413 | a_Player:SendMessage(cChatColor.LightPurple .. "Region shifted.") 414 | return true 415 | end 416 | 417 | 418 | 419 | 420 | 421 | function HandleShrinkCommand(a_Split, a_Player) 422 | -- //shrink 423 | 424 | local State = GetPlayerState(a_Player) 425 | 426 | if not(State.Selection:IsValid()) then 427 | a_Player:SendMessage(cChatColor.Rose .. "No region set") 428 | return true 429 | end 430 | 431 | local SrcCuboid = State.Selection:GetSortedCuboid() 432 | local BlockArea = cBlockArea() 433 | BlockArea:Read(a_Player:GetWorld(), SrcCuboid) 434 | local MinRelX, MinRelY, MinRelZ, MaxRelX, MaxRelY, MaxRelZ = BlockArea:GetNonAirCropRelCoords() 435 | 436 | -- Set the new points. This will not take the previous points in account. (For example p1 and p2 could get switched) 437 | State.Selection:SetFirstPoint(SrcCuboid.p1.x + MinRelX, SrcCuboid.p1.y + MinRelY, SrcCuboid.p1.z + MinRelZ) 438 | State.Selection:SetSecondPoint(SrcCuboid.p1.x + MaxRelX, SrcCuboid.p1.y + MaxRelY, SrcCuboid.p1.z + MaxRelZ) 439 | 440 | -- Send the change of the selection to the client 441 | State.Selection:NotifySelectionChanged() 442 | 443 | a_Player:SendMessage(cChatColor.LightPurple .. "Region shrunk") 444 | return true 445 | end 446 | 447 | 448 | 449 | 450 | 451 | function HandleSizeCommand(a_Split, a_Player) 452 | -- //size 453 | 454 | local State = GetPlayerState(a_Player) 455 | if (not State.Selection:IsValid()) then 456 | a_Player:SendMessage(cChatColor.LightPurple .. "Please select a region first") 457 | return true 458 | end 459 | 460 | a_Player:SendMessage(cChatColor.LightPurple .. "The selection size is " .. State.Selection:GetSizeDesc() .. ".") 461 | return true 462 | end 463 | -------------------------------------------------------------------------------- /Commands/Special.lua: -------------------------------------------------------------------------------- 1 | 2 | -- cmd_Other.lua 3 | 4 | -- Has the commands that don't really fit in a category. 5 | 6 | 7 | 8 | 9 | 10 | -- Complete CUI handshake 11 | function HandleWorldEditCuiCommand(a_Split, a_Player) 12 | -- /we cui 13 | 14 | local State = GetPlayerState(a_Player) 15 | State.IsWECUIActivated = true 16 | State.Selection:NotifySelectionChanged() 17 | return true 18 | end 19 | 20 | 21 | 22 | 23 | 24 | -- Sends the version of the plugin. 25 | function HandleWorldEditVersionCommand(a_Split, a_Player) 26 | -- /we version 27 | 28 | a_Player:SendMessage(cChatColor.LightPurple .. "This is version " .. cPluginManager:GetCurrentPlugin():GetVersion()) 29 | return true 30 | end 31 | 32 | 33 | 34 | 35 | 36 | -- Sends all the available commands to the player. 37 | function HandleWorldEditHelpCommand(a_Split, a_Player) 38 | -- /we help 39 | 40 | if (not a_Player:HasPermission("worldedit.help")) then 41 | a_Player:SendMessage(cChatColor.Rose .. "You do not have permission for this command.") 42 | return true 43 | end 44 | 45 | local Commands = "" 46 | for Command, CommandInfo in pairs(g_PluginInfo.Commands) do 47 | if (a_Player:HasPermission(CommandInfo.Permission)) then 48 | Commands = Commands .. cChatColor.LightPurple .. Command .. ", " 49 | end 50 | end 51 | 52 | a_Player:SendMessage(cChatColor.LightPurple .. "Available commands:") 53 | a_Player:SendMessage(string.sub(Commands, 1, string.len(Commands) - 2)) -- Remove the last ", " 54 | return true 55 | end 56 | 57 | 58 | 59 | 60 | 61 | -- Gives the player the wand item. 62 | function HandleWandCommand(a_Split, a_Player) 63 | -- //wand 64 | 65 | local Item = cItem(g_Config.WandItem) -- create the cItem object 66 | if (a_Player:GetInventory():AddItem(Item)) then -- check if the player got the item 67 | a_Player:SendMessage(cChatColor.Green .. "You have received the wand.") 68 | else 69 | a_Player:SendMessage(cChatColor.Green .. "Not enough inventory space.") 70 | end 71 | return true 72 | end 73 | 74 | 75 | 76 | 77 | 78 | -- Toggles if the wand is active or not. 79 | function HandleToggleEditWandCommand(a_Split, a_Player) 80 | -- //togglewand 81 | 82 | local State = GetPlayerState(a_Player) 83 | if not(State.WandActivated) then 84 | State.WandActivated = true 85 | a_Player:SendMessage(cChatColor.LightPurple .. "Edit wand enabled.") 86 | else 87 | State.WandActivated = false 88 | a_Player:SendMessage(cChatColor.LightPurple .. "Edit wand disabled.") 89 | end 90 | return true 91 | end 92 | -------------------------------------------------------------------------------- /Commands/Tool.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | function HandleReplCommand(a_Split, a_Player) 7 | -- //repl 8 | 9 | if a_Split[2] == nil then -- check if the player gave a block id 10 | a_Player:SendMessage(cChatColor.Rose .. "Too few arguments.") 11 | a_Player:SendMessage(cChatColor.Rose .. "/repl ") 12 | return true 13 | end 14 | 15 | local BlockType, BlockMeta = GetBlockTypeMeta(a_Split[2]) 16 | 17 | if (not BlockType) then 18 | a_Player:SendMessage(cChatColor.Rose .. "Unknown character \"" .. a_Split[2] .. "\"") 19 | return true 20 | end 21 | 22 | if not IsValidBlock(BlockType) then -- check if the player gave a valid block id 23 | a_Player:SendMessage(cChatColor.Rose .. a_Split[2] .. " isn't a valid block") 24 | return true 25 | end 26 | 27 | -- Initialize the handler. 28 | local function ReplaceHandler(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 29 | if (a_BlockFace == BLOCK_FACE_NONE) then 30 | return true 31 | end 32 | 33 | local AffectedBlock = cCuboid(a_BlockX, a_BlockY, a_BlockZ) 34 | if (CallHook("OnAreaChanging", AffectedBlock, a_Player, a_Player:GetWorld(), "replacetool")) then 35 | return true 36 | end 37 | 38 | a_Player:GetWorld():SetBlock(Vector3i(a_BlockX, a_BlockY, a_BlockZ), BlockType, BlockMeta) 39 | CallHook("OnAreaChanged", AffectedBlock, a_Player, a_Player:GetWorld(), "replacetool") 40 | return false 41 | end 42 | 43 | local State = GetPlayerState(a_Player) 44 | local Succes, error = State.ToolRegistrator:BindRightClickTool(a_Player:GetEquippedItem().m_ItemType, ReplaceHandler, "replacetool") 45 | 46 | if (not Succes) then 47 | a_Player:SendMessage(cChatColor.Rose .. error) 48 | return true 49 | end 50 | 51 | a_Player:SendMessage(cChatColor.LightPurple .. "Block replacer tool bound to " .. ItemToString(a_Player:GetEquippedItem())) 52 | return true 53 | end 54 | 55 | 56 | ------------------------------------------------ 57 | ----------------------NONE---------------------- 58 | ------------------------------------------------ 59 | function HandleNoneCommand(a_Split, a_Player) 60 | local State = GetPlayerState(a_Player) 61 | local Success, error = State.ToolRegistrator:UnbindTool(a_Player:GetEquippedItem().m_ItemType) 62 | local SuccessMask, errorMask = State.ToolRegistrator:UnbindMask(a_Player:GetEquippedItem().m_ItemType) 63 | 64 | if ((not Success) and (not SuccessMask)) then 65 | a_Player:SendMessage(cChatColor.Rose .. error) 66 | return true 67 | end 68 | 69 | a_Player:SendMessage(cChatColor.LightPurple .. "Tool unbound from your current item.") 70 | return true 71 | end 72 | 73 | 74 | ------------------------------------------------ 75 | ----------------------TREE---------------------- 76 | ------------------------------------------------ 77 | function HandleTreeCommand(a_Split, a_Player) 78 | 79 | local function HandleTree(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 80 | if (a_BlockFace == BLOCK_FACE_NONE) then 81 | return false 82 | end 83 | 84 | local World = a_Player:GetWorld() 85 | if (World:GetBlock(Vector3i(a_BlockX, a_BlockY, a_BlockZ)) == E_BLOCK_GRASS) or (World:GetBlock(Vector3i(a_BlockX, a_BlockY, a_BlockZ)) == E_BLOCK_DIRT) then 86 | World:GrowTree(Vector3i(a_BlockX, a_BlockY + 1, a_BlockZ)) 87 | else 88 | a_Player:SendMessage(cChatColor.Rose .. "A tree can't go there.") 89 | end 90 | end 91 | 92 | local State = GetPlayerState(a_Player) 93 | local Succes, error = State.ToolRegistrator:BindRightClickTool(a_Player:GetEquippedItem().m_ItemType, HandleTree, "tree") 94 | 95 | if (not Succes) then 96 | a_Player:SendMessage(cChatColor.Rose .. error) 97 | return true 98 | end 99 | 100 | a_Player:SendMessage(cChatColor.LightPurple .. "Tree tool bound to " .. ItemToString(a_Player:GetEquippedItem())) 101 | return true 102 | end 103 | 104 | 105 | ----------------------------------------------- 106 | -------------------SUPERPICK------------------- 107 | ----------------------------------------------- 108 | function HandleSuperPickCommand(a_Split, a_Player) 109 | -- // 110 | -- /, 111 | 112 | -- A table containing all the ID's of the pickaxes 113 | local Pickaxes = 114 | { 115 | E_ITEM_WOODEN_PICKAXE, 116 | E_ITEM_STONE_PICKAXE, 117 | E_ITEM_IRON_PICKAXE, 118 | E_ITEM_GOLD_PICKAXE, 119 | E_ITEM_DIAMOND_PICKAXE, 120 | } 121 | 122 | -- The handler that breaks the block of the superpickaxe. 123 | local function SuperPickaxe(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 124 | local AffectedArea = cCuboid(a_BlockX, a_BlockY, a_BlockZ) 125 | if (CallHook("OnAreaChanging", AffectedArea, a_Player, a_Player:GetWorld(), "superpickaxe")) then 126 | return true 127 | end 128 | 129 | local World = a_Player:GetWorld() 130 | World:BroadcastSoundParticleEffect(2001, Vector3i(a_BlockX, a_BlockY, a_BlockZ), World:GetBlock(Vector3i(a_BlockX, a_BlockY, a_BlockZ))) 131 | World:DigBlock(a_BlockX, a_BlockY, a_BlockZ) 132 | 133 | -- Notify other plugins of the change 134 | CallHook("OnAreaChanged", AffectedArea, a_Player, a_Player:GetWorld(), "superpickaxe") 135 | end 136 | 137 | local State = GetPlayerState(a_Player) 138 | 139 | -- Check if at least one of the pickaxe types has the superpickaxe tool. 140 | -- If not then we bind the superpickaxe tool, otherwise unbind all the pickaxes 141 | local WasActivated = false 142 | for Idx, Pickaxe in ipairs(Pickaxes) do 143 | local Info = State.ToolRegistrator:GetLeftClickCallbackInfo(Pickaxe) 144 | if (Info) then 145 | WasActivated = WasActivated or (Info.ToolName == "superpickaxe") 146 | end 147 | end 148 | 149 | if (WasActivated) then 150 | a_Player:SendMessage(cChatColor.LightPurple .. "Super pick axe disabled") 151 | State.ToolRegistrator:UnbindTool(Pickaxes, "superpickaxe") 152 | else 153 | a_Player:SendMessage(cChatColor.LightPurple .. "Super pick axe enabled") 154 | State.ToolRegistrator:BindLeftClickTool(Pickaxes, SuperPickaxe, "superpickaxe") 155 | end 156 | 157 | return true 158 | end 159 | 160 | 161 | 162 | 163 | 164 | function HandleFarwandCommand(a_Split, a_Player) 165 | -- /farwand 166 | 167 | local State = GetPlayerState(a_Player) 168 | 169 | -- Common code for both left and right 170 | local FarWand = function(a_Player, a_BlockFace, a_Point) 171 | if (a_BlockFace ~= BLOCK_FACE_NONE) then 172 | return true 173 | end 174 | 175 | -- Get the block the player is looking at 176 | local TargetBlock, BlockFace = GetTargetBlock(a_Player) 177 | if (not TargetBlock) then 178 | return true 179 | end 180 | 181 | local Succes, Msg = State.Selection:SetPos(TargetBlock.x, TargetBlock.y, TargetBlock.z, BlockFace, a_Point) 182 | a_Player:SendMessage(Msg) 183 | return true 184 | end 185 | 186 | local LeftClick = function(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 187 | return FarWand(a_Player, a_BlockFace, "First") 188 | end 189 | 190 | local RightClick = function(a_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace) 191 | return FarWand(a_Player, a_BlockFace, "Second") 192 | end 193 | 194 | local EquippedItemType = a_Player:GetInventory():GetEquippedItem().m_ItemType 195 | local Succes, Err = State.ToolRegistrator:BindRightClickTool(EquippedItemType, RightClick, "farwand") 196 | if (not Succes) then 197 | a_Player:SendMessage(cChatColor.Rose .. Err) 198 | return true 199 | end 200 | 201 | Succes, Err = State.ToolRegistrator:BindLeftClickTool(EquippedItemType, LeftClick, "farwand") 202 | if (not Succes) then 203 | a_Player:SendMessage(cChatColor.Rose .. Err) 204 | return true 205 | end 206 | 207 | a_Player:SendMessage(cChatColor.LightPurple .. "Far wand tool bound to " .. ItemTypeToString(EquippedItemType)) 208 | return true 209 | end 210 | -------------------------------------------------------------------------------- /Config.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Config.lua 3 | 4 | -- Contains the functions to initialize and write the configuration file. 5 | 6 | 7 | 8 | 9 | 10 | g_Config = {} 11 | 12 | 13 | 14 | 15 | 16 | -- Create an environment for the config loader where the admin can use item names directly without quotes. 17 | local g_LoaderEnv = {} 18 | for Key, Value in pairs(_G) do 19 | if (Key:match("^E_.*")) then 20 | g_LoaderEnv[ItemTypeToString(Value)] = Value 21 | end 22 | end 23 | 24 | 25 | 26 | 27 | 28 | local g_ConfigDefault = 29 | [[ 30 | WandItem = woodenaxe, 31 | Limits = 32 | { 33 | ButcherRadius = -1, 34 | MaxBrushRadius = 5, 35 | DisallowedBlocks = {6, 7, 14, 15, 16, 26, 27, 28, 29, 39, 31, 32, 33, 34, 36, 37, 38, 39, 40, 46, 50, 51, 56, 59, 69, 73, 74, 75, 76, 77, 81, 83}, 36 | }, 37 | 38 | Defaults = 39 | { 40 | ButcherRadius = 20, 41 | }, 42 | 43 | NavigationWand = 44 | { 45 | Item = compass, 46 | MaxDistance = 120, 47 | TeleportNoHit = true, 48 | }, 49 | 50 | Scripting = 51 | { 52 | -- If true it logs an error when a craftscript failed 53 | Debug = false, 54 | 55 | -- The amount of seconds that a script may be active. Any longer and the script will be aborted. 56 | -- If negative the time a script can run is unlimited. 57 | MaxExecutionTime = 5, 58 | }, 59 | 60 | Schematics = 61 | { 62 | OverrideExistingFiles = true, 63 | }, 64 | 65 | Updates = 66 | { 67 | CheckForUpdates = true, 68 | NumAttempts = 3, 69 | ShowMessageWhenUpToDate = true, 70 | DownloadNewerVersion = true, 71 | }, 72 | 73 | Storage = 74 | { 75 | -- If set to true the selection of a player will be remembered once he leaves. 76 | RememberPlayerSelection = true, 77 | 78 | -- If WorldEdit needs to change a format in the database the database will be backuped first before changing. 79 | -- This doesn't mean when adding or removing data the database will be backed up. Only when the used database is outdated. 80 | BackupDatabaseWhenUpdating = true, 81 | } 82 | ]] 83 | 84 | 85 | 86 | 87 | 88 | -- Writes the default configuration to a_Path 89 | local function WriteDefaultConfiguration(a_Path) 90 | LOGWARNING("Default configuration written to \"" .. a_Path .. "\"") 91 | local File = io.open(a_Path, "w") 92 | File:write(g_ConfigDefault) 93 | File:close() 94 | end 95 | 96 | 97 | 98 | 99 | 100 | -- Returns the default configuration table. This can be directly used in g_Config 101 | local function GetDefaultConfigurationTable() 102 | -- Load the default config 103 | local Loader = loadstring("return {" .. g_ConfigDefault .. "}") 104 | 105 | -- Apply the environment to the configloader. 106 | setfenv(Loader, g_LoaderEnv) 107 | 108 | return Loader() 109 | end 110 | 111 | 112 | 113 | 114 | 115 | -- Sets g_Config to the default configuration 116 | local function LoadDefaultConfiguration() 117 | LOGWARNING("The default configuration will be used.") 118 | g_Config = GetDefaultConfigurationTable() 119 | end 120 | 121 | 122 | 123 | 124 | 125 | -- Finds from an error message where the error occurred. 126 | local function FindErrorPosition(a_ErrorMessage) 127 | local ErrorPosition = a_ErrorMessage:match(":(.-):") or 0 128 | return ErrorPosition 129 | end 130 | 131 | 132 | 133 | 134 | 135 | -- Loads the configuration from the given path. If it doesn't exist, or if there is an error in the config file it will load the defaults. 136 | function InitializeConfiguration(a_Path) 137 | local ConfigContent = cFile:ReadWholeFile(a_Path) 138 | 139 | -- The configuration file doesn't exist or is empty. Write and load the default value 140 | if (ConfigContent == "") then 141 | WriteDefaultConfiguration(a_Path) 142 | LoadDefaultConfiguration() 143 | return 144 | end 145 | 146 | -- Load the content of the config file. Place brackets around it to make the whole thing a table. 147 | -- Also, return the table when executed 148 | local ConfigLoader, Error = loadstring("return {" .. ConfigContent .. "}") 149 | if (not ConfigLoader) then 150 | local ErrorPosition = FindErrorPosition(Error) 151 | LOGWARNING("Error in the configuration file near line " .. ErrorPosition) 152 | LoadDefaultConfiguration() 153 | return 154 | end 155 | 156 | -- Apply the environment to the configloader. 157 | setfenv(ConfigLoader, g_LoaderEnv) 158 | 159 | -- Execute the loader. It returns true + the configuration if it executed properly. Else it returns false with the error message. 160 | local Succes, Result = pcall(ConfigLoader) 161 | if (not Succes) then 162 | local ErrorPosition = FindErrorPosition(Result) 163 | LOGWARNING("Error in the configuration file at line " .. ErrorPosition) 164 | LoadDefaultConfiguration() 165 | return 166 | end 167 | 168 | -- Merge the configuration with the default configuration. 169 | -- When the admin missed something in the configuration it will be set to the default value. 170 | local DefaultConfig = GetDefaultConfigurationTable() 171 | table.merge(Result, DefaultConfig) 172 | 173 | -- Make a dictionary out of the array of disallowed blocks. 174 | if (Result.Limits and (type(Result.Limits.DisallowedBlocks) == "table")) then 175 | Result.Limits.DisallowedBlocks = table.todictionary(Result.Limits.DisallowedBlocks) 176 | end 177 | 178 | -- Set the g_Config table. 179 | g_Config = Result 180 | end 181 | -------------------------------------------------------------------------------- /Constants.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Constants.lua 3 | 4 | -- Contains global variables that WorldEdit reguraly uses. 5 | 6 | 7 | 8 | 9 | 10 | --- Some blocks won't render if the meta value is 0. 11 | -- Here we keep a list of all of them with the first allowed meta value. 12 | -- Please keep the list alphasorted. 13 | g_DefaultMetas = { 14 | [E_BLOCK_CHEST] = 2, 15 | [E_BLOCK_ENDER_CHEST] = 2, 16 | [E_BLOCK_FURNACE] = 2, 17 | [E_BLOCK_LADDER] = 2, 18 | [E_BLOCK_LIT_FURNACE] = 2, 19 | [E_BLOCK_NETHER_PORTAL] = 1, 20 | [E_BLOCK_TORCH] = 1, 21 | [E_BLOCK_TRAPPED_CHEST] = 2, 22 | [E_BLOCK_REDSTONE_TORCH_ON] = 1, 23 | [E_BLOCK_REDSTONE_TORCH_OFF] = 1, 24 | [E_BLOCK_WALLSIGN] = 2, 25 | [E_BLOCK_WALL_BANNER] = 2 26 | } 27 | 28 | 29 | 30 | 31 | 32 | E_DIRECTION_NORTH1 = 0 33 | E_DIRECTION_NORTH2 = 4 34 | E_DIRECTION_EAST = 1 35 | E_DIRECTION_SOUTH = 2 36 | E_DIRECTION_WEST = 3 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Cuberite Team 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LibrariesExpansion/math.lua: -------------------------------------------------------------------------------- 1 | 2 | -- math.lua 3 | 4 | -- Contains functions to expand the math library 5 | 6 | 7 | 8 | 9 | 10 | -- Rounds the number. 11 | function math.round(a_GivenNumber) 12 | local Number, Decimal = math.modf(a_GivenNumber) 13 | if (Decimal >= 0.5) then 14 | return Number + 1 15 | else 16 | return Number 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /LibrariesExpansion/string.lua: -------------------------------------------------------------------------------- 1 | 2 | -- string.lua 3 | 4 | -- Contains functions to expand the string library 5 | 6 | 7 | 8 | 9 | 10 | -- Makes the first character of a string uppercase, and lowercases the rest. 11 | function string.ucfirst(a_String) 12 | local firstChar = a_String:sub(1, 1):upper() 13 | local Rest = a_String:sub(2):lower() 14 | 15 | return firstChar .. Rest 16 | end 17 | 18 | 19 | 20 | 21 | -- Returns true if str ends with ending 22 | function string.endswith(str, ending) 23 | return ending == "" or str:sub(-#ending) == ending 24 | end 25 | -------------------------------------------------------------------------------- /LibrariesExpansion/table.lua: -------------------------------------------------------------------------------- 1 | 2 | -- table.lua 3 | 4 | -- Contains functions to expand the table library 5 | 6 | 7 | 8 | 9 | 10 | -- Returns true if the given table is an array, otherwise it returns false 11 | function table.isarray(a_Table) 12 | local i = 0 13 | for _, t in pairs(a_Table) do 14 | i = i + 1 15 | if (not rawget(a_Table, i)) then 16 | return false 17 | end 18 | end 19 | 20 | return true 21 | end 22 | 23 | 24 | 25 | 26 | 27 | -- Merges all values (except arrays) from a_DstTable into a_SrcTable if the key doesn't exist in a_SrcTable 28 | function table.merge(a_SrcTable, a_DstTable) 29 | for Key, Value in pairs(a_DstTable) do 30 | if (a_SrcTable[Key] == nil) then 31 | a_SrcTable[Key] = Value 32 | elseif ((type(Value) == "table") and (type(a_SrcTable[Key]) == "table")) then 33 | if (not table.isarray(a_SrcTable[Key])) then 34 | table.merge(a_SrcTable[Key], Value) 35 | end 36 | end 37 | end 38 | return a_SrcTable 39 | end 40 | 41 | 42 | 43 | 44 | 45 | -- Creates a table using all the values in a_Table as an index 46 | function table.todictionary(a_Table) 47 | local res = {} 48 | for Key, Value in pairs(a_Table) do 49 | res[Value] = true 50 | end 51 | return res 52 | end 53 | -------------------------------------------------------------------------------- /Storage/ChangeScripts/1.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "PlayerSelection" ( 2 | `uuid` TEXT PRIMARY KEY, 3 | `MinX` INTEGER, 4 | `MaxX` INTEGER, 5 | `MinY` INTEGER, 6 | `MaxY` INTEGER, 7 | `MinZ` INTEGER, 8 | `MaxZ` INTEGER 9 | ); 10 | 11 | CREATE TABLE IF NOT EXISTS "DatabaseInfo" ( 12 | `DatabaseVersion` INTEGER NOT NULL 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS "NamedPlayerSelection" ( 16 | `uuid` TEXT, 17 | `selname` TEXT, 18 | `MinX` INTEGER, 19 | `MaxX` INTEGER, 20 | `MinY` INTEGER, 21 | `MaxY` INTEGER, 22 | `MinZ` INTEGER, 23 | `MaxZ` INTEGER, 24 | PRIMARY KEY(`uuid`, `selname`) 25 | ); 26 | 27 | INSERT INTO "DatabaseInfo" (`DatabaseVersion`) 28 | VALUES (1) 29 | -------------------------------------------------------------------------------- /Storage/Queries/get_namedplayerselection.sql: -------------------------------------------------------------------------------- 1 | SELECT `MinX`, `MaxX`, `MinY`, `MaxY`, `MinZ`, `MaxZ` 2 | FROM "NamedPlayerSelection" 3 | WHERE `uuid` = $playeruuid 4 | AND `selname` = $selname -------------------------------------------------------------------------------- /Storage/Queries/get_playerselection.sql: -------------------------------------------------------------------------------- 1 | SELECT `MinX`, `MaxX`, `MinY`, `MaxY`, `MinZ`, `MaxZ` 2 | FROM "PlayerSelection" 3 | WHERE uuid = $playeruuid -------------------------------------------------------------------------------- /Storage/Queries/remove_playerselection.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM "PlayerSelection" 2 | WHERE `uuid` = $playeruuid -------------------------------------------------------------------------------- /Storage/Queries/set_namedplayerselection.sql: -------------------------------------------------------------------------------- 1 | REPLACE INTO "NamedPlayerSelection" (`uuid`, `selname`, `MinX`, `MaxX`, `MinY`, `MaxY`, `MinZ`, `MaxZ`) 2 | VALUES ($playeruuid, $selname, $MinX, $MaxX, $MinY, $MaxY, $MinZ, $MaxZ) -------------------------------------------------------------------------------- /Storage/Queries/set_playerselection.sql: -------------------------------------------------------------------------------- 1 | REPLACE INTO "PlayerSelection" (`uuid`, `MinX`, `MaxX`, `MinY`, `MaxY`, `MinZ`, `MaxZ`) 2 | VALUES ($playeruuid, $MinX, $MaxX, $MinY, $MaxY, $MinZ, $MaxZ) -------------------------------------------------------------------------------- /Storage/Storage.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Storage.lua 3 | 4 | -- Implements the SQL storage. 5 | 6 | 7 | 8 | 9 | 10 | -- The current database version. If something changes in the structure of the database or table use this 11 | local g_CurrentDatabaseVersion = 1 12 | 13 | 14 | 15 | 16 | 17 | local g_Queries = {} 18 | local QueryPath = cPluginManager:Get():GetCurrentPlugin():GetLocalFolder() .. "/Storage/Queries" 19 | for _, FileName in ipairs(cFile:GetFolderContents(QueryPath)) do 20 | if (FileName:match("%.sql$")) then 21 | g_Queries[FileName:match("^(.*)%.sql$")] = cFile:ReadWholeFile(QueryPath .. "/" .. FileName) 22 | end 23 | end 24 | 25 | 26 | 27 | 28 | 29 | local g_ChangeScripts = {} 30 | local ChangeScriptPath = cPluginManager:Get():GetCurrentPlugin():GetLocalFolder() .. "/Storage/ChangeScripts" 31 | for _, FileName in ipairs(cFile:GetFolderContents(ChangeScriptPath)) do 32 | if (FileName:match("%.sql$")) then 33 | g_ChangeScripts[FileName:match("^(.*)%.sql$")] = cFile:ReadWholeFile(ChangeScriptPath .. "/" .. FileName) 34 | end 35 | end 36 | 37 | 38 | 39 | 40 | 41 | cSQLStorage = {} 42 | 43 | 44 | 45 | 46 | 47 | -- Creates new cSQLStorage object and initializes or updates the database 48 | local function cSQLStorage_new() 49 | local Obj = {} 50 | 51 | setmetatable(Obj, cSQLStorage) 52 | cSQLStorage.__index = cSQLStorage 53 | 54 | local PluginRoot = cPluginManager:Get():GetCurrentPlugin():GetLocalFolder() 55 | local ErrorCode, ErrorMsg; 56 | Obj.DB, ErrorCode, ErrorMsg = sqlite3.open(PluginRoot .. "/Storage/storage.sqlite") 57 | if (Obj.DB == nil) then 58 | LOGWARNING("Database could not be opened. Aborting"); 59 | error(ErrorMsg); -- Abort the plugin 60 | end 61 | 62 | -- Get the version of the database 63 | local SavedDatabaseVersion = -1 64 | Obj:ExecuteStatement("SELECT * FROM sqlite_master WHERE name = 'DatabaseInfo' AND type='table'", nil, 65 | function() 66 | -- This function will be called if the "DatabaseInfo" table exists. 67 | Obj:ExecuteStatement("SELECT `DatabaseVersion` FROM DatabaseInfo", nil, 68 | function(a_Data) 69 | SavedDatabaseVersion = a_Data["DatabaseVersion"] 70 | end 71 | ) 72 | end 73 | ) 74 | 75 | -- Check if the database version is valid or if we should update it. 76 | if (SavedDatabaseVersion < g_CurrentDatabaseVersion) then 77 | -- Check if we are allowed to update the database. 78 | if (g_Config.BackupDatabaseWhenUpdating) then 79 | -- Only back up the database if the database wasn't just created 80 | if (SavedDatabaseVersion ~= -1) then 81 | if (not cFile:IsFolder(PluginRoot .. "/Storage/Backups")) then 82 | cFile:CreateDirectory(PluginRoot .. "/Storage/Backups") 83 | end 84 | 85 | cFile:Copy(PluginRoot .. "/Storage/storage.sqlite", PluginRoot .. ("/Storage/Backups/storage %s.sqlite"):format(os.date("%Y-%m-%d"))) 86 | end 87 | end 88 | 89 | for I = math.max(SavedDatabaseVersion, 1), g_CurrentDatabaseVersion do 90 | Obj:ExecuteChangeScript(tostring(I)) 91 | end 92 | elseif (SavedDatabaseVersion > g_CurrentDatabaseVersion) then 93 | error("Unknown database version!") 94 | end 95 | 96 | return Obj 97 | end 98 | 99 | 100 | 101 | 102 | 103 | --- Returns the cSQLStorage singleton object. 104 | -- Creates it first if it doesn't exist yet. 105 | function cSQLStorage:Get() 106 | if (not cSQLStorage.Storage) then 107 | cSQLStorage.Storage = cSQLStorage_new() 108 | 109 | end 110 | 111 | return cSQLStorage.Storage 112 | end 113 | 114 | 115 | 116 | 117 | 118 | 119 | --- Executes a query that was loaded before. 120 | -- The parameters is a dictionary. The key is the name of the parameter. 121 | -- This can be found with a $ or : in front of it in the actual query. 122 | -- If a callback is given it calls that for each row where the parameter is a dictionary 123 | -- Returns true on success, while it returns false with the error message when failing 124 | function cSQLStorage:ExecuteCommand(a_QueryName, a_Parameters, a_Callback) 125 | local Command = assert(g_Queries[a_QueryName], "Requested Query doesn't exist") 126 | local Commands = StringSplit(Command, ";") 127 | 128 | for _, Sql in ipairs(Commands) do 129 | local CommandExecuted, ErrMsg = self:ExecuteStatement(Sql, a_Parameters, a_Callback) 130 | if (not CommandExecuted) then 131 | return false, ErrMsg 132 | end 133 | end 134 | 135 | return true 136 | end 137 | 138 | 139 | 140 | 141 | 142 | -- Executes a ChangeScript that was loaded before. 143 | -- This is to update the database if something changed in it's structure 144 | function cSQLStorage:ExecuteChangeScript(a_ChangeScriptName) 145 | local Command = assert(g_ChangeScripts[a_ChangeScriptName], "Requested changescript doesn't exist") 146 | local Commands = StringSplit(Command, ";") 147 | for _, Sql in ipairs(Commands) do 148 | local CommandExecuted, ErrMsg = self:ExecuteStatement(Sql) 149 | if (not CommandExecuted) then 150 | return false, ErrMsg 151 | end 152 | end 153 | end 154 | 155 | 156 | 157 | 158 | 159 | --- Executes an SQL statement 160 | -- The parameters is a dictionary. The key is the name of the parameter. 161 | -- This can be found with a $ or : in front of it in the actual query. 162 | -- If a callback is given it calls that for each row where the parameter is a dictionary 163 | -- Returns true on success, while it returns false with the error message when failing 164 | function cSQLStorage:ExecuteStatement(a_Sql, a_Parameters, a_Callback) 165 | local Stmt, ErrCode, ErrMsg = self.DB:prepare(a_Sql) 166 | if (not Stmt) then 167 | LOGWARNING("Cannot prepare query >>" .. a_Sql .. "<<: " .. (ErrCode or "") .. " (" .. (ErrMsg or "") .. ")") 168 | return false, ErrMsg or "" 169 | end 170 | 171 | if (a_Parameters ~= nil) then 172 | Stmt:bind_names(a_Parameters) 173 | end 174 | 175 | if (a_Callback ~= nil) then 176 | for val in Stmt:nrows() do 177 | if (a_Callback(val)) then 178 | break 179 | end 180 | end 181 | else 182 | Stmt:step() 183 | end 184 | 185 | Stmt:finalize() 186 | return true 187 | end 188 | -------------------------------------------------------------------------------- /Tests/selection.lua: -------------------------------------------------------------------------------- 1 | -- selection.lua 2 | 3 | -- These files are used with the CuberitePluginChecker script. 4 | -- This test tests the selection and actions on the selection. 5 | -- Usage: lua CuberitePluginChecker.lua -a AutoAPI -e ManualAPI.lua -i APIImpl/All.lua -p -s /selection.lua -f ^E_ 6 | 7 | 8 | 9 | 10 | 11 | scenario 12 | { 13 | redirectPluginFiles 14 | { 15 | -- Redirect the default config file. 16 | -- This disables the update check and also works around a bug in the simulator which causes block/item enums to not be in the global environment. 17 | ["config.cfg"] = "test.config.cfg" 18 | }, 19 | world 20 | { 21 | name = "world" 22 | }, 23 | initializePlugin(), 24 | connectPlayer 25 | { 26 | name = "TestUser" 27 | }, 28 | playerCommand 29 | { 30 | playerName = "TestUser", 31 | command = "//pos1", 32 | }, 33 | playerCommand 34 | { 35 | playerName = "TestUser", 36 | command = "//pos2", 37 | }, 38 | playerCommand 39 | { 40 | playerName = "TestUser", 41 | command = "//set 0", 42 | }, 43 | } 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Tests/test.config.cfg: -------------------------------------------------------------------------------- 1 | WandItem = 271, 2 | Limits = 3 | { 4 | ButcherRadius = -1, 5 | MaxBrushRadius = 5, 6 | DisallowedBlocks = {6, 7, 14, 15, 16, 26, 27, 28, 29, 39, 31, 32, 33, 34, 36, 37, 38, 39, 40, 46, 50, 51, 56, 59, 69, 73, 74, 75, 76, 77, 81, 83}, 7 | }, 8 | 9 | Defaults = 10 | { 11 | ButcherRadius = 20, 12 | }, 13 | 14 | NavigationWand = 15 | { 16 | Item = 345, 17 | MaxDistance = 120, 18 | TeleportNoHit = true, 19 | }, 20 | 21 | Scripting = 22 | { 23 | -- If true it logs an error when a craftscript failed 24 | Debug = false, 25 | 26 | -- The amount of seconds that a script may be active. Any longer and the script will be aborted. 27 | -- If negative the time a script can run is unlimited. 28 | MaxExecutionTime = 5, 29 | }, 30 | 31 | Schematics = 32 | { 33 | OverrideExistingFiles = true, 34 | }, 35 | 36 | Updates = 37 | { 38 | CheckForUpdates = false, 39 | NumAttempts = 3, 40 | ShowMessageWhenUpToDate = true, 41 | DownloadNewerVersion = true, 42 | }, 43 | 44 | Storage = 45 | { 46 | -- If set to true the selection of a player will be remembered once he leaves. 47 | RememberPlayerSelection = true, 48 | 49 | -- If WorldEdit needs to change a format in the database the database will be backuped first before changing. 50 | -- This doesn't mean when adding or removing data the database will be backed up. Only when the used database is outdated. 51 | BackupDatabaseWhenUpdating = true, 52 | } 53 | -------------------------------------------------------------------------------- /WorldEdit.deproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Clipboard.lua 5 | 6 | 7 | Info.lua 8 | 9 | 10 | ..\InfoReg.lua 11 | 12 | 13 | PlayerSelection.lua 14 | 15 | 16 | PlayerState.lua 17 | 18 | 19 | UndoStack.lua 20 | 21 | 22 | api_Check.lua 23 | 24 | 25 | api_Manage.lua 26 | 27 | 28 | cmd_AlterLandscape.lua 29 | 30 | 31 | cmd_Entities.lua 32 | 33 | 34 | cmd_Navigation.lua 35 | 36 | 37 | cmd_Other.lua 38 | 39 | 40 | cmd_Selection.lua 41 | 42 | 43 | cmd_Tools.lua 44 | 45 | 46 | cmd_functions.lua 47 | 48 | 49 | functions.lua 50 | 51 | 52 | hooks.lua 53 | 54 | 55 | main.lua 56 | 57 | 58 | -------------------------------------------------------------------------------- /craftscripts/FloodyWater.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Floody simulator craftscript for WorldEdit (Cuberite) 3 | 4 | Permission needed for this script is "worldedit.scripting.execute" (By default) and "worldedit.craftscript.FloodyWater". 5 | Usage: Select the region you want water to be simulated with the wand and then execute this script. 6 | ]]-- 7 | 8 | 9 | local a_Player, a_Split = ... 10 | 11 | -- Check if the player has the right permission 12 | if (not a_Player:HasPermission("worldedit.craftscript.FloodyWater")) then 13 | a_Player:SendMessage(cChatColor.Rose .. "You don't have permission to use this script.") 14 | return true 15 | end 16 | 17 | local PlayerState = GetPlayerState(a_Player) 18 | 19 | -- Check if the selected region is valid. 20 | if (not PlayerState.Selection:IsValid()) then 21 | a_Player:SendMessage(cChatColor.Rose .. "Please select a region first.") 22 | return true 23 | end 24 | 25 | local SrcCuboid = PlayerState.Selection:GetSortedCuboid() 26 | local World = a_Player:GetWorld() 27 | 28 | -- Check if other plugins might want to block this action. 29 | if (CallHook("OnAreaChanging", SrcCuboid, a_Player, World, "craftscript.FloodyWater")) then 30 | return true 31 | end 32 | 33 | 34 | 35 | 36 | 37 | ------------------------------------------------------------------------------------------------------------------------------ 38 | --------------------------------------------------------------- 39 | -- Helper Functions 40 | 41 | -- IsWater returns true if the given blocktype is water. 42 | local function IsWater(a_BlockType) 43 | if ( 44 | (a_BlockType == E_BLOCK_WATER) or 45 | (a_BlockType == E_BLOCK_STATIONARY_WATER) 46 | ) then 47 | return true 48 | end 49 | return false 50 | end 51 | 52 | 53 | 54 | 55 | 56 | -- IsWashAble returns true if the given blocktype is a block that water can wash away. 57 | local g_IsWashAble = table.todictionary{ 58 | E_BLOCK_AIR, 59 | E_BLOCK_BROWN_MUSHROOM, 60 | E_BLOCK_CACTUS, 61 | E_BLOCK_COBWEB, 62 | E_BLOCK_CROPS, 63 | E_BLOCK_DEAD_BUSH, 64 | E_BLOCK_LILY_PAD, 65 | E_BLOCK_RAIL, 66 | E_BLOCK_REDSTONE_TORCH_OFF, 67 | E_BLOCK_REDSTONE_TORCH_ON, 68 | E_BLOCK_REDSTONE_WIRE, 69 | E_BLOCK_RED_MUSHROOM, 70 | E_BLOCK_RED_ROSE, 71 | E_BLOCK_SNOW, 72 | E_BLOCK_SUGARCANE, 73 | E_BLOCK_TALL_GRASS, 74 | E_BLOCK_TORCH, 75 | E_BLOCK_YELLOW_FLOWER, 76 | E_BLOCK_TALL_GRASS, 77 | E_BLOCK_BIG_FLOWER, 78 | } 79 | 80 | 81 | 82 | 83 | 84 | local g_PossibleFlowCoordinates = { 85 | {x = -1, z = 0}, 86 | {x = 1, z = 0}, 87 | {x = 0, z = -1}, 88 | {x = 0, z = 1}, 89 | } 90 | 91 | -- End of helper functions. 92 | --------------------------------------------------------------- 93 | ------------------------------------------------------------------------------------------------------------------------------ 94 | 95 | 96 | 97 | 98 | 99 | local BlockArea = cBlockArea() 100 | BlockArea:Read(World, SrcCuboid, cBlockArea.baTypes + cBlockArea.baMetas) 101 | 102 | PlayerState.UndoStack:PushUndoFromCuboid(World, SrcCuboid, "craftscript.FloodyWater") 103 | 104 | local SizeX, SizeY, SizeZ = BlockArea:GetCoordRange() 105 | local Cuboid = cCuboid( 106 | Vector3i(0, 0, 0), 107 | Vector3i(SizeX, SizeY, SizeZ) 108 | ) 109 | local ins = table.insert 110 | 111 | -- Find all the waterblocks that are currently in the area. 112 | local WaterBlocks = {} 113 | for X = 0, SizeX do 114 | for Y = 0, SizeY do 115 | for Z = 0, SizeZ do 116 | if (IsWater(BlockArea:GetRelBlockType(X, Y, Z))) then 117 | ins(WaterBlocks, {x = X, y = Y, z = Z}) 118 | end 119 | end 120 | end 121 | end 122 | 123 | 124 | while (WaterBlocks[1]) do 125 | local OldWaterBlocks = WaterBlocks 126 | WaterBlocks = {} 127 | 128 | for Idx, Coord in ipairs(OldWaterBlocks) do 129 | if (Cuboid:IsInside(Coord.x, Coord.y - 1, Coord.z) and g_IsWashAble[BlockArea:GetRelBlockType(Coord.x, Coord.y - 1, Coord.z)] or IsWater(BlockArea:GetRelBlockType(Coord.x, Coord.y - 1, Coord.z))) then 130 | BlockArea:SetRelBlockTypeMeta(Coord.x, Coord.y - 1, Coord.z, E_BLOCK_WATER, 8) 131 | ins(WaterBlocks, {x = Coord.x, y = Coord.y - 1, z = Coord.z}) 132 | else 133 | local Meta = BlockArea:GetRelBlockMeta(Coord.x, Coord.y, Coord.z) 134 | if (Meta ~= 7) then -- Higher then 7 isn't possible, and we already checked down. 135 | for Idx, RelCoords in ipairs(g_PossibleFlowCoordinates) do 136 | local X, Y, Z = Coord.x + RelCoords.x, Coord.y, Coord.z + RelCoords.z 137 | if (Cuboid:IsInside(X, Y, Z) and g_IsWashAble[BlockArea:GetRelBlockType(X, Y, Z)]) then 138 | BlockArea:SetRelBlockTypeMeta(X, Y, Z, E_BLOCK_WATER, (Meta == 8 and 1) or Meta + 1) 139 | ins(WaterBlocks, {x = X, y = Y, z = Z}) 140 | end 141 | end 142 | end 143 | end 144 | end 145 | end 146 | 147 | 148 | BlockArea:Write(World, SrcCuboid.p1.x, SrcCuboid.p1.y, SrcCuboid.p1.z, cBlockArea.baTypes + cBlockArea.baMetas) 149 | CallHook("OnAreaChanged", SrcCuboid, a_Player, World, "craftscript.FloodyWater") 150 | 151 | return true 152 | -------------------------------------------------------------------------------- /craftscripts/Readme.md: -------------------------------------------------------------------------------- 1 | ### WARNING! Only use scripts that you trust. Scripts are able to bypass the server's permission, change server settings and more. 2 | ### Anyone with the worldedit.scripting.execute permission is able to execute them. 3 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | 2 | -- main.lua 3 | 4 | -- Contains the Initialize and OnDisable functions. It also loads all the necessary files. 5 | 6 | 7 | 8 | 9 | 10 | -- First of all load all the library expansions 11 | dofolder(cPluginManager:Get():GetCurrentPlugin():GetLocalFolder() .. "/LibrariesExpansion") 12 | 13 | 14 | 15 | 16 | 17 | --- All the folders that shouldn't be loaded by 18 | g_ExcludedFolders = table.todictionary{ 19 | "craftscripts", 20 | "LibrariesExpansion", 21 | "Tests", 22 | ".", 23 | "..", 24 | } 25 | 26 | 27 | 28 | 29 | 30 | -- Load all the folders 31 | local WorldEditPath = cPluginManager:Get():GetCurrentPlugin():GetLocalFolder() 32 | for _, Folder in ipairs(cFile:GetFolderContents(WorldEditPath)) do repeat 33 | local Path = WorldEditPath .. "/" .. Folder 34 | if (not cFile:IsFolder(Path)) then 35 | break -- Is a continue due to a do-while directly after the for 36 | end 37 | 38 | if (g_ExcludedFolders[Folder]) then 39 | break -- Is a continue due to a do-while directly after the for 40 | end 41 | 42 | dofolder(Path) 43 | until true end 44 | 45 | 46 | 47 | 48 | 49 | function Initialize(a_Plugin) 50 | a_Plugin:SetName(g_PluginInfo.Name) 51 | a_Plugin:SetVersion(g_PluginInfo.Version) 52 | 53 | InitializeConfiguration(a_Plugin:GetLocalFolder() .. "/config.cfg") 54 | 55 | -- Load the InfoReg shared library: 56 | dofile(cPluginManager:GetPluginsPath() .. "/InfoReg.lua") 57 | 58 | --Bind all the commands: 59 | RegisterPluginInfoCommands(); 60 | 61 | if (g_Config.Updates.CheckForUpdates) then 62 | cUpdater:CheckForNewerVersion() 63 | end 64 | 65 | -- Initialize SQL Storage 66 | cSQLStorage:Get() 67 | 68 | cFile:CreateFolder("schematics") 69 | 70 | LOG("Enabling v" .. g_PluginInfo.DisplayVersion) 71 | return true 72 | end 73 | 74 | 75 | 76 | 77 | 78 | function OnDisable() 79 | LOG("Disabling v" .. g_PluginInfo.DisplayVersion) 80 | ForEachPlayerState( 81 | function(a_State) 82 | a_State:Save(a_State:GetUUID()) 83 | end 84 | ) 85 | end 86 | --------------------------------------------------------------------------------