├── .github └── workflows │ └── build.yml ├── .gitignore ├── license ├── pom.xml ├── readme.md └── src └── main ├── java └── me │ └── youhavetrouble │ └── blockedit │ ├── BELocale.java │ ├── BEPlayer.java │ ├── BlockEdit.java │ ├── BlockEditCommands.java │ ├── JoinLeaveListener.java │ ├── SchematicHandler.java │ ├── WandsHandler.java │ ├── api │ ├── BlockEditAPI.java │ ├── BlockEditOperation.java │ ├── BlockEditWand.java │ ├── OperationWork.java │ └── WorkSplitter.java │ ├── commands │ ├── arguments │ │ ├── BlockDataArgument.java │ │ ├── InvalidDataException.java │ │ ├── InvalidMaterialException.java │ │ └── SchematicProviderArgument.java │ └── exceptiontype │ │ └── UnknownProviderExceptionType.java │ ├── exception │ ├── NoProviderForSchematicFileExtensionException.java │ ├── SchematicException.java │ ├── SchematicHandlerRegistrationException.java │ ├── SchematicLoadException.java │ └── SchematicSaveException.java │ ├── operations │ ├── PasteOperation.java │ ├── ReplaceOperation.java │ └── SetOperation.java │ ├── schematic │ ├── FileSchematicProvider.java │ ├── Schematic.java │ └── SchematicProvider.java │ ├── util │ ├── ChunkWork.java │ ├── Clipboard.java │ ├── NameFilenameFilter.java │ └── Selection.java │ └── wands │ └── SelectionWand.java └── resources ├── locale ├── en_US.json └── pl_PL.json └── paper-plugin.yml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build plugin 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Set up JDK 21 12 | uses: actions/setup-java@v3 13 | with: 14 | java-version: 21 15 | distribution: 'temurin' 16 | - name: Build with Maven 17 | run: mvn -B package --file pom.xml -Drevision=${{ github.ref_name }}-${{ github.sha }} 18 | - name: Copy artifacts 19 | run: mkdir staging && cp target/BlockEdit-*.jar staging 20 | - name: Upload artifacts 21 | uses: actions/upload-artifact@v4 22 | with: 23 | name: BlockEdit-${{ github.ref_name }}-${{ github.sha }} 24 | path: staging -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | me.youhavetrouble 8 | BlockEdit 9 | ${revision} 10 | jar 11 | 12 | BlockEdit 13 | 14 | Modern WorldEdit alternative 15 | 16 | 21 17 | UTF-8 18 | 1.0-SNAPSHOT 19 | 20 | youhavetrouble.me 21 | 22 | 23 | 24 | 25 | 26 | org.apache.maven.plugins 27 | maven-compiler-plugin 28 | 3.8.1 29 | 30 | ${java.version} 31 | ${java.version} 32 | 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-shade-plugin 37 | 3.6.0 38 | 39 | 40 | package 41 | 42 | shade 43 | 44 | 45 | false 46 | 47 | 48 | net.querz 49 | me.youhavetrouble.blockedit.net.querz 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | src/main/resources 60 | true 61 | 62 | 63 | 64 | 65 | 66 | 67 | papermc 68 | https://repo.papermc.io/repository/maven-public/ 69 | 70 | 71 | jitpack.io 72 | https://jitpack.io 73 | 74 | 75 | 76 | 77 | 78 | io.papermc.paper 79 | paper-api 80 | 1.21.3-R0.1-SNAPSHOT 81 | provided 82 | 83 | 84 | com.github.Querz 85 | NBT 86 | 6.1 87 | compile 88 | 89 | 90 | org.reflections 91 | reflections 92 | 0.10.2 93 | compile 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

BlockEdit

2 |

An alternative to WorldEdit for modern Paper servers.

3 | 4 |

❗ This is just a hobby project, it will never get as good as worldedit, but you're free to use it as you like.

5 | 6 |

❗ This plugin is not finished yet, it's still in development.

7 | 8 |

Perks

9 |
    10 |
  • 11 | No NMS - This plugin doesn't use any NMS, so it doesn't require updating every time a new Minecraft version 12 | comes out. 13 |
  • 14 |
  • 15 | Work splitter - This plugin splits the work between multiple ticks by chunk. This means that it won't lag the 16 | server as much as WorldEdit. It will also ease the client lag when nearby updating chunks, since only set number of 17 | chunks will be updated per tick. 18 |
  • 19 |
  • 20 | Simple API - BlockEdit is made with being extendable in mind. You can add your own operations that can be used 21 | with the work splitter system. You can also add your own wands that can do anything you want them to do. 22 |
  • 23 | 24 |
-------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/BELocale.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit; 2 | 3 | import com.google.gson.JsonObject; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.HashMap; 7 | import java.util.Locale; 8 | import java.util.Map; 9 | 10 | public class BELocale { 11 | 12 | private static final Map locales = new HashMap<>(); 13 | private static final Locale defaultLocale = Locale.of("en_US"); 14 | 15 | public final String couldNotFindWandById, selectArea, copiedSelectionToClipboard, selectionReset, firstPositionSet, 16 | secondPositionSet, pastingClipboard, clipboardRotated, settingBlocks, replacingBlocks, schematicLoaded, 17 | startedLoadingSchematic, noProviderForSchematicFileExtension, schematicLoadError, schematicNotFound, 18 | providerNotFound; 19 | 20 | 21 | protected BELocale(JsonObject json) { 22 | couldNotFindWandById = getString(json, "could_not_find_wand_by_id"); 23 | selectArea = getString(json, "select_area"); 24 | copiedSelectionToClipboard = getString(json, "copied_selection_to_clipboard"); 25 | selectionReset = getString(json, "selection_reset"); 26 | firstPositionSet = getString(json, "first_position_set"); 27 | secondPositionSet = getString(json, "second_position_set"); 28 | pastingClipboard = getString(json, "pasting_clipboard"); 29 | clipboardRotated = getString(json, "clipboard_rotated"); 30 | settingBlocks = getString(json, "setting_blocks"); 31 | replacingBlocks = getString(json, "replacing_blocks"); 32 | startedLoadingSchematic = getString(json, "started_loading_schematic"); 33 | schematicLoaded = getString(json, "schematic_loaded"); 34 | noProviderForSchematicFileExtension = getString(json, "no_provider_for_schematic_file_extension"); 35 | schematicLoadError = getString(json, "schematic_load_error"); 36 | schematicNotFound = getString(json, "schematic_not_found"); 37 | providerNotFound = getString(json, "provider_not_found"); 38 | } 39 | 40 | @Nullable 41 | private String getString(JsonObject jsonObject, String key) { 42 | if (jsonObject.has(key)) { 43 | return jsonObject.get(key).getAsString(); 44 | } else { 45 | return null; 46 | } 47 | } 48 | 49 | protected static void registerLocale(Locale locale, BELocale blockEditLocale) { 50 | locales.put(locale, blockEditLocale); 51 | } 52 | 53 | public static BELocale getLocale(@Nullable Locale locale) { 54 | if (locale == null) return locales.get(defaultLocale); 55 | BELocale beLocale = locales.get(locale); 56 | if (beLocale == null) beLocale = locales.get(defaultLocale); 57 | return beLocale; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/BEPlayer.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit; 2 | 3 | import me.youhavetrouble.blockedit.util.Clipboard; 4 | import me.youhavetrouble.blockedit.util.Selection; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.Location; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.util.Vector; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.HashMap; 12 | import java.util.UUID; 13 | 14 | public class BEPlayer { 15 | 16 | private static final HashMap playerHashMap = new HashMap<>(); 17 | private Selection selection; 18 | 19 | private Clipboard clipboard; 20 | private Location selectionPoint1, selectionPoint2; 21 | private final UUID playerUuid; 22 | 23 | public BEPlayer(Player player) { 24 | this.playerUuid = player.getUniqueId(); 25 | this.clipboard = new Clipboard(player.getLocation()); 26 | } 27 | 28 | public Player getPlayer() { 29 | return Bukkit.getPlayer(playerUuid); 30 | } 31 | 32 | public Selection getSelection() { 33 | return selection; 34 | } 35 | 36 | public Clipboard getClipboard() { 37 | return clipboard; 38 | } 39 | 40 | public void setClipboardFromSelection() { 41 | if (selection == null) throw new IllegalStateException("Selection is null"); 42 | // add every block between selection points to clipboard 43 | Clipboard clipboard = new Clipboard(getPlayer().getLocation().toBlockLocation()); 44 | for (int x = (int) selection.getMinX(); x <= selection.getMaxX(); x++) { 45 | for (int y = (int) selection.getMinY(); y <= selection.getMaxY(); y++) { 46 | for (int z = (int) selection.getMinZ(); z <= selection.getMaxZ(); z++) { 47 | Vector relativeLocation = new Vector(x, y, z).subtract(clipboard.getBaseLocationVector()); 48 | Location location = new Location(selectionPoint1.getWorld(), x, y, z); 49 | clipboard.addBlock(relativeLocation, location.getBlock().getState()); 50 | } 51 | } 52 | } 53 | setClipboard(clipboard); 54 | } 55 | 56 | public void setClipboard(@Nullable Clipboard clipboard) { 57 | if (clipboard == null) { 58 | this.clipboard.clear(); 59 | return; 60 | } 61 | this.clipboard = clipboard; 62 | } 63 | 64 | public void resetSelection() { 65 | this.selection = null; 66 | this.selectionPoint1 = null; 67 | this.selectionPoint2 = null; 68 | } 69 | 70 | private void updateSelection() { 71 | if (selectionPoint1 == null || selectionPoint2 == null) { 72 | selection = null; 73 | return; 74 | } 75 | if (selectionPoint1.getWorld() == null || selectionPoint2 == null) { 76 | selection = null; 77 | return; 78 | } 79 | if (!selectionPoint1.getWorld().equals(selectionPoint2.getWorld())) { 80 | selection = null; 81 | return; 82 | } 83 | 84 | selection = new Selection(selectionPoint1.toBlockLocation(), selectionPoint2.toBlockLocation(), selectionPoint1.getWorld().getUID()); 85 | } 86 | 87 | public void setSelectionPoint1(Location selectionPoint1) { 88 | if (this.selectionPoint1 != null && this.selectionPoint1.equals(selectionPoint1)) return; 89 | this.selectionPoint1 = selectionPoint1; 90 | updateSelection(); 91 | } 92 | 93 | public void setSelectionPoint2(Location selectionPoint2) { 94 | if (this.selectionPoint2 != null && this.selectionPoint2.equals(selectionPoint2)) return; 95 | this.selectionPoint2 = selectionPoint2; 96 | updateSelection(); 97 | } 98 | 99 | /** 100 | * @return UUID of a world within which the selection is made. 101 | */ 102 | public UUID getSelectionWorld() { 103 | if (selection == null) return null; 104 | return selectionPoint1.getWorld().getUID(); 105 | } 106 | 107 | /** 108 | * @return Clone of selectionPoint1 109 | */ 110 | public Location getSelectionPoint1() { 111 | if (selectionPoint1 == null) return null; 112 | return selectionPoint1.clone(); 113 | } 114 | 115 | /** 116 | * @return Clone of selectionPoint2 117 | */ 118 | public Location getSelectionPoint2() { 119 | if (selectionPoint2 == null) return null; 120 | return selectionPoint2.clone(); 121 | } 122 | 123 | protected static void addPlayer(Player player) { 124 | playerHashMap.put(player.getUniqueId(), new BEPlayer(player)); 125 | } 126 | 127 | protected static void removePlayer(Player player) { 128 | playerHashMap.remove(player.getUniqueId()); 129 | } 130 | 131 | public static BEPlayer getByPlayer(Player player) { 132 | return getByUuid(player.getUniqueId()); 133 | } 134 | 135 | protected static BEPlayer getByUuid(UUID uuid) { 136 | return playerHashMap.get(uuid); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/BlockEdit.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.stream.JsonReader; 6 | import me.youhavetrouble.blockedit.schematic.Schematic; 7 | import me.youhavetrouble.blockedit.wands.SelectionWand; 8 | import org.bukkit.plugin.java.JavaPlugin; 9 | import org.reflections.Reflections; 10 | import org.reflections.scanners.Scanners; 11 | 12 | import java.io.*; 13 | import java.util.*; 14 | import java.util.regex.Pattern; 15 | 16 | public final class BlockEdit extends JavaPlugin { 17 | 18 | private static BlockEdit plugin; 19 | private static SchematicHandler schematicHandler; 20 | private static WandsHandler wandsHandler; 21 | 22 | @Override 23 | public void onEnable() { 24 | plugin = this; 25 | 26 | if (!initLocales()) { 27 | plugin.getSLF4JLogger().error("Could not load locale files"); 28 | plugin.getServer().getPluginManager().disablePlugin(plugin); 29 | return; 30 | } 31 | 32 | getServer().getPluginManager().registerEvents(new JoinLeaveListener(), this); 33 | 34 | schematicHandler = new SchematicHandler<>(this); 35 | wandsHandler = new WandsHandler(this); 36 | 37 | SelectionWand selectionWand = new SelectionWand(); 38 | wandsHandler.registerWand(selectionWand); 39 | getServer().getPluginManager().registerEvents(selectionWand, this); 40 | 41 | BlockEditCommands.registerCommands(this); 42 | } 43 | 44 | public static BlockEdit getPlugin() { 45 | return plugin; 46 | } 47 | 48 | public static SchematicHandler getSchematicHandler() { 49 | return schematicHandler; 50 | } 51 | 52 | public static WandsHandler getWandsHandler() { 53 | return wandsHandler; 54 | } 55 | 56 | private boolean initLocales() { 57 | Reflections reflections = new Reflections("locale", Scanners.Resources); 58 | Set fileNames = reflections.getResources(Pattern.compile("([a-zA-Z]{1,3}_[a-zA-Z]{1,3})(\\.json)")); 59 | 60 | Gson gson = new Gson(); 61 | for (String fileName : fileNames) { 62 | Locale locale; 63 | try { 64 | String localeString = fileName 65 | .replace(".json", "") 66 | .replace("locale/", ""); 67 | String[] localeStringSplit = localeString.split("_"); 68 | if (localeStringSplit.length != 2) { 69 | throw new IllegalArgumentException("Invalid locale file name: " + fileName); 70 | } 71 | locale = Locale.of(localeStringSplit[0], localeStringSplit[1].toUpperCase()); 72 | } catch (IllegalArgumentException e) { 73 | plugin.getSLF4JLogger().error("Invalid locale file name: {}", fileName); 74 | continue; 75 | } 76 | String resourcePath = "/" + fileName; 77 | try (InputStream fileStream = BlockEdit.class.getResourceAsStream(resourcePath)) { 78 | if (fileStream == null) { 79 | plugin.getSLF4JLogger().error("Locale file not found: {}", resourcePath); 80 | continue; 81 | } 82 | JsonReader reader = new JsonReader(new InputStreamReader(fileStream)); 83 | JsonObject json = gson.fromJson(reader, JsonObject.class); 84 | BELocale beLocale = new BELocale(json); 85 | BELocale.registerLocale(locale, beLocale); 86 | } catch (IOException e) { 87 | plugin.getSLF4JLogger().error("Error reading locale file: {}", resourcePath, e); 88 | } 89 | } 90 | return true; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/BlockEditCommands.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit; 2 | 3 | import com.mojang.brigadier.Command; 4 | import com.mojang.brigadier.arguments.DoubleArgumentType; 5 | import com.mojang.brigadier.arguments.IntegerArgumentType; 6 | import com.mojang.brigadier.arguments.StringArgumentType; 7 | import com.mojang.brigadier.tree.LiteralCommandNode; 8 | import io.papermc.paper.command.brigadier.CommandSourceStack; 9 | import io.papermc.paper.command.brigadier.Commands; 10 | import io.papermc.paper.command.brigadier.argument.ArgumentTypes; 11 | import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver; 12 | import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; 13 | import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; 14 | import me.youhavetrouble.blockedit.api.BlockEditAPI; 15 | import me.youhavetrouble.blockedit.commands.arguments.SchematicProviderArgument; 16 | import me.youhavetrouble.blockedit.exception.SchematicLoadException; 17 | import me.youhavetrouble.blockedit.operations.PasteOperation; 18 | import me.youhavetrouble.blockedit.operations.ReplaceOperation; 19 | import me.youhavetrouble.blockedit.operations.SetOperation; 20 | import me.youhavetrouble.blockedit.schematic.Schematic; 21 | import me.youhavetrouble.blockedit.schematic.SchematicProvider; 22 | import me.youhavetrouble.blockedit.util.Selection; 23 | import net.kyori.adventure.text.Component; 24 | import net.kyori.adventure.text.format.NamedTextColor; 25 | import org.bukkit.Location; 26 | import org.bukkit.block.BlockState; 27 | import org.bukkit.entity.Player; 28 | import org.bukkit.inventory.ItemStack; 29 | import org.bukkit.plugin.Plugin; 30 | import org.bukkit.util.Vector; 31 | import org.jetbrains.annotations.NotNull; 32 | import org.jetbrains.annotations.Nullable; 33 | 34 | import java.util.HashMap; 35 | import java.util.concurrent.CompletableFuture; 36 | 37 | @SuppressWarnings("UnstableApiUsage") 38 | public class BlockEditCommands { 39 | 40 | protected static void registerCommands(@NotNull BlockEdit plugin) { 41 | LifecycleEventManager<@NotNull Plugin> manager = plugin.getLifecycleManager(); 42 | manager.registerEventHandler(LifecycleEvents.COMMANDS, event -> { 43 | final Commands commands = event.registrar(); 44 | 45 | commands.register( 46 | wandCommand(), 47 | "Gives the player chosen wand" 48 | ); 49 | 50 | commands.register( 51 | copyCommand(), 52 | "Copies the selected area to the clipboard" 53 | ); 54 | 55 | commands.register( 56 | pasteCommand(), 57 | "Pastes the clipboard at the player's location" 58 | ); 59 | 60 | commands.register( 61 | deselCommand(), 62 | "Resets the player's selection" 63 | ); 64 | 65 | commands.register( 66 | pos1Command(), 67 | "Sets the first point of the player's selection" 68 | ); 69 | 70 | commands.register( 71 | pos2Command(), 72 | "Sets the second point of the player's selection" 73 | ); 74 | 75 | commands.register( 76 | rotateCommand(), 77 | "Rotates the clipboard by the specified angle" 78 | ); 79 | 80 | commands.register( 81 | setCommand(), 82 | "Sets the selected area to the specified block" 83 | ); 84 | 85 | commands.register( 86 | replaceCommand(), 87 | "Replaces the specified block with another block" 88 | ); 89 | 90 | commands.register( 91 | schematicCommand(), 92 | "Loads a schematic" 93 | ); 94 | 95 | }); 96 | } 97 | 98 | private static LiteralCommandNode wandCommand() { 99 | return Commands.literal("wand") 100 | .requires(css -> { 101 | if (!(css.getSender() instanceof Player player)) return false; 102 | return player.hasPermission("blockedit.command.wand"); 103 | }) 104 | .then( 105 | Commands.argument("wand_id", StringArgumentType.word()) 106 | .suggests((ctx, builder) -> { 107 | String[] inputArgs = ctx.getInput().split(" "); 108 | String lastArg = inputArgs[inputArgs.length - 1]; 109 | for (String id : BlockEditAPI.getWandsHandler().getWandIds()) { 110 | if (inputArgs.length != 1) continue; 111 | if (id.startsWith(lastArg)) continue; 112 | builder.suggest(id); 113 | } 114 | return builder.buildFuture(); 115 | }) 116 | .executes(ctx -> { 117 | Player player = (Player) ctx.getSource().getSender(); 118 | String wandId = ctx.getArgument("wand_id", String.class); 119 | ItemStack wand = BlockEditAPI.getWandsHandler().getWand(wandId); 120 | if (wand == null) { 121 | BlockEdit.getPlugin().getSLF4JLogger().info(String.valueOf(player.locale())); 122 | ctx.getSource().getSender().sendMessage(Component.text(BELocale.getLocale(player.locale()).couldNotFindWandById.formatted(wandId), NamedTextColor.RED)); 123 | return Command.SINGLE_SUCCESS; 124 | } 125 | player.getInventory().addItem(wand); 126 | return Command.SINGLE_SUCCESS; 127 | }) 128 | ) 129 | .executes(ctx -> { 130 | Player player = (Player) ctx.getSource().getSender(); 131 | ItemStack wand = BlockEditAPI.getWandsHandler().getWand("select"); 132 | player.getInventory().addItem(wand); 133 | return Command.SINGLE_SUCCESS; 134 | }) 135 | .build(); 136 | } 137 | 138 | private static LiteralCommandNode copyCommand() { 139 | return Commands.literal("copy") 140 | .requires(css -> { 141 | if (!(css.getSender() instanceof Player player)) return false; 142 | return player.hasPermission("blockedit.command.copy"); 143 | }) 144 | .executes(ctx -> { 145 | Player player = (Player) ctx.getSource().getSender(); 146 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 147 | try { 148 | bePlayer.setClipboardFromSelection(); 149 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).copiedSelectionToClipboard, NamedTextColor.GRAY)); 150 | } catch (IllegalStateException e) { 151 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).selectArea, NamedTextColor.RED)); 152 | } 153 | return Command.SINGLE_SUCCESS; 154 | }) 155 | .build(); 156 | } 157 | 158 | private static LiteralCommandNode deselCommand() { 159 | return Commands.literal("desel") 160 | .requires(css -> { 161 | if (!(css.getSender() instanceof Player player)) return false; 162 | return player.hasPermission("blockedit.command.desel"); 163 | }) 164 | .executes(ctx -> { 165 | Player player = (Player) ctx.getSource().getSender(); 166 | BEPlayer.getByPlayer(player).resetSelection(); 167 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).selectionReset, NamedTextColor.GRAY)); 168 | return Command.SINGLE_SUCCESS; 169 | }) 170 | .build(); 171 | } 172 | 173 | private static LiteralCommandNode pos1Command() { 174 | return Commands.literal("pos1") 175 | .requires(css -> { 176 | if (!(css.getSender() instanceof Player player)) return false; 177 | return player.hasPermission("blockedit.command.pos"); 178 | }) 179 | .executes(ctx -> { 180 | Player player = (Player) ctx.getSource().getSender(); 181 | Location location = player.getLocation().toBlockLocation(); 182 | BEPlayer.getByPlayer(player).setSelectionPoint1(location); 183 | String locationString = "X: " + location.blockX() + " Y: " + location.blockY() + " Z: " + location.blockZ(); 184 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).firstPositionSet.formatted(locationString), NamedTextColor.GRAY)); 185 | return Command.SINGLE_SUCCESS; 186 | }) 187 | .then(Commands.argument("position", ArgumentTypes.blockPosition()) 188 | .executes(ctx -> { 189 | Player player = (Player) ctx.getSource().getSender(); 190 | BlockPositionResolver blockPositionResolver = ctx.getArgument("position", BlockPositionResolver.class); 191 | Location location = blockPositionResolver.resolve(ctx.getSource()).toLocation(player.getWorld()); 192 | BEPlayer.getByPlayer(player).setSelectionPoint1(location); 193 | String locationString = "X: " + location.blockX() + " Y: " + location.blockY() + " Z: " + location.blockZ(); 194 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).firstPositionSet.formatted(locationString), NamedTextColor.GRAY)); 195 | return Command.SINGLE_SUCCESS; 196 | })) 197 | .build(); 198 | } 199 | 200 | private static LiteralCommandNode pos2Command() { 201 | return Commands.literal("pos2") 202 | .requires(css -> { 203 | if (!(css.getSender() instanceof Player player)) return false; 204 | return player.hasPermission("blockedit.command.pos"); 205 | }) 206 | .executes(ctx -> { 207 | Player player = (Player) ctx.getSource().getSender(); 208 | Location location = player.getLocation().toBlockLocation(); 209 | BEPlayer.getByPlayer(player).setSelectionPoint2(location); 210 | String locationString = "X: " + location.blockX() + " Y: " + location.blockY() + " Z: " + location.blockZ(); 211 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).secondPositionSet.formatted(locationString), NamedTextColor.GRAY)); 212 | return Command.SINGLE_SUCCESS; 213 | }) 214 | .then(Commands.argument("position", ArgumentTypes.blockPosition()) 215 | .executes(ctx -> { 216 | Player player = (Player) ctx.getSource().getSender(); 217 | BlockPositionResolver blockPositionResolver = ctx.getArgument("position", BlockPositionResolver.class); 218 | Location location = blockPositionResolver.resolve(ctx.getSource()).toLocation(player.getWorld()); 219 | BEPlayer.getByPlayer(player).setSelectionPoint2(location); 220 | String locationString = "X: " + location.blockX() + " Y: " + location.blockY() + " Z: " + location.blockZ(); 221 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).secondPositionSet.formatted(locationString), NamedTextColor.GRAY)); 222 | return Command.SINGLE_SUCCESS; 223 | })) 224 | .build(); 225 | } 226 | 227 | private static LiteralCommandNode pasteCommand() { 228 | return Commands.literal("paste") 229 | .requires(css -> { 230 | if (!(css.getSender() instanceof Player player)) return false; 231 | return player.hasPermission("blockedit.command.paste"); 232 | }) 233 | .executes(ctx -> { 234 | Player player = (Player) ctx.getSource().getSender(); 235 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 236 | Vector playerLocationVector = player.getLocation().toBlockLocation().toVector(); 237 | 238 | HashMap absoluteBlocks = new HashMap<>(bePlayer.getClipboard().getBlocks().size()); 239 | 240 | bePlayer.getClipboard().getBlocks().forEach((vector, blockState) -> { 241 | Vector absolutePosition = vector.clone().add(playerLocationVector); 242 | absoluteBlocks.put(absolutePosition, blockState); 243 | }); 244 | 245 | Selection selection = Selection.fromClipboard(absoluteBlocks.keySet(), player.getWorld()); 246 | BlockEditAPI.runOperation(selection, 1, new PasteOperation(absoluteBlocks)); 247 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).pastingClipboard, NamedTextColor.GRAY)); 248 | 249 | return Command.SINGLE_SUCCESS; 250 | }) 251 | .then( 252 | Commands.argument("chunks_per_tick", IntegerArgumentType.integer(1)) 253 | .executes(ctx -> { 254 | Player player = (Player) ctx.getSource().getSender(); 255 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 256 | Vector playerLocationVector = player.getLocation().toBlockLocation().toVector(); 257 | 258 | HashMap absoluteBlocks = new HashMap<>(bePlayer.getClipboard().getBlocks().size()); 259 | 260 | int chunksPerTick = ctx.getArgument("chunks_per_tick", Integer.class); 261 | 262 | bePlayer.getClipboard().getBlocks().forEach((vector, blockState) -> { 263 | Vector absolutePosition = vector.clone().add(playerLocationVector); 264 | absoluteBlocks.put(absolutePosition, blockState); 265 | }); 266 | 267 | Selection selection = Selection.fromClipboard(absoluteBlocks.keySet(), player.getWorld()); 268 | BlockEditAPI.runOperation(selection, chunksPerTick, new PasteOperation(absoluteBlocks)); 269 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).pastingClipboard, NamedTextColor.GRAY)); 270 | 271 | return Command.SINGLE_SUCCESS; 272 | }) 273 | ) 274 | .build(); 275 | } 276 | 277 | private static LiteralCommandNode rotateCommand() { 278 | return Commands.literal("rotate") 279 | .requires(css -> { 280 | if (!(css.getSender() instanceof Player player)) return false; 281 | return player.hasPermission("blockedit.command.rotate"); 282 | }) 283 | .then( 284 | Commands.argument("angle", DoubleArgumentType.doubleArg(-360, 360)) 285 | .executes(ctx -> { 286 | Player player = (Player) ctx.getSource().getSender(); 287 | double angle = ctx.getArgument("angle", Double.class); 288 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 289 | bePlayer.getClipboard().rotate(angle); 290 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).clipboardRotated.formatted(angle), NamedTextColor.GRAY)); 291 | return Command.SINGLE_SUCCESS; 292 | }) 293 | ) 294 | .build(); 295 | } 296 | 297 | private static LiteralCommandNode setCommand() { 298 | return Commands.literal("set") 299 | .requires(css -> { 300 | if (!(css.getSender() instanceof Player player)) return false; 301 | return player.hasPermission("blockedit.command.set"); 302 | }) 303 | .then( 304 | Commands.argument("block", ArgumentTypes.blockState()) 305 | .executes(ctx -> { 306 | Player player = (Player) ctx.getSource().getSender(); 307 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 308 | BlockState blockState = ctx.getArgument("block", BlockState.class); 309 | Selection selection = bePlayer.getSelection(); 310 | if (selection == null) { 311 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).selectArea, NamedTextColor.RED)); 312 | return Command.SINGLE_SUCCESS; 313 | } 314 | BlockEditAPI.runOperation(selection, 1, new SetOperation(blockState)); 315 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).settingBlocks, NamedTextColor.GRAY)); 316 | return Command.SINGLE_SUCCESS; 317 | }) 318 | .then( 319 | Commands.argument("chunks_per_tick", IntegerArgumentType.integer(1)) 320 | .executes(ctx -> { 321 | Player player = (Player) ctx.getSource().getSender(); 322 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 323 | BlockState blockState = ctx.getArgument("block", BlockState.class); 324 | int chunksPerTick = ctx.getArgument("chunks_per_tick", Integer.class); 325 | Selection selection = bePlayer.getSelection(); 326 | if (selection == null) { 327 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).selectArea, NamedTextColor.RED)); 328 | return Command.SINGLE_SUCCESS; 329 | } 330 | BlockEditAPI.runOperation(selection, chunksPerTick, new SetOperation(blockState)); 331 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).settingBlocks, NamedTextColor.GRAY)); 332 | return Command.SINGLE_SUCCESS; 333 | })) 334 | ) 335 | 336 | .build(); 337 | } 338 | 339 | private static LiteralCommandNode replaceCommand() { 340 | return Commands.literal("replace") 341 | .requires(css -> { 342 | if (!(css.getSender() instanceof Player player)) return false; 343 | return player.hasPermission("blockedit.command.replace"); 344 | }) 345 | .then( 346 | Commands.argument("to_replace", ArgumentTypes.blockState()) 347 | .then( 348 | Commands.argument("replace_with", ArgumentTypes.blockState()) 349 | .executes(ctx -> { 350 | Player player = (Player) ctx.getSource().getSender(); 351 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 352 | BlockState toReplace = ctx.getArgument("to_replace", BlockState.class); 353 | BlockState replaceWith = ctx.getArgument("replace_with", BlockState.class); 354 | Selection selection = bePlayer.getSelection(); 355 | if (selection == null) { 356 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).selectArea, NamedTextColor.RED)); 357 | return Command.SINGLE_SUCCESS; 358 | } 359 | BlockEditAPI.runOperation(selection, 1, new ReplaceOperation(toReplace, replaceWith)); 360 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).replacingBlocks, NamedTextColor.GRAY)); 361 | return Command.SINGLE_SUCCESS; 362 | }) 363 | .then( 364 | Commands.argument("chunks_per_tick", IntegerArgumentType.integer(1)) 365 | .executes(ctx -> { 366 | Player player = (Player) ctx.getSource().getSender(); 367 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 368 | BlockState toReplace = ctx.getArgument("to_replace", BlockState.class); 369 | BlockState replaceWith = ctx.getArgument("replace_with", BlockState.class); 370 | int chunksPerTick = ctx.getArgument("chunks_per_tick", Integer.class); 371 | Selection selection = bePlayer.getSelection(); 372 | if (selection == null) { 373 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).selectArea, NamedTextColor.RED)); 374 | return Command.SINGLE_SUCCESS; 375 | } 376 | BlockEditAPI.runOperation(selection, chunksPerTick, new ReplaceOperation(toReplace, replaceWith)); 377 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).replacingBlocks, NamedTextColor.GRAY)); 378 | return Command.SINGLE_SUCCESS; 379 | }) 380 | ) 381 | ) 382 | ) 383 | 384 | .build(); 385 | } 386 | 387 | private static LiteralCommandNode schematicCommand() { 388 | return Commands.literal("schematic") 389 | .requires(css -> { 390 | if (!(css.getSender() instanceof Player player)) return false; 391 | return player.hasPermission("blockedit.command.schematic"); 392 | }) 393 | .then(Commands.literal("load") 394 | .then(Commands.literal("file") 395 | .then(Commands.argument("schematic_name", StringArgumentType.word()) 396 | .requires(commandSourceStack -> { 397 | if (!(commandSourceStack.getSender() instanceof Player player)) 398 | return false; 399 | return player.hasPermission("blockedit.command.schematic.load"); 400 | }) 401 | .executes(context -> { 402 | Player player = (Player) context.getSource().getSender(); 403 | loadSchematic( 404 | player, 405 | null, // file provider 406 | context.getArgument("schematic_name", String.class) 407 | ); 408 | return Command.SINGLE_SUCCESS; 409 | }) 410 | ) 411 | ) 412 | .then(Commands.argument("provider_name", new SchematicProviderArgument(BlockEdit.getSchematicHandler())) 413 | .then(Commands.argument("schematic_name", StringArgumentType.word()) 414 | .requires(commandSourceStack -> { 415 | if (!(commandSourceStack.getSender() instanceof Player player)) 416 | return false; 417 | return player.hasPermission("blockedit.command.schematic.load"); 418 | }) 419 | .executes(context -> { 420 | Player player = (Player) context.getSource().getSender(); 421 | loadSchematic( 422 | player, 423 | context.getArgument("provider_name", SchematicProvider.class), 424 | context.getArgument("schematic_name", String.class) 425 | ); 426 | return Command.SINGLE_SUCCESS; 427 | }) 428 | ) 429 | ) 430 | ) 431 | .build(); 432 | } 433 | 434 | private static void loadSchematic(@NotNull Player player, @Nullable SchematicProvider provider, @NotNull String schematicName) { 435 | CompletableFuture.runAsync(() -> { 436 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).startedLoadingSchematic.formatted(schematicName), NamedTextColor.GRAY)); 437 | Schematic schematic; 438 | 439 | if (provider == null) { 440 | try { 441 | schematic = BlockEditAPI.getSchematicHandler().loadSchematic(schematicName); 442 | } catch (SchematicLoadException e) { 443 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).schematicLoadError.formatted(schematicName), NamedTextColor.RED)); 444 | BlockEdit.getPlugin().getSLF4JLogger().error("Could not load schematic {} due to provider error ", schematicName, e); 445 | return; 446 | } 447 | } else { 448 | try { 449 | schematic = provider.load(schematicName); 450 | } catch (SchematicLoadException e) { 451 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).schematicLoadError.formatted(schematicName), NamedTextColor.RED)); 452 | BlockEdit.getPlugin().getSLF4JLogger().error("Could not load schematic {} due to provider error ", schematicName, e); 453 | return; 454 | } 455 | } 456 | 457 | if (schematic == null) { 458 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).schematicNotFound.formatted(schematicName), NamedTextColor.RED)); 459 | return; 460 | } 461 | 462 | BEPlayer bePlayer = BEPlayer.getByPlayer(player); 463 | bePlayer.setClipboard(schematic.asClipboard()); 464 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).schematicLoaded.formatted(schematicName), NamedTextColor.GRAY)); 465 | }); 466 | } 467 | 468 | } 469 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/JoinLeaveListener.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit; 2 | 3 | import org.bukkit.event.EventHandler; 4 | import org.bukkit.event.EventPriority; 5 | import org.bukkit.event.Listener; 6 | import org.bukkit.event.player.PlayerJoinEvent; 7 | import org.bukkit.event.player.PlayerQuitEvent; 8 | 9 | public class JoinLeaveListener implements Listener { 10 | 11 | @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) 12 | public void onPlayerJoin(PlayerJoinEvent event) { 13 | BEPlayer.addPlayer(event.getPlayer()); 14 | } 15 | 16 | @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) 17 | public void onPlayerQuit(PlayerQuitEvent event) { 18 | BEPlayer.removePlayer(event.getPlayer()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/SchematicHandler.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit; 2 | 3 | import me.youhavetrouble.blockedit.exception.NoProviderForSchematicFileExtensionException; 4 | import me.youhavetrouble.blockedit.exception.SchematicHandlerRegistrationException; 5 | import me.youhavetrouble.blockedit.exception.SchematicLoadException; 6 | import me.youhavetrouble.blockedit.exception.SchematicSaveException; 7 | import me.youhavetrouble.blockedit.schematic.FileSchematicProvider; 8 | import me.youhavetrouble.blockedit.schematic.Schematic; 9 | import me.youhavetrouble.blockedit.schematic.SchematicProvider; 10 | import me.youhavetrouble.blockedit.util.Clipboard; 11 | import me.youhavetrouble.blockedit.util.NameFilenameFilter; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | 15 | import java.io.*; 16 | import java.util.Collection; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | 20 | public class SchematicHandler { 21 | 22 | private final BlockEdit plugin; 23 | 24 | private final Map> schematicProvidersByExtension = new HashMap<>(); 25 | private final Map> schematicProvidersByName = new HashMap<>(); 26 | 27 | public final File schematicsDirectory; 28 | 29 | protected SchematicHandler(BlockEdit plugin) { 30 | this.plugin = plugin; 31 | this.schematicsDirectory = new File(plugin.getDataFolder(), "schematics"); 32 | createSchematicsDirectory(); 33 | } 34 | 35 | private void createSchematicsDirectory() { 36 | try { 37 | this.schematicsDirectory.mkdirs(); 38 | } catch (SecurityException e) { 39 | plugin.getLogger().warning("Could not create schematics directory. Make sure server has read/write access to the plugin folder."); 40 | } 41 | } 42 | 43 | public Collection getSchematicProvidersList() { 44 | return schematicProvidersByName.keySet(); 45 | } 46 | 47 | public SchematicProvider getSchematicProviderByName(String name) { 48 | return schematicProvidersByName.get(name); 49 | } 50 | 51 | public void registerSchematicProvider( 52 | @NotNull SchematicProvider schematicProvider 53 | ) throws SchematicHandlerRegistrationException { 54 | 55 | String schematicProviderName = schematicProvider.name().trim(); 56 | 57 | if (!schematicProviderName.matches("^[a-z0-9]+$")) { 58 | throw new SchematicHandlerRegistrationException("Schematic provider name can only contain lowercase letters and numbers", schematicProvider); 59 | } 60 | 61 | if (schematicProviderName.equals("file")) { 62 | throw new SchematicHandlerRegistrationException("Schematic provider name cannot be 'file'", schematicProvider); 63 | } 64 | 65 | if (schematicProvidersByName.containsKey(schematicProviderName)) { 66 | throw new SchematicHandlerRegistrationException("Schematic provider with name " + schematicProvider.name() + " is already registered", schematicProvider); 67 | } 68 | 69 | // Register the provider as a file provider that associates the provider with file extensions 70 | if (schematicProvider instanceof FileSchematicProvider fileSchematicProvider) { 71 | // Loop extensions to verify if they're valid and not already registered 72 | for (String extension : fileSchematicProvider.fileExtensions()) { 73 | String trimmedExtension = extension.trim(); 74 | if (trimmedExtension.isEmpty()) throw new SchematicHandlerRegistrationException("File extension cannot be empty", fileSchematicProvider); 75 | if (!trimmedExtension.matches("^[a-z0-9]+$")) throw new SchematicHandlerRegistrationException("File extension can only contain lowercase letters and numbers", fileSchematicProvider); 76 | if (schematicProvidersByExtension.containsKey(trimmedExtension)) { 77 | throw new SchematicHandlerRegistrationException("File extension " + trimmedExtension + " is already registered to " + schematicProvidersByExtension.get(trimmedExtension).name(), fileSchematicProvider); 78 | } 79 | } 80 | // Loop again to actually register the extensions 81 | for (String extension : fileSchematicProvider.fileExtensions()) { 82 | String trimmedExtension = extension.trim(); 83 | schematicProvidersByExtension.put(trimmedExtension, schematicProvider); 84 | } 85 | return; 86 | } 87 | 88 | schematicProvidersByName.put(schematicProvider.name(), schematicProvider); 89 | } 90 | 91 | /** 92 | * Loads a schematic from the schematic provider 93 | * @param providerName Schematic provider to use 94 | * @param schematicName Name of the schematic 95 | * @return Clipboard object containing the schematic. Null if schematic does not exist or could not be loaded. 96 | */ 97 | public @Nullable Schematic loadSchematic(@NotNull String providerName, @NotNull String schematicName) { 98 | 99 | // file provider is a special case here 100 | if (providerName.equals("file")) { 101 | return loadSchematic(schematicName); 102 | } 103 | 104 | SchematicProvider schematicProvider = schematicProvidersByName.get(providerName); 105 | if (schematicProvider == null) { 106 | throw new IllegalArgumentException("Schematic provider " + providerName + " is not registered"); 107 | } 108 | return schematicProvider.load(schematicName); 109 | } 110 | 111 | /** 112 | * Loads a schematic from the schematic directory. File schematic provider will be matched by the file extenstion 113 | * @param schematicName File name without extension 114 | * @return Schematic object 115 | */ 116 | public @Nullable Schematic loadSchematic(@NotNull String schematicName) throws SchematicLoadException { 117 | File[] files; 118 | try { 119 | files = this.schematicsDirectory.listFiles(new NameFilenameFilter(schematicName)); 120 | if (files == null || files.length == 0) return null; 121 | } catch (SecurityException e) { 122 | throw new SchematicLoadException("Could not list schematics directory", schematicName, e); 123 | } 124 | 125 | File schematicFile = files[0]; 126 | String fileName = schematicFile.getName(); 127 | String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); 128 | SchematicProvider schematicProvider = schematicProvidersByExtension.get(fileExtension); 129 | if (schematicProvider == null) { 130 | throw new NoProviderForSchematicFileExtensionException("No schematic provider found for file extension " + fileExtension, schematicName, fileExtension); 131 | } 132 | return schematicProvider.load(schematicName); 133 | } 134 | 135 | /** 136 | * Saves a schematic to the schematics directory 137 | * @param schematicName Name of the schematic 138 | * @param clipboard Clipboard object containing the schematic 139 | */ 140 | public void saveSchematic( 141 | @NotNull String schematicName, 142 | @NotNull String providerName, 143 | @NotNull Clipboard clipboard 144 | ) throws SchematicSaveException { 145 | SchematicProvider schematicProvider = schematicProvidersByName.get(providerName); 146 | if (schematicProvider == null) { 147 | throw new SchematicSaveException("Schematic provider " + providerName + " is not registered", schematicName); 148 | } 149 | S schematic = schematicProvider.fromClipboard(schematicName, clipboard); 150 | schematicProvider.save(schematic); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/WandsHandler.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import me.youhavetrouble.blockedit.api.BlockEditWand; 5 | import org.bukkit.Material; 6 | import org.bukkit.NamespacedKey; 7 | import org.bukkit.inventory.ItemStack; 8 | import org.bukkit.inventory.meta.ItemMeta; 9 | import org.bukkit.persistence.PersistentDataType; 10 | 11 | import java.util.Collection; 12 | import java.util.HashMap; 13 | 14 | public class WandsHandler { 15 | 16 | private final NamespacedKey wandKey; 17 | private static final HashMap wands = new HashMap<>(); 18 | 19 | protected WandsHandler(BlockEdit plugin) { 20 | wandKey = new NamespacedKey(plugin, "wand"); 21 | 22 | } 23 | 24 | /** 25 | * Gets wand id from ItemStack 26 | * @param itemStack ItemStack to check 27 | * @return WandId if Itemstack is a wand, null otherwise 28 | */ 29 | public String getWandId(ItemStack itemStack) { 30 | if (itemStack == null) return null; 31 | if (itemStack.getItemMeta() == null) return null; 32 | if (!itemStack.getItemMeta().getPersistentDataContainer().has(wandKey, PersistentDataType.STRING)) return null; 33 | return itemStack.getItemMeta().getPersistentDataContainer().get(wandKey, PersistentDataType.STRING); 34 | } 35 | 36 | /** 37 | * @return Immutable set of registered wand IDs 38 | */ 39 | public Collection getWandIds() { 40 | return ImmutableSet.copyOf(wands.keySet()); 41 | } 42 | 43 | /** 44 | * Gets wand ItemStack from wand id 45 | * @param wandId Wand id 46 | * @return Wand ItemStack if wand with given ID exists, null otherwise 47 | */ 48 | public ItemStack getWand(String wandId) { 49 | BlockEditWand wand = wands.get(wandId); 50 | if (wand == null) return null; 51 | ItemStack itemStack = new ItemStack(Material.WOODEN_AXE); 52 | ItemMeta meta = itemStack.getItemMeta(); 53 | if (wand.getName() != null) 54 | meta.displayName(wand.getName()); 55 | if (wand.getCustomModelData() != 0) 56 | meta.setCustomModelData(wand.getCustomModelData()); 57 | meta.getPersistentDataContainer().set(wandKey, PersistentDataType.STRING, wandId); 58 | itemStack.setItemMeta(meta); 59 | return itemStack; 60 | } 61 | 62 | 63 | /** 64 | * PSA: Wand IDs will get converted to lowercase. 65 | * @return true if registered successfully, false if not 66 | */ 67 | public boolean registerWand(BlockEditWand wand) { 68 | if (wands.containsKey(wand.getId().toLowerCase())) { 69 | BlockEdit.getPlugin().getLogger().warning("Tried to register wand with id \""+wand.getId()+"\", but wand with that id already exists!"); 70 | return false; 71 | } 72 | wands.put(wand.getId().toLowerCase(), wand); 73 | return true; 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/api/BlockEditAPI.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.api; 2 | 3 | import me.youhavetrouble.blockedit.BlockEdit; 4 | import me.youhavetrouble.blockedit.SchematicHandler; 5 | import me.youhavetrouble.blockedit.WandsHandler; 6 | import me.youhavetrouble.blockedit.schematic.Schematic; 7 | import me.youhavetrouble.blockedit.util.ChunkWork; 8 | import me.youhavetrouble.blockedit.util.Selection; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.Set; 12 | 13 | public class BlockEditAPI { 14 | 15 | /** 16 | * Runs an operation on the given selection with the given amount of chunks per tick 17 | * @param selection The area that will be operated on 18 | * @param chunksPerTick Amount of chunks per tick to modify 19 | * @param operation Operation to execute 20 | */ 21 | public static OperationWork runOperation( 22 | @NotNull Selection selection, 23 | int chunksPerTick, 24 | @NotNull BlockEditOperation operation 25 | ) { 26 | Set work = WorkSplitter.getOperatedOnChunks(selection); 27 | return WorkSplitter.runOperation(work, selection, chunksPerTick, operation); 28 | } 29 | 30 | /** 31 | * Gets the wands handler object that can be used to work with wands 32 | * @return Wands handler 33 | */ 34 | public static WandsHandler getWandsHandler() { 35 | return BlockEdit.getWandsHandler(); 36 | } 37 | 38 | /** 39 | * Gets schematic handler object that can be used to work with schematics 40 | * @return Schematic handler 41 | */ 42 | public static SchematicHandler getSchematicHandler() { 43 | return BlockEdit.getSchematicHandler(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/api/BlockEditOperation.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.api; 2 | 3 | import me.youhavetrouble.blockedit.util.Selection; 4 | import org.bukkit.block.Block; 5 | 6 | /** 7 | * This interface is used to define a BlockEdit operation.
8 | * Implement this interface and pass it to {@link BlockEditAPI#runOperation(Selection, int, BlockEditOperation)} to run it. 9 | */ 10 | public interface BlockEditOperation { 11 | 12 | /** 13 | * This function will run for every block in the selection it is executed on. 14 | * @param block Current block. 15 | */ 16 | void transformBlock(Block block); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/api/BlockEditWand.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.api; 2 | 3 | 4 | import net.kyori.adventure.text.Component; 5 | 6 | public interface BlockEditWand { 7 | 8 | /** 9 | * A unique id to identify the wand. Also used in /wand command. 10 | */ 11 | String getId(); 12 | 13 | /** 14 | * Name of the wand that will be used as wand item name. 15 | */ 16 | Component getName(); 17 | 18 | /** 19 | * Custom model data for the wand item. Set to 0 to not give the wand custom model data. 20 | */ 21 | int getCustomModelData(); 22 | 23 | /** 24 | * Permission for the wand usage 25 | */ 26 | String getPermission(); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/api/OperationWork.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.api; 2 | 3 | import me.youhavetrouble.blockedit.BlockEdit; 4 | import me.youhavetrouble.blockedit.util.ChunkWork; 5 | import me.youhavetrouble.blockedit.util.Selection; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.World; 8 | import org.bukkit.block.Block; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Set; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | public class OperationWork { 16 | 17 | private boolean started, finished = false; 18 | private final List chunkWorkList = new ArrayList<>(); 19 | private final Selection selection; 20 | private final int chunksPerTick; 21 | private final BlockEditOperation operation; 22 | private final AtomicInteger chunksLeft; 23 | private final AtomicInteger chunksProcessed = new AtomicInteger(0); 24 | 25 | protected OperationWork( 26 | Set chunkWorks, 27 | Selection selection, 28 | int chunksPerTick, 29 | BlockEditOperation operation 30 | ) { 31 | this.chunkWorkList.addAll(chunkWorks); 32 | this.chunksLeft = new AtomicInteger(this.chunkWorkList.size() - 1); 33 | this.selection = new Selection(selection.clone().expand(0.1), selection.getWorldUuid()); 34 | this.chunksPerTick = chunksPerTick; 35 | this.operation = operation; 36 | } 37 | 38 | /** 39 | * Starts the operation. This will start processing chunks in the background. 40 | * This method can only be called once. 41 | * 42 | * @throws IllegalStateException if called when the operation has already been started 43 | */ 44 | protected void start() { 45 | if (started) throw new IllegalStateException("Operation already started"); 46 | started = true; 47 | Bukkit.getGlobalRegionScheduler().runAtFixedRate(BlockEdit.getPlugin(), (task -> { 48 | if (chunksLeft.get() < 0) { 49 | this.finished = true; 50 | task.cancel(); 51 | return; 52 | } 53 | for (int i = 0; i < chunksPerTick; i++) { 54 | int chunkWorkIndex = chunksLeft.getAndDecrement(); 55 | if (chunkWorkIndex < 0) { 56 | this.finished = true; 57 | task.cancel(); 58 | return; 59 | } 60 | ChunkWork chunkWork = chunkWorkList.get(chunkWorkIndex); 61 | World world = selection.getWorld(); 62 | if (world == null) return; 63 | Bukkit.getRegionScheduler().execute(BlockEdit.getPlugin(), selection.getWorld(), chunkWork.getX(), chunkWork.getZ(), () -> { 64 | processChunkWork(chunkWork, selection, operation); 65 | }); 66 | } 67 | }), 1, 1); 68 | } 69 | 70 | /** 71 | * Returns the total amount of chunks that will be processed. 72 | * 73 | * @return Total amount of chunks 74 | */ 75 | public int getTotalChunks() { 76 | return chunkWorkList.size(); 77 | } 78 | 79 | /** 80 | * Returns the amount of chunks processed. This might not match total amount of chunks after operation finishes. 81 | * 82 | * @return Amount of chunks processed 83 | * @see #finished to check if the operation has finished 84 | */ 85 | public int getChunksProcessed() { 86 | return chunksProcessed.get(); 87 | } 88 | 89 | /** 90 | * Returns the amount of chunks left to process. 91 | * 92 | * @return Amount of chunks left to process 93 | */ 94 | public int getChunksLeft() { 95 | return chunksLeft.get(); 96 | } 97 | 98 | /** 99 | * Returns whether the operation has finished. This will return true after all work tasks have been delegated. 100 | * 101 | * @return Whether the operation has finished 102 | * @see #getChunksLeft() to get the amount of chunks left to process 103 | * @see #getChunksProcessed() to get the amount of chunks processed 104 | */ 105 | public boolean isFinished() { 106 | return finished; 107 | } 108 | 109 | private void processChunkWork(ChunkWork chunkWork, Selection selection, BlockEditOperation operation) { 110 | World world = selection.getWorld(); 111 | if (world == null) return; 112 | chunkWork.getChunkAsync(world).thenAccept(chunk -> { 113 | // skip y levels that are not in the selection 114 | for (int y = (int) selection.getMinY(); y <= selection.getMaxY(); y++) { 115 | for (int x = 0; x <= 15; x++) { 116 | for (int z = 0; z <= 15; z++) { 117 | Block block = chunk.getBlock(x, y, z); 118 | if (!selection.contains(block.getLocation().toVector())) continue; 119 | operation.transformBlock(block); 120 | } 121 | } 122 | } 123 | }).thenRunAsync(this.chunksProcessed::incrementAndGet); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/api/WorkSplitter.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.api; 2 | 3 | import me.youhavetrouble.blockedit.util.ChunkWork; 4 | import me.youhavetrouble.blockedit.util.Selection; 5 | import org.bukkit.util.BoundingBox; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | public class WorkSplitter { 12 | 13 | protected static Set getOperatedOnChunks(BoundingBox boundingBox) { 14 | HashSet chunks = new HashSet<>(); 15 | int minChunkX = (int) Math.floor(boundingBox.getMinX()) >> 4; 16 | int maxChunkX = (int) Math.floor(boundingBox.getMaxX()) >> 4; 17 | int minChunkZ = (int) Math.floor(boundingBox.getMinZ()) >> 4; 18 | int maxChunkZ = (int) Math.floor(boundingBox.getMaxZ()) >> 4; 19 | 20 | ChunkWork chunkWork = new ChunkWork(0,0); 21 | for (int x = minChunkX; x <= maxChunkX; x++) { 22 | for (int z = minChunkZ; z <= maxChunkZ; z++) { 23 | chunkWork.setCoords(x << 4, z << 4); 24 | chunks.add(chunkWork.clone()); 25 | } 26 | } 27 | return chunks; 28 | } 29 | 30 | protected static OperationWork runOperation(@NotNull Set chunkWorks, @NotNull Selection selection, int chunksPerTick, @NotNull BlockEditOperation operation) { 31 | OperationWork operationWork = new OperationWork(chunkWorks, selection, chunksPerTick, operation); 32 | operationWork.start(); 33 | return operationWork; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/commands/arguments/BlockDataArgument.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.commands.arguments; 2 | 3 | import org.bukkit.Material; 4 | import org.bukkit.block.data.BlockData; 5 | 6 | import java.util.Locale; 7 | 8 | 9 | public class BlockDataArgument { 10 | 11 | public static BlockData getBlockData(String string) throws InvalidMaterialException, InvalidDataException { 12 | 13 | String[] split = string.split("\\["); 14 | Material material = null; 15 | BlockData blockData = null; 16 | if (split.length >= 1) { 17 | String materialString = split[0].toUpperCase(Locale.ENGLISH); 18 | material = Material.getMaterial(materialString); 19 | if (material == null) throw new InvalidMaterialException(); 20 | } 21 | 22 | if (split.length == 1) { 23 | blockData = material.createBlockData(); 24 | } 25 | if (split.length == 2) { 26 | try { 27 | blockData = material.createBlockData("[" + split[1]); 28 | } catch (IllegalArgumentException e) { 29 | throw new InvalidDataException(); 30 | } 31 | } 32 | 33 | return blockData; 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/commands/arguments/InvalidDataException.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.commands.arguments; 2 | 3 | public class InvalidDataException extends IllegalArgumentException { 4 | 5 | public InvalidDataException() { 6 | super(); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/commands/arguments/InvalidMaterialException.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.commands.arguments; 2 | 3 | public class InvalidMaterialException extends IllegalArgumentException { 4 | 5 | public InvalidMaterialException() { 6 | super(); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/commands/arguments/SchematicProviderArgument.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.commands.arguments; 2 | 3 | import com.mojang.brigadier.LiteralMessage; 4 | import com.mojang.brigadier.StringReader; 5 | import com.mojang.brigadier.arguments.ArgumentType; 6 | import com.mojang.brigadier.arguments.StringArgumentType; 7 | import com.mojang.brigadier.context.CommandContext; 8 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 9 | import com.mojang.brigadier.suggestion.Suggestions; 10 | import com.mojang.brigadier.suggestion.SuggestionsBuilder; 11 | import io.papermc.paper.command.brigadier.argument.CustomArgumentType; 12 | import me.youhavetrouble.blockedit.SchematicHandler; 13 | import me.youhavetrouble.blockedit.commands.exceptiontype.UnknownProviderExceptionType; 14 | import me.youhavetrouble.blockedit.schematic.Schematic; 15 | import me.youhavetrouble.blockedit.schematic.SchematicProvider; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import java.util.Locale; 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | @SuppressWarnings("UnstableApiUsage") 22 | public class SchematicProviderArgument implements CustomArgumentType, String> { 23 | 24 | private final SchematicHandler schematicHandler; 25 | 26 | public SchematicProviderArgument(@NotNull SchematicHandler schematicHandler) { 27 | this.schematicHandler = schematicHandler; 28 | } 29 | 30 | @Override 31 | public @NotNull SchematicProvider parse(StringReader reader) throws CommandSyntaxException { 32 | int cursor = reader.getCursor(); 33 | String arg = reader.getString().toLowerCase(Locale.ENGLISH); 34 | 35 | if (!this.schematicHandler.getSchematicProvidersList().contains(arg)) { 36 | throw new CommandSyntaxException( 37 | new UnknownProviderExceptionType(), 38 | new LiteralMessage("Provider not found"), 39 | arg, 40 | cursor 41 | ); 42 | } 43 | 44 | return schematicHandler.getSchematicProviderByName(arg); 45 | } 46 | 47 | @Override 48 | public @NotNull ArgumentType getNativeType() { 49 | return StringArgumentType.word(); 50 | } 51 | 52 | @Override 53 | public @NotNull CompletableFuture listSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { 54 | for (String providerName : schematicHandler.getSchematicProvidersList()) { 55 | if (!providerName.startsWith(builder.getRemaining())) continue; 56 | builder.suggest(providerName); 57 | } 58 | return builder.buildFuture(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/commands/exceptiontype/UnknownProviderExceptionType.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.commands.exceptiontype; 2 | 3 | import com.mojang.brigadier.exceptions.CommandExceptionType; 4 | 5 | public class UnknownProviderExceptionType implements CommandExceptionType { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/exception/NoProviderForSchematicFileExtensionException.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.exception; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class NoProviderForSchematicFileExtensionException extends SchematicLoadException { 6 | 7 | private String extension; 8 | 9 | public NoProviderForSchematicFileExtensionException( 10 | @NotNull String message, 11 | @NotNull String schematicName, 12 | @NotNull String extension 13 | ) { 14 | super(message, schematicName); 15 | this.extension = extension; 16 | } 17 | 18 | public String getExtension() { 19 | return extension; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/exception/SchematicException.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.exception; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public class SchematicException extends RuntimeException { 7 | 8 | private final String schematicName; 9 | 10 | public SchematicException(@NotNull String message, @Nullable String schematicName, Throwable cause) { 11 | super(message); 12 | this.schematicName = schematicName; 13 | initCause(cause); 14 | } 15 | 16 | public SchematicException(@NotNull String message, @Nullable String schematicName) { 17 | super(message); 18 | this.schematicName = schematicName; 19 | } 20 | 21 | public String getSchematicName() { 22 | return schematicName; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/exception/SchematicHandlerRegistrationException.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.exception; 2 | 3 | import me.youhavetrouble.blockedit.schematic.SchematicProvider; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | public class SchematicHandlerRegistrationException extends IllegalArgumentException { 8 | 9 | private final SchematicProvider schematicProvider; 10 | 11 | public SchematicHandlerRegistrationException( 12 | @NotNull String message, 13 | @Nullable SchematicProvider schematicProvider 14 | ) { 15 | super(message); 16 | this.schematicProvider = schematicProvider; 17 | } 18 | 19 | @Nullable 20 | public SchematicProvider getSchematicProvider() { 21 | return schematicProvider; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/exception/SchematicLoadException.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.exception; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public class SchematicLoadException extends SchematicException { 7 | 8 | public SchematicLoadException(@NotNull String message, @NotNull String schematicName, @Nullable Throwable cause) { 9 | super(message, schematicName, cause); 10 | } 11 | 12 | public SchematicLoadException(@NotNull String message, @NotNull String schematicName) { 13 | super(message, schematicName); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/exception/SchematicSaveException.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.exception; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class SchematicSaveException extends SchematicException { 6 | 7 | public SchematicSaveException(@NotNull String message, @NotNull String schematicName, Throwable cause) { 8 | super(message, schematicName, cause); 9 | } 10 | 11 | public SchematicSaveException(@NotNull String message, @NotNull String schematicName) { 12 | super(message, schematicName); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/operations/PasteOperation.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.operations; 2 | 3 | import me.youhavetrouble.blockedit.api.BlockEditOperation; 4 | import org.bukkit.block.Block; 5 | import org.bukkit.block.BlockState; 6 | import org.bukkit.util.Vector; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * Pastes blocks from a map of vectors and block states. Caution! Vectors must be floored to align with block locations. 12 | * @param blockStateMap 13 | */ 14 | @SuppressWarnings("UnstableApiUsage") 15 | public record PasteOperation(Map blockStateMap) implements BlockEditOperation { 16 | 17 | @Override 18 | public void transformBlock(Block block) { 19 | if (!blockStateMap.containsKey(block.getLocation().toVector())) return; 20 | BlockState blockState = blockStateMap.get(block.getLocation().toVector()); 21 | BlockState newState = blockState.copy(block.getLocation()); 22 | newState.update(true, false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/operations/ReplaceOperation.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.operations; 2 | 3 | import me.youhavetrouble.blockedit.api.BlockEditOperation; 4 | import org.bukkit.block.Block; 5 | import org.bukkit.block.BlockState; 6 | 7 | @SuppressWarnings("UnstableApiUsage") 8 | public record ReplaceOperation(BlockState blockToReplace, BlockState blockToSet) implements BlockEditOperation { 9 | 10 | @Override 11 | public void transformBlock(Block block) { 12 | if (!block.getBlockData().matches(blockToReplace.getBlockData())) return; 13 | BlockState newState = blockToSet.copy(block.getLocation()); 14 | newState.update(true, false); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/operations/SetOperation.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.operations; 2 | 3 | import me.youhavetrouble.blockedit.api.BlockEditOperation; 4 | import org.bukkit.block.Block; 5 | import org.bukkit.block.BlockState; 6 | 7 | @SuppressWarnings("UnstableApiUsage") 8 | public record SetOperation(BlockState blockToSet) implements BlockEditOperation { 9 | 10 | @Override 11 | public void transformBlock(Block block) { 12 | BlockState newState = blockToSet.copy(block.getLocation()); 13 | newState.update(true, false); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/schematic/FileSchematicProvider.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.schematic; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public interface FileSchematicProvider extends SchematicProvider { 6 | 7 | /** 8 | * Get the file extensions of the schematic provider. 9 | */ 10 | @NotNull String[] fileExtensions(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/schematic/Schematic.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.schematic; 2 | 3 | import me.youhavetrouble.blockedit.util.Clipboard; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | public abstract class Schematic { 7 | 8 | /** 9 | * Get the schematic as a Clipboard object 10 | * @return The clipboard object containing the schematic 11 | */ 12 | public abstract @NotNull Clipboard asClipboard(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/schematic/SchematicProvider.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.schematic; 2 | 3 | import me.youhavetrouble.blockedit.exception.SchematicLoadException; 4 | import me.youhavetrouble.blockedit.util.Clipboard; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | /** 9 | * Interface for schematic providers. Schematic providers are used to load and save schematics in different formats. 10 | */ 11 | public interface SchematicProvider { 12 | 13 | /** 14 | * Get the name of the schematic provider. Can only contain lowercase letters and numbers. 15 | * @return The name of the schematic provider 16 | */ 17 | @NotNull String name(); 18 | 19 | /** 20 | * Save the schematic 21 | * @param schematic The schematic to save 22 | */ 23 | void save(@NotNull S schematic); 24 | 25 | /** 26 | * Load a schematic 27 | * @param name Name of the schematic to load 28 | * @return The loaded schematic. Returns null if the schematic does not exist. 29 | * @throws SchematicLoadException If the schematic could not be loaded 30 | */ 31 | @Nullable S load(@NotNull String name) throws SchematicLoadException; 32 | 33 | /** 34 | * Create a new schematic from the given clipboard 35 | * @param name Name of the schematic 36 | * @param clipboard Clipboard object containing the schematic 37 | * @return The created schematic 38 | */ 39 | @NotNull S fromClipboard(@NotNull String name, @NotNull Clipboard clipboard); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/util/ChunkWork.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.util; 2 | 3 | import org.bukkit.Chunk; 4 | import org.bukkit.World; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public class ChunkWork { 9 | 10 | private int x, z; 11 | 12 | public ChunkWork(double x, double z) { 13 | setCoords(x,z); 14 | } 15 | 16 | public CompletableFuture getChunkAsync(World world) { 17 | return world.getChunkAtAsync(x,z, true); 18 | } 19 | 20 | public int getX() { 21 | return x; 22 | } 23 | 24 | public int getZ() { 25 | return z; 26 | } 27 | 28 | public void setCoords(double x, double z) { 29 | this.x = (int) x >> 4; 30 | this.z = (int) z >> 4; 31 | } 32 | 33 | @Override 34 | public boolean equals(Object object) { 35 | if (!(object instanceof ChunkWork chunkWork)) return false; 36 | return this.x == chunkWork.getX() && this.z == chunkWork.getZ(); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | int res = 17; 42 | res = res * 31 + Math.min(x, z); 43 | res = res * 31 + Math.max(x, z); 44 | return res; 45 | } 46 | 47 | public ChunkWork clone() { 48 | return new ChunkWork(x*16,z*16); 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/util/Clipboard.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.util; 2 | 3 | import org.bukkit.Location; 4 | import org.bukkit.util.Vector; 5 | import org.bukkit.block.BlockState; 6 | 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Iterator; 10 | import java.util.Map; 11 | 12 | public class Clipboard { 13 | 14 | /** 15 | * Map of locations relative to the center of the clipboard and their block states 16 | */ 17 | private HashMap blocks = new HashMap<>(); 18 | private Location baseLocation; 19 | private Vector baseLocationVector; 20 | 21 | public Clipboard(Location baseLocation) { 22 | this.baseLocation = baseLocation; 23 | } 24 | 25 | public void addBlock(Vector relativeLocation, BlockState blockState) { 26 | this.blocks.put(relativeLocation, blockState); 27 | } 28 | 29 | public Map getBlocks() { 30 | return Collections.unmodifiableMap(this.blocks); 31 | } 32 | 33 | public void setBaseLocation(Location baseLocation) { 34 | this.baseLocation = baseLocation.toBlockLocation(); 35 | this.baseLocationVector = baseLocation.toVector(); 36 | } 37 | 38 | public Location getBaseLocation() { 39 | return baseLocation; 40 | } 41 | 42 | public Vector getBaseLocationVector() { 43 | return baseLocationVector; 44 | } 45 | 46 | public void clear() { 47 | this.blocks.clear(); 48 | } 49 | 50 | public boolean isEmpty() { 51 | return this.blocks.isEmpty(); 52 | } 53 | 54 | /** 55 | * Rotates clipboard by specified degrees around the base location. 56 | * @param angle angle in degrees 57 | */ 58 | public void rotate(double angle) { 59 | double radians = Math.toRadians(angle); 60 | HashMap newBlocks = new HashMap<>(); 61 | Iterator> iterator = this.blocks.entrySet().iterator(); 62 | while (iterator.hasNext()) { 63 | Map.Entry entry = iterator.next(); 64 | Vector relativeLocation = entry.getKey(); 65 | relativeLocation.rotateAroundY(radians); 66 | relativeLocation.setX(Math.round(relativeLocation.getX())); 67 | relativeLocation.setZ(Math.round(relativeLocation.getZ())); 68 | newBlocks.put(relativeLocation, entry.getValue()); 69 | iterator.remove(); 70 | } 71 | this.blocks = newBlocks; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/util/NameFilenameFilter.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.util; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.File; 6 | import java.io.FilenameFilter; 7 | import java.util.List; 8 | 9 | public class NameFilenameFilter implements FilenameFilter { 10 | 11 | private final String filterName; 12 | 13 | public NameFilenameFilter(@NotNull String name) { 14 | this.filterName = name; 15 | } 16 | 17 | @Override 18 | public boolean accept(File dir, String name) { 19 | 20 | String[] parts = name.split("\\."); 21 | if (parts.length < 2) return filterName.equals(name); 22 | String joined = String.join(".", List.of(parts).subList(0, parts.length - 1)); 23 | return joined.equals(filterName); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/util/Selection.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.util; 2 | 3 | import org.bukkit.Bukkit; 4 | import org.bukkit.Location; 5 | import org.bukkit.World; 6 | import org.bukkit.util.BoundingBox; 7 | import org.bukkit.util.Vector; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.Set; 12 | import java.util.UUID; 13 | 14 | public class Selection extends BoundingBox { 15 | 16 | private UUID worldUuid; 17 | private Location selectionPoint1, selectionPoint2; 18 | 19 | public Selection(Location location1, Location location2, UUID worldUuid) { 20 | super(location1.getX(), location1.getY(), location1.getZ(), location2.getX(), location2.getY(), location2.getZ()); 21 | this.worldUuid = worldUuid; 22 | this.selectionPoint1 = location1; 23 | this.selectionPoint2 = location2; 24 | } 25 | 26 | public Selection(BoundingBox boundingBox, UUID worldUuid) { 27 | super(boundingBox.getMinX(), boundingBox.getMinY(), boundingBox.getMinZ(), boundingBox.getMaxX(), boundingBox.getMaxY(), boundingBox.getMaxZ()); 28 | this.worldUuid = worldUuid; 29 | } 30 | 31 | @Nullable 32 | public Location getSelectionPoint1() { 33 | return selectionPoint1; 34 | } 35 | 36 | @Nullable 37 | public Location getSelectionPoint2() { 38 | return selectionPoint2; 39 | } 40 | 41 | @Nullable 42 | public World getWorld() { 43 | return Bukkit.getWorld(worldUuid); 44 | } 45 | 46 | @NotNull 47 | public UUID getWorldUuid() { 48 | return worldUuid; 49 | } 50 | 51 | public void setWorldUuid(UUID worldUuid) { 52 | this.worldUuid = worldUuid; 53 | } 54 | 55 | @NotNull 56 | @Override 57 | public Selection clone() { 58 | return new Selection(this, worldUuid); 59 | } 60 | 61 | public static Selection fromClipboard(Set locations, World world) { 62 | Vector closestVector = new Vector(Double.MAX_VALUE,Double.MAX_VALUE,Double.MAX_VALUE); 63 | Vector farthestVector = new Vector(Double.MIN_VALUE,Double.MIN_VALUE,Double.MIN_VALUE); 64 | 65 | for (Vector vector : locations) { 66 | if (vector.getX() + vector.getY() + vector.getZ() < closestVector.getX() + closestVector.getY() + closestVector.getZ()) { 67 | closestVector = vector; 68 | } 69 | if (vector.getX() + vector.getY() + vector.getZ() > farthestVector.getX() + farthestVector.getY() + farthestVector.getZ()) { 70 | farthestVector = vector; 71 | } 72 | } 73 | 74 | Location minLocation = closestVector.toLocation(world); 75 | Location maxLocation = farthestVector.toLocation(world); 76 | 77 | return new Selection(minLocation, maxLocation, world.getUID()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/me/youhavetrouble/blockedit/wands/SelectionWand.java: -------------------------------------------------------------------------------- 1 | package me.youhavetrouble.blockedit.wands; 2 | 3 | import me.youhavetrouble.blockedit.BELocale; 4 | import me.youhavetrouble.blockedit.BEPlayer; 5 | import me.youhavetrouble.blockedit.api.BlockEditAPI; 6 | import me.youhavetrouble.blockedit.api.BlockEditWand; 7 | import net.kyori.adventure.text.Component; 8 | import net.kyori.adventure.text.format.NamedTextColor; 9 | import net.kyori.adventure.text.format.TextDecoration; 10 | import org.bukkit.Location; 11 | import org.bukkit.block.Block; 12 | import org.bukkit.entity.Player; 13 | import org.bukkit.event.EventHandler; 14 | import org.bukkit.event.EventPriority; 15 | import org.bukkit.event.Listener; 16 | import org.bukkit.event.block.Action; 17 | import org.bukkit.event.player.PlayerInteractEvent; 18 | 19 | @SuppressWarnings("UnstableApiUsage") 20 | public class SelectionWand implements Listener, BlockEditWand { 21 | 22 | @Override 23 | public String getId() { 24 | return "select"; 25 | } 26 | 27 | @Override 28 | public Component getName() { 29 | return Component.text("Selection Wand").decoration(TextDecoration.ITALIC, false); 30 | } 31 | 32 | @Override 33 | public int getCustomModelData() { 34 | return 0; 35 | } 36 | 37 | @Override 38 | public String getPermission() { 39 | return "blockedit.selectwand"; 40 | } 41 | 42 | @EventHandler(priority = EventPriority.LOWEST) 43 | public void onPlayerSelectBlock(PlayerInteractEvent event) { 44 | Player player = event.getPlayer(); 45 | String wandId = BlockEditAPI.getWandsHandler().getWandId(event.getItem()); 46 | if (wandId == null) return; 47 | if (!wandId.equals(getId())) return; 48 | if (!player.hasPermission(getPermission())) return; 49 | Block block = event.getClickedBlock(); 50 | if (block == null) return; 51 | Action action = event.getAction(); 52 | if (action.equals(Action.LEFT_CLICK_BLOCK)) { 53 | event.setCancelled(true); 54 | Location location = block.getLocation(); 55 | BEPlayer.getByPlayer(player).setSelectionPoint1(location); 56 | String locationString = "X: " + location.blockX() + " Y: " + location.blockY() + " Z: " + location.blockZ(); 57 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).firstPositionSet.formatted(locationString), NamedTextColor.GRAY)); 58 | return; 59 | } 60 | if (action.equals(Action.RIGHT_CLICK_BLOCK)) { 61 | event.setCancelled(true); 62 | Location location = block.getLocation(); 63 | BEPlayer.getByPlayer(player).setSelectionPoint2(location); 64 | String locationString = "X: " + location.blockX() + " Y: " + location.blockY() + " Z: " + location.blockZ(); 65 | player.sendMessage(Component.text(BELocale.getLocale(player.locale()).secondPositionSet.formatted(locationString), NamedTextColor.GRAY)); 66 | return; 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/resources/locale/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "could_not_find_wand_by_id": "Could not find wand with id %s", 3 | "select_area": "You need to select an area first", 4 | "copied_selection_to_clipboard": "Copied selection to clipboard", 5 | "selection_reset": "You have reset your selection", 6 | "first_position_set": "First position set to %s", 7 | "second_position_set": "Second position set to %s", 8 | "pasting_clipboard": "Pasting clipboard...", 9 | "clipboard_rotated": "Clipboard rotated %s degrees", 10 | "setting_blocks": "Setting blocks...", 11 | "replacing_blocks": "Replacing blocks...", 12 | "started_loading_schematic": "Started loading schematic %s", 13 | "schematic_loaded": "Loaded schematic %s", 14 | "provider_not_found": "Provider not found", 15 | "no_provider_for_schematic_file": "No provider for schematic file type %s", 16 | "schematic_load_error": "Error loading schematic %s: %s", 17 | "schematic_not_found": "Schematic %s not found" 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/locale/pl_PL.json: -------------------------------------------------------------------------------- 1 | { 2 | "could_not_find_wand_by_id": "Nie znaleziono różdźki o id %s", 3 | "select_area": "Musisz najpierw wybrać obszar", 4 | "copied_selection_to_clipboard": "Skopiowano zaznaczenie do schowka", 5 | "selection_reset": "Zresetowałeś swoje zaznaczenie", 6 | "first_position_set": "Pierwsza pozycja ustawiona na %s", 7 | "second_position_set": "Druga pozycja ustawiona na %s", 8 | "pasting_clipboard": "Wklejanie ze schowka...", 9 | "clipboard_rotated": "Schowek obrócony o %s stopni", 10 | "setting_blocks": "Ustawianie bloków...", 11 | "replacing_blocks": "Zastępowanie bloków...", 12 | "started_loading_schematic": "Rozpoczęto ładowanie schematu %s", 13 | "schematic_loaded": "Załadowano schemat %s", 14 | "provider_not_found": "Nie znaleziono dostawcy", 15 | "no_provider_for_schematic_file": "Nie znaleziono dostawcy dla typu pliku %s", 16 | "schematic_load_error": "Błąd podczas ładowania schematu %s: %s", 17 | "schematic_not_found": "Nie znaleziono schematu %s" 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/paper-plugin.yml: -------------------------------------------------------------------------------- 1 | name: BlockEdit 2 | version: ${project.version} 3 | main: me.youhavetrouble.blockedit.BlockEdit 4 | api-version: "1.20" 5 | authors: [ YouHaveTrouble ] 6 | description: Modern WorldEdit alternative 7 | website: https://youhavetrouble.me 8 | folia-supported: true 9 | --------------------------------------------------------------------------------