├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ ├── EClean.kt │ ├── clean │ │ ├── Clean.kt │ │ ├── Trashcan.kt │ │ ├── chunk.kt │ │ ├── drop.kt │ │ └── living.kt │ ├── command │ │ ├── Clean.kt │ │ ├── Commands.kt │ │ ├── Debug.kt │ │ ├── EntityStats.kt │ │ ├── Players.kt │ │ ├── Reload.kt │ │ ├── Show.kt │ │ ├── Stats.kt │ │ ├── Trash.kt │ │ └── check.kt │ ├── config │ │ ├── Config.kt │ │ └── Lang.kt │ ├── hook │ │ └── PapiHook.kt │ ├── menu │ │ ├── MenuManager.kt │ │ ├── dense │ │ │ ├── DenseMenu.kt │ │ │ ├── DenseZone.kt │ │ │ ├── EntityInfo.kt │ │ │ ├── NextButton.kt │ │ │ └── PrevButton.kt │ │ └── trashcan │ │ │ ├── NextButton.kt │ │ │ ├── PrevButton.kt │ │ │ ├── TrashInfo.kt │ │ │ ├── TrashcanMenu.kt │ │ │ └── TrashcanZone.kt │ ├── papi │ │ └── Papi.kt │ ├── update │ │ └── Update.kt │ └── util │ │ ├── online.kt │ │ └── util.kt └── resources │ ├── config.yml │ ├── lang.yml │ └── plugin.yml └── test └── kotlin ├── ECleanTest.kt ├── clean ├── ChunkCleanTest.kt ├── DropCleanTest.kt └── LivingCleanTest.kt ├── package.kt ├── trash └── TrashcanTest.kt └── util.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | build 4 | gradle/wrapper 5 | !gradle/wrapper/gradle-wrapper.properties -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [EClean](https://github.com/4o4E/EClean) 2 | 3 | > [!IMPORTANT] 4 | > 目前最新版本不支持java8, 请使用以下版本 5 | > https://github.com/4o4E/EClean/releases/tag/1.16.1 6 | 7 | > 基于BukkitAPI的清理插件, 适用于Spigot和Paper等Bukkit的下游分支核心, 支持`1.8.x` 8 | > 及以上版本, `1.8和1.18.x`, `1.19.x`, `1.20.x`经过测试 9 | > 10 | > mod核心(`mohist`/`arclight`)不在支持范围内, 若一定要使用请不要在此反馈问题 11 | 12 | [![Release](https://img.shields.io/github/v/release/4o4E/EClean?label=Release)](https://github.com/4o4E/EClean/releases/latest) 13 | [![Downloads](https://img.shields.io/github/downloads/4o4E/EClean/total?label=Download)](https://github.com/4o4E/EClean/releases) 14 | 15 | [![bstats](https://bstats.org/signatures/bukkit/EClean.svg)](https://bstats.org/plugin/bukkit/EClean) 16 | 17 | ## 支持设置 18 | 19 | - 清理间隔 20 | - 清理前通知 21 | - 忽略的世界 22 | - 生物/实体/掉落物类型匹配(支持正则) 23 | - 设置拴绳拴住/乘骑中/捡起物品的实体是否清理 24 | - 密集实体检测 25 | 26 | ## 指令 27 | 28 | > 插件主命令为`/eclean`,包括缩写`/ecl`,如果`/ecl`与其他插件冲突,请使用`/eclean` 29 | 30 | - `/eclean reload` 重载插件, 重载后计划清理的任务将重新开始计时 31 | - `/eclean clean` 立刻执行一次清理(不显示清理前提示,在有玩家的服务器中慎用) 32 | - `/eclean clean entity` 立刻执行一次实体清理(不显示清理前提示) 33 | - `/eclean clean entity <世界名>` 立刻在指定世界执行一次实体清理(不显示清理前提示) 34 | - `/eclean clean drop` 立刻执行一次掉落物清理(不显示清理前提示) 35 | - `/eclean clean drop <世界名>` 立刻在指定世界执行一次掉落物清理(不显示清理前提示) 36 | - `/eclean clean chunk` 立刻执行一次密集实体清理(不显示清理前提示) 37 | - `/eclean clean chunk <世界名>` 立刻在指定世界执行一次密集实体清理(不显示清理前提示) 38 | - `/eclean entity <实体名>` 统计当前世界每个区块的指定实体 39 | - `/eclean entity <实体名> <世界名>` 统计指定世界每个区块的指定实体 40 | - `/eclean entity <实体名> <世界名> <纳入统计所需数量>` 统计指定世界每个区块的指定实体并隐藏不超过指定数量的内容 41 | - `/eclean stats` 统计当前所在世界的实体和区块统计 42 | - `/eclean stats <世界名>` 统计实体和区块统计 43 | - `/eclean trash` 打开垃圾桶 44 | - `/eclean show` 打开密集实体统计信息菜单 45 | 46 | ## 权限 47 | 48 | - `eclean.admin` 使用插件指令 49 | - `eclean.trash` 打开垃圾桶 50 | 51 | ## PlaceholderAPI 52 | 53 | - `%eclean_before_next%` - `距离下一次清理的时间, 单位秒` 54 | - `%eclean_before_next_formatted%` - `距离下一次清理的时间, 格式化的时间` 55 | - `%eclean_last_drop%` - `上次清理的掉落物数量` 56 | - `%eclean_last_living%` - `上次清理的生物数量` 57 | - `%eclean_last_chunk%` - `上次清理的密集实体数量` 58 | - `%eclean_trashcan_countdown%` - `垃圾桶清理倒计时, 单位秒` 59 | - `%eclean_trashcan_countdown_formatted%` - `垃圾桶清理倒计时, 格式化的时间` 60 | 61 | ## 配置 62 | 63 | 插件默认配置见[配置文件](src/main/resources/config.yml), 配置项均有注释描述用法和含义 64 | 65 | ## 下载 66 | 67 | - [最新版](https://github.com/4o4E/EClean/releases/latest) 68 | 69 | ## 计划添加 70 | 71 | - [x] ~~不清理附魔物品,以及书写过的书~~ 2023.01.11添加 72 | - [ ] 清理规则按世界单独配置(判断优先级: 实体规则 -> 世界规则 -> 默认规则) 73 | - [x] ~~公共垃圾桶(支持翻页, 物品过期时间)~~ ~~如果做会单独做一个插件~~ 写了轻量版的不在关服后持久化垃圾桶物品数据的实现 74 | - [ ] 红石统计及高频清理 75 | - [ ] 区块卸载 76 | - [x] ~~区块上限实现多种实体共用一个上限~~ 2023.07.29添加 77 | 78 | ~~咕咕咕~~ 79 | 80 | ## 更新记录 81 | 82 | ``` 83 | 2022.02.15 - 1.0.1 发布插件 84 | 2022.02.15 - 1.0.2 添加更新检查;当设置中的finish字段设置为 "" 时将不会发送清理完成的消息,若希望清理结束只发送一次消息,可以只设置一个为发送消息,其余设置为 "" 85 | 2022.02.16 - 1.0.3 添加低版本支持(1.8.x - 1.18.x) 86 | 2022.03.14 - 1.0.4 添加区块实体统计和世界实体统计 87 | 2022.04.05 - 1.0.5 添加手动执行清理的指令 88 | 2022.04.17 - 1.0.6 修改指令格式, 更换指令别名`ec`至`ecl`以避免与其他指令冲突导致的无法补全 89 | 2022.04.19 - 1.0.7 添加垃圾桶功能 `eclean trash` 90 | 2022.04.20 - 1.0.8 优化插件, 添加更新检查开关 91 | 2023.01.09 - 1.0.9 优化插件, 修复kotlin依赖版本冲突导致的插件无法加载, 添加语言文件 92 | 2023.01.10 - 1.0.10 修复插件加载低版本配置文件时报错的问题 93 | 2023.01.11 - 1.0.11 修复玩家打开背包时无法打开垃圾桶的问题, 添加物品清理时关于附魔物品和写过的书的相关设置 94 | 2023.01.12 - 1.0.12 添加papi支持 95 | 2023.01.13 - 1.0.13 支持显示已有papi, 修复清理消息不正常发送的问题 96 | 2023.01.17 - 1.0.14 添加缺失的i18n, 修复错误的广播消息 97 | 2023.06.30 - 1.15.0 更改版本号方式, 更新密集实体统计信息菜单 98 | 2023.07.21 - 1.16.0 在1.8也使用utf8作为配置文件编码, 避免转码; 修复1.8中无法正常使用的bug; 添加菜单的右键移除实体功能; 支持密集实体清理中多种实体共用一个上限; 修复1.8中不兼容的音效的问题 99 | 2023.08.31 - 1.16.1 优化代码, 修复配置文件中区块清理配置和注释相反的问题 100 | 2023.09.27 - 1.17.0 补全单元测试, 优化处理逻辑和debug信息, 现在debug信息会完整输出搜索的过程细节, 添加公共垃圾箱功能 101 | 2023.09.27 - 1.17.1 补全垃圾桶清理的papi, 优化代码逻辑 102 | 2023.10.11 - 1.17.2 修复关闭垃圾桶功能时无法正确清理掉落物的bug 103 | 2024.05.12 - 1.18.0 添加无玩家在线时的配置 104 | 2024.06.22 - 1.18.1 修复不清理的bug 105 | 2025.01.21 - 1.19.0 修复上游依赖 106 | ``` 107 | 108 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | plugins { 5 | kotlin("jvm") version "2.1.0" 6 | kotlin("plugin.serialization") version "2.1.0" 7 | id("com.gradleup.shadow") version "9.0.0-beta4" 8 | } 9 | 10 | group = "top.e404" 11 | version = "1.20.0" 12 | val epluginVer = "1.4.0" 13 | 14 | fun eplugin(module: String, version: String = epluginVer) = "top.e404:eplugin-$module:$version" 15 | 16 | repositories { 17 | mavenLocal() 18 | // papermc 19 | maven("https://repo.papermc.io/repository/maven-public/") 20 | // spigot 21 | maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") 22 | // sonatype 23 | maven("https://oss.sonatype.org/content/groups/public/") 24 | // placeholderAPI 25 | maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") 26 | mavenCentral() 27 | } 28 | 29 | dependencies { 30 | // spigot 31 | compileOnly("org.spigotmc:spigot-api:1.13.2-R0.1-SNAPSHOT") 32 | // eplugin 33 | implementation(eplugin("core")) 34 | implementation(eplugin("menu")) 35 | implementation(eplugin("serialization")) 36 | implementation(eplugin("hook-placeholderapi")) 37 | // placeholderAPI 38 | compileOnly("me.clip:placeholderapi:2.11.6") 39 | // Bstats 40 | implementation("org.bstats:bstats-bukkit:3.0.2") 41 | 42 | // mock bukkit 43 | testImplementation(kotlin("test", "2.1.0")) 44 | testImplementation("io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT") 45 | testImplementation("com.github.seeseemelk:MockBukkit-v1.20:3.87.0") 46 | testImplementation("org.slf4j:slf4j-simple:2.0.13") 47 | testImplementation("net.kyori:adventure-text-serializer-legacy:4.17.0") 48 | } 49 | 50 | java { 51 | sourceCompatibility = JavaVersion.VERSION_17 52 | targetCompatibility = JavaVersion.VERSION_1_8 53 | } 54 | 55 | kotlin { 56 | jvmToolchain(17) 57 | compilerOptions { 58 | jvmTarget.set(JvmTarget.JVM_1_8) 59 | } 60 | } 61 | 62 | tasks { 63 | build { 64 | finalizedBy(shadowJar) 65 | } 66 | 67 | shadowJar { 68 | val archiveName = "${project.name}-${project.version}.jar" 69 | archiveFileName.set(archiveName) 70 | 71 | relocate("org.bstats", "top.e404.eclean.relocate.bstats") 72 | relocate("kotlin", "top.e404.eclean.relocate.kotlin") 73 | relocate("top.e404.eplugin", "top.e404.eclean.relocate.eplugin") 74 | relocate("com.charleskorn.kaml", "top.e404.eclean.relocate.kaml") 75 | exclude("META-INF/**") 76 | 77 | doLast { 78 | val archiveFile = archiveFile.get().asFile 79 | println(archiveFile.parentFile.absolutePath) 80 | println(archiveFile.absolutePath) 81 | } 82 | } 83 | 84 | withType { 85 | dependsOn(clean) 86 | } 87 | 88 | processResources { 89 | filteringCharset = Charsets.UTF_8.name() 90 | filesMatching("plugin.yml") { 91 | expand("version" to project.version) 92 | } 93 | } 94 | 95 | test { 96 | useJUnitPlatform() 97 | this.systemProperties["eclean.debug"] = true 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "EClean" -------------------------------------------------------------------------------- /src/main/kotlin/EClean.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.plugin.PluginDescriptionFile 5 | import org.bukkit.plugin.java.JavaPluginLoader 6 | import top.e404.eclean.clean.Clean 7 | import top.e404.eclean.clean.Trashcan 8 | import top.e404.eclean.command.Commands 9 | import top.e404.eclean.config.Config 10 | import top.e404.eclean.config.Lang 11 | import top.e404.eclean.hook.HookManager 12 | import top.e404.eclean.hook.PapiHook 13 | import top.e404.eclean.menu.MenuManager 14 | import top.e404.eclean.papi.Papi 15 | import top.e404.eclean.update.Update 16 | import top.e404.eplugin.EPlugin 17 | import java.io.File 18 | 19 | open class EClean : EPlugin { 20 | companion object { 21 | val logo = listOf( 22 | """&6 ______ ______ __ ______ ______ __ __ """.color, 23 | """&6/\ ___\ /\ ___\ /\ \ /\ ___\ /\ __ \ /\ "-.\ \ """.color, 24 | """&6\ \ __\ \ \ \____ \ \ \____ \ \ __\ \ \ __ \ \ \ \-. \ """.color, 25 | """&6 \ \_____\ \ \_____\ \ \_____\ \ \_____\ \ \_\ \_\ \ \_\\"\_\""".color, 26 | """&6 \/_____/ \/_____/ \/_____/ \/_____/ \/_/\/_/ \/_/ \/_/""".color 27 | ) 28 | } 29 | 30 | @Suppress("UNUSED") 31 | constructor() : super() 32 | 33 | @Suppress("UNUSED") 34 | constructor( 35 | loader: JavaPluginLoader, 36 | description: PluginDescriptionFile, 37 | dataFolder: File, 38 | file: File 39 | ) : super(loader, description, dataFolder, file) 40 | 41 | override val debugPrefix get() = langManager["debug_prefix"] 42 | override val prefix get() = langManager["prefix"] 43 | 44 | override val bstatsId = 14312 45 | override var debug: Boolean 46 | get() = Config.config.debug 47 | set(value) { 48 | Config.config.debug = value 49 | } 50 | override val langManager by lazy { Lang } 51 | 52 | init { 53 | PL = this 54 | } 55 | 56 | override fun onEnable() { 57 | if (!unit) bstats() 58 | Lang.load(null) 59 | Config.load(null) 60 | Commands.register() 61 | Update.register() 62 | Clean.schedule() 63 | HookManager.register() 64 | MenuManager.register() 65 | Trashcan.register() 66 | if (PapiHook.enable) Papi.register() 67 | for (line in logo) info(line) 68 | info("&a加载完成, 作者404E, 感谢使用".color) 69 | } 70 | 71 | override fun onDisable() { 72 | MenuManager.shutdown() 73 | if (PapiHook.enable) Papi.unregister() 74 | Bukkit.getScheduler().cancelTasks(this) 75 | info("&a已卸载, 作者404E, 感谢使用".color) 76 | } 77 | } 78 | 79 | lateinit var PL: EPlugin 80 | private set 81 | 82 | internal var unit = false 83 | -------------------------------------------------------------------------------- /src/main/kotlin/clean/Clean.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.clean 2 | 3 | import org.bukkit.scheduler.BukkitTask 4 | import top.e404.eclean.PL 5 | import top.e404.eclean.config.Config 6 | import top.e404.eclean.config.Lang 7 | import top.e404.eclean.util.noOnline 8 | import top.e404.eclean.util.noOnlineClean 9 | import top.e404.eclean.util.noOnlineMessage 10 | import top.e404.eplugin.EPlugin.Companion.color 11 | 12 | object Clean { 13 | private var task: BukkitTask? = null 14 | private val duration get() = Config.config.duration 15 | 16 | /** 17 | * 计数, 每20tick++ 18 | */ 19 | var count = 0L 20 | private set 21 | 22 | fun schedule() { 23 | count = 0 24 | task?.cancel() 25 | // 清理任务 26 | PL.info("&f设置清理任务, 间隔${duration}秒") 27 | Config.config.message.forEach { (delay, message) -> 28 | if (delay > duration) PL.warn(Lang["warn.out_of_range", "message" to message, "duration" to duration]) 29 | else PL.info("&f设置清理前${delay}秒提醒: ${message.color}") 30 | } 31 | task = PL.runTaskTimer(20, 20) { 32 | count++ 33 | if (noOnline) { 34 | if (noOnlineMessage) { 35 | Config.config.message[duration - count]?.let { PL.broadcastMsg(it) } 36 | } 37 | } else { 38 | Config.config.message[duration - count]?.let { PL.broadcastMsg(it) } 39 | } 40 | if (count >= duration) { 41 | count = 0 42 | if (noOnline) { 43 | if (noOnlineClean) { 44 | clean() 45 | } 46 | } else { 47 | clean() 48 | } 49 | } 50 | } 51 | } 52 | 53 | fun clean() { 54 | cleanDrop() 55 | cleanLiving() 56 | cleanDenseEntities() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/clean/Trashcan.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.clean 2 | 3 | import org.bukkit.entity.Player 4 | import org.bukkit.event.EventHandler 5 | import org.bukkit.event.inventory.InventoryCloseEvent 6 | import org.bukkit.inventory.ItemStack 7 | import org.bukkit.scheduler.BukkitTask 8 | import top.e404.eclean.PL 9 | import top.e404.eclean.config.Config 10 | import top.e404.eclean.config.Lang 11 | import top.e404.eclean.menu.MenuManager 12 | import top.e404.eclean.menu.trashcan.TrashInfo 13 | import top.e404.eclean.menu.trashcan.TrashcanMenu 14 | import top.e404.eplugin.listener.EListener 15 | 16 | object Trashcan : EListener(PL) { 17 | /** 18 | * 垃圾桶中的物品及其数量, ItemStack的数量没有作用, 以value的数量为准 19 | */ 20 | val trashData = mutableMapOf() 21 | 22 | val trashValues = mutableListOf() 23 | 24 | private val openTrash = mutableMapOf() 25 | 26 | var task: BukkitTask? = null 27 | 28 | fun cleanTrash(){ 29 | Config.plugin.debug { "清空垃圾桶" } 30 | trashData.clear() 31 | trashValues.clear() 32 | PL.broadcastMsg(Lang["command.trash_clean_done"]) 33 | } 34 | 35 | /** 36 | * 垃圾桶清空倒计时, 单位秒 37 | */ 38 | var countdown = 0L 39 | 40 | fun schedule() { 41 | task?.cancel() 42 | val duration = Config.config.trashcan.duration 43 | if (duration == null) { 44 | task = null 45 | return 46 | } 47 | if (!Config.config.trashcan.enable) return 48 | countdown = duration 49 | task = Config.plugin.runTaskTimer(20, 20) { 50 | countdown-- 51 | if (countdown <= 0L) { 52 | countdown = duration 53 | cleanTrash() 54 | update() 55 | } 56 | } 57 | } 58 | 59 | fun ItemStack.sign() = ItemSign(this) 60 | class ItemSign(val item: ItemStack) { 61 | override fun equals(other: Any?): Boolean { 62 | if (other == null) return false 63 | if (other !is ItemSign) return false 64 | return item.isSimilar(other.item) 65 | } 66 | 67 | override fun hashCode(): Int { 68 | var hash = 1 69 | hash = hash * 31 + item.type.hashCode() 70 | @Suppress("DEPRECATION") 71 | hash = hash * 31 + (item.durability.toInt() and 0xffff) 72 | if (item.hasItemMeta()) hash = hash * 31 + item.itemMeta.hashCode() 73 | return hash 74 | } 75 | } 76 | 77 | @EventHandler 78 | fun InventoryCloseEvent.onEvent() { 79 | openTrash.remove(player) 80 | } 81 | 82 | fun open(player: Player) { 83 | val menu = TrashcanMenu() 84 | MenuManager.openMenu(menu, player) 85 | openTrash[player] = menu 86 | } 87 | 88 | fun addItems(items: Collection) { 89 | for (item in items) { 90 | val sign = item.sign() 91 | val exists = trashData[sign] 92 | if (exists != null) { 93 | exists.amount += item.amount 94 | continue 95 | } 96 | val info = TrashInfo(item, item.amount) 97 | trashData[sign] = info 98 | } 99 | trashValues.clear() 100 | trashValues.addAll(trashData.values) 101 | openTrash.values.forEach { it.zone.update() } 102 | } 103 | 104 | fun addItem(item: ItemStack) { 105 | val sign = item.sign() 106 | val exists = trashData[sign] 107 | if (exists != null) { 108 | exists.amount += item.amount 109 | openTrash.values.forEach { it.zone.update() } 110 | return 111 | } 112 | val info = TrashInfo(item, item.amount) 113 | trashData[sign] = info 114 | trashValues.clear() 115 | trashValues.addAll(trashData.values) 116 | openTrash.values.forEach { it.zone.update() } 117 | } 118 | 119 | fun update() { 120 | plugin.debug { "更新全部玩家的公共垃圾桶菜单" } 121 | openTrash.entries.forEach { (player, menu) -> 122 | plugin.debug { "更新玩家${player.name}的公共垃圾桶菜单" } 123 | menu.updateIcon() 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /src/main/kotlin/clean/chunk.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.clean 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.Chunk 5 | import org.bukkit.World 6 | import org.bukkit.entity.Entity 7 | import org.bukkit.entity.LivingEntity 8 | import top.e404.eclean.PL 9 | import top.e404.eclean.config.Config 10 | import top.e404.eclean.util.info 11 | import top.e404.eclean.util.noOnline 12 | import top.e404.eclean.util.noOnlineMessage 13 | import top.e404.eplugin.EPlugin.Companion.placeholder 14 | 15 | private inline val chunkCfg get() = Config.config.chunk 16 | 17 | /** 18 | * 最近一次清理区块密集实体的数量 19 | */ 20 | var lastChunk = 0 21 | private set 22 | 23 | /** 24 | * 清理全服区块中的密集实体 25 | */ 26 | fun cleanDenseEntities() { 27 | if (!chunkCfg.enable) { 28 | PL.debug { "密集实体清理已禁用" } 29 | return 30 | } 31 | val worlds = Bukkit.getWorlds().filterNot { chunkCfg.disableWorld.any { regex -> it.name matches regex } } 32 | PL.debug { "开始进行密集实体检查" } 33 | PL.debug { 34 | buildString { 35 | append("启用密集实体检查的世界: [") 36 | worlds.joinTo(this, ", ", transform = World::getName) 37 | append("]") 38 | } 39 | } 40 | PL.debug { if (chunkCfg.settings.name) "清理被命名的生物" else "不清理被命名的生物" } 41 | PL.debug { if (chunkCfg.settings.lead) "清理拴绳拴住的生物" else "不清理拴绳拴住的生物" } 42 | PL.debug { if (chunkCfg.settings.mount) "清理乘骑中的生物" else "不清理乘骑中的生物" } 43 | 44 | var time = System.currentTimeMillis() 45 | lastChunk = worlds.sumOf { it.cleanChunkDenseEntities() } 46 | time = System.currentTimeMillis() - time 47 | 48 | PL.debug { "密集实体清理共${lastChunk}个, 耗时${time}ms" } 49 | 50 | if (noOnline) { 51 | if (noOnlineMessage) { 52 | val finish = Config.config.chunk.finish 53 | if (finish.isNotBlank()) PL.broadcastMsg(finish.placeholder("clean" to lastChunk)) 54 | } 55 | } else { 56 | val finish = Config.config.chunk.finish 57 | if (finish.isNotBlank()) PL.broadcastMsg(finish.placeholder("clean" to lastChunk)) 58 | } 59 | } 60 | 61 | /** 62 | * 清理指定世界的密集实体 63 | * 64 | * @return 清理的实体数量 65 | */ 66 | fun World.cleanChunkDenseEntities() = loadedChunks.sumOf { it.cleanDenseEntities() } 67 | 68 | private fun Chunk.cleanDenseEntities(): Int { 69 | // 最终要移除的实体 70 | val willBeRemoved = entities.toMutableList() 71 | if (willBeRemoved.isEmpty()) return 0 72 | PL.debug { "" } 73 | val chunkInfo = info() 74 | PL.debug { "开始检测区块${chunkInfo}中的密集实体" } 75 | PL.buildDebug { 76 | append("所有实体共").append(willBeRemoved.size).append("个: [") 77 | willBeRemoved.info().entries.joinTo(this, ", ") { (k, v) -> "$k: $v" } 78 | append("]") 79 | } 80 | val settings = chunkCfg.settings 81 | // chunkCfg.settings.name == true 时清理被命名的生物, false -> 从列表中移除(不清理) 82 | if (!settings.name) willBeRemoved.removeIf { it.customName != null } 83 | // chunkCfg.settings.lead == true 时清理拴绳拴住的生物, false -> 从列表中移除(不清理) 84 | if (!settings.lead) willBeRemoved.removeIf { it is LivingEntity && it.isLeashed } 85 | // chunkCfg.settings.mount == true 时清理乘骑中的生物, false 从列表中移除(不清理) 86 | if (!settings.mount) willBeRemoved.removeIf { it.isInsideVehicle || it.passengers.isNotEmpty() } 87 | 88 | var count = 0 89 | 90 | // 规则匹配 91 | chunkCfg.limit.entries.mapNotNull { (regex, limit) -> 92 | val matches = willBeRemoved.filter { it.type.name.matches(regex) }.toMutableList() 93 | val execute = matches.size > limit 94 | PL.debug { "检查区块($chunkInfo)的密集实体, 规则${regex}" } 95 | PL.buildDebug { 96 | append("匹配实体").append(matches.size).append("个(") 97 | if (execute) append("&c清理其中&a").append(matches.size - limit).append("个&b") 98 | else append("不清理") 99 | append("): ") 100 | matches.info().entries.joinTo(this, ", ", "[", "]") { (k, v) -> "$k: $v" } 101 | } 102 | 103 | // 不清理匹配数量未达阈值的 104 | if (!execute) return@mapNotNull null 105 | 106 | // 截取超出阈值的实体 107 | matches.subList(limit, matches.size).also { 108 | count += it.size 109 | // 从待清理中移除已匹配的实体 110 | willBeRemoved.removeAll(it) 111 | } 112 | }.forEach { 113 | it.forEach(Entity::remove) 114 | } 115 | if (!chunkCfg.format.isNullOrBlank()) { 116 | val recv = Bukkit.getOnlinePlayers().filter { it.hasPermission("eclean.admin") } 117 | entities.asList().info().filter { it.value > chunkCfg.count }.forEach { (entity, count) -> 118 | val message = chunkCfg.format!!.placeholder( 119 | "chunk" to chunkInfo, 120 | "entity" to entity, 121 | "count" to count, 122 | ) 123 | recv.forEach { PL.sendMsgWithPrefix(it, message) } 124 | } 125 | } 126 | return count 127 | } 128 | 129 | fun Chunk.info() = "x: ${x * 16}..${x * 16 + 15}, z: ${z * 16}..${z * 16 + 15}" -------------------------------------------------------------------------------- /src/main/kotlin/clean/drop.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.clean 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.Material 5 | import org.bukkit.World 6 | import org.bukkit.entity.Item 7 | import org.bukkit.inventory.meta.BookMeta 8 | import top.e404.eclean.PL 9 | import top.e404.eclean.config.Config 10 | import top.e404.eclean.util.isMatch 11 | import top.e404.eclean.util.noOnline 12 | import top.e404.eclean.util.noOnlineMessage 13 | import top.e404.eplugin.EPlugin.Companion.placeholder 14 | 15 | private inline val dropCfg get() = Config.config.drop 16 | private inline val trashcanCfg get() = Config.config.trashcan 17 | 18 | 19 | /** 20 | * 最近一次清理掉落物的数量 21 | */ 22 | var lastDrop = 0 23 | private set 24 | 25 | /** 26 | * 清理全服掉落物 27 | */ 28 | fun cleanDrop() { 29 | if (!dropCfg.enable) { 30 | PL.debug { "掉落物清理已禁用" } 31 | return 32 | } 33 | val worlds = Bukkit.getWorlds().filterNot { dropCfg.disableWorld.any { regex -> it.name matches regex } } 34 | PL.buildDebug { 35 | append("开始清理掉落物, 启用掉落物清理的世界: [") 36 | worlds.joinTo(this, ", ", transform = World::getName) 37 | append("]") 38 | } 39 | PL.debug { if (dropCfg.enchant) "不清理附魔的物品" else "清理附魔的物品" } 40 | PL.debug { if (dropCfg.writtenBook) "不清理成书" else "清理成书" } 41 | 42 | var time = System.currentTimeMillis() 43 | val result = worlds.map { it.cleanDrop() } 44 | time = System.currentTimeMillis() - time 45 | 46 | lastDrop = result.sumOf { it.first } 47 | PL.debug { "掉落物清理共${lastDrop}个, 耗时${time}ms" } 48 | 49 | if (noOnline) { 50 | if (noOnlineMessage) { 51 | val all = result.sumOf { it.second } 52 | val finish = dropCfg.finish 53 | if (finish.isNotBlank()) PL.broadcastMsg(finish.placeholder("clean" to lastDrop, "all" to all)) 54 | } 55 | } else { 56 | val all = result.sumOf { it.second } 57 | val finish = dropCfg.finish 58 | if (finish.isNotBlank()) PL.broadcastMsg(finish.placeholder("clean" to lastDrop, "all" to all)) 59 | } 60 | } 61 | 62 | /** 63 | * 清理指定世界的掉落物 64 | * 65 | * @return Pair(clean, all) 66 | */ 67 | fun World.cleanDrop(): Pair { 68 | PL.debug { "" } 69 | PL.debug { "开始清理世界${name}中的掉落物" } 70 | // 所有物品 71 | val waitingForClean = entities.filterIsInstance().toMutableList() 72 | PL.buildDebug { 73 | val items = mutableMapOf() 74 | append("世界").append(name).append("中的所有掉落物: [") 75 | for (item in waitingForClean) { 76 | items.compute(item.itemStack.type.name) { _, v -> (v ?: 0) + 1 } 77 | } 78 | items.entries.joinTo(this, ", ") { (k, v) -> "$k: $v" } 79 | append("]") 80 | } 81 | 82 | // dropCfg.enchant == true 时不清理附魔物品(从列表中移除) 83 | if (dropCfg.enchant) waitingForClean.removeIf { it.itemStack.itemMeta?.hasEnchants() == true } 84 | // dropCfg.writtenBook == true 时不清理写过的书(从列表中移除) 85 | if (dropCfg.writtenBook) waitingForClean.removeIf { 86 | it.itemStack.type == Material.WRITABLE_BOOK 87 | && (it.itemStack.itemMeta as? BookMeta)?.hasPages() == true 88 | } 89 | // dropCfg.lore == true 时不清理lore的物品(从列表中移除) 90 | if (dropCfg.lore) waitingForClean.removeIf { 91 | it.itemStack.itemMeta?.hasLore() == true 92 | } 93 | 94 | val items = mutableMapOf>() 95 | waitingForClean.forEach { 96 | items.getOrPut(it.itemStack.type.name) { mutableListOf() }.add(it) 97 | } 98 | 99 | // 黑名单 名字匹配的清理 名字不匹配的从列表中移除(不清理) 100 | if (dropCfg.black) items.entries.removeIf { (type, list) -> 101 | // 首个匹配的正则 102 | val matchesRegex = type.isMatch(dropCfg.match) 103 | // 没有匹配的 -> noMatch = true -> remove -> 从列表中移除 -> 不清理 104 | val noMatch = matchesRegex == null 105 | PL.buildDebug { 106 | if (noMatch) append("不") 107 | append("清理").append(type).append("x").append(list.size) 108 | if (matchesRegex != null) append(", 命中规则: ").append(matchesRegex.pattern) 109 | } 110 | noMatch 111 | } 112 | // 白名单 名字匹配的从列表中移除(不清理) 名字不匹配的清理 113 | else items.entries.removeIf { (type, list) -> 114 | val matchesRegex = type.isMatch(dropCfg.match) 115 | // 有匹配的 -> matches = true -> remove -> 从列表中移除 -> 不清理 116 | val matches = matchesRegex != null 117 | PL.buildDebug { 118 | if (matches) append("不") 119 | append("清理").append(type).append("x").append(list.size) 120 | if (matchesRegex != null) append(", 命中规则: ").append(matchesRegex.pattern) 121 | } 122 | matches 123 | } 124 | 125 | var count = 0 126 | // 启用从垃圾清理中收集掉落物 127 | if (trashcanCfg.enable && trashcanCfg.collect) { 128 | items.values.forEach { 129 | count += it.size 130 | Trashcan.addItems(it.map(Item::getItemStack)) 131 | it.forEach(Item::remove) 132 | } 133 | Trashcan.update() 134 | } 135 | // 不启用垃圾箱收集 136 | else { 137 | items.values.forEach { 138 | count += it.size 139 | it.forEach(Item::remove) 140 | } 141 | } 142 | PL.debug { "世界${name}掉落物清理完成($count/${waitingForClean.size})" } 143 | return count to waitingForClean.size 144 | } -------------------------------------------------------------------------------- /src/main/kotlin/clean/living.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.clean 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.World 5 | import org.bukkit.entity.LivingEntity 6 | import org.bukkit.entity.Player 7 | import top.e404.eclean.PL 8 | import top.e404.eclean.config.Config 9 | import top.e404.eclean.util.info 10 | import top.e404.eclean.util.isMatch 11 | import top.e404.eclean.util.noOnline 12 | import top.e404.eclean.util.noOnlineMessage 13 | import top.e404.eplugin.EPlugin.Companion.placeholder 14 | 15 | private inline val livingCfg get() = Config.config.living 16 | 17 | /** 18 | * 最近一次清理生物实体的数量 19 | */ 20 | var lastLiving = 0 21 | private set 22 | 23 | /** 24 | * 清理全服生物 25 | */ 26 | fun cleanLiving() { 27 | if (!livingCfg.enable) { 28 | PL.debug { "生物清理已禁用" } 29 | return 30 | } 31 | val worlds = Bukkit.getWorlds().filterNot { livingCfg.disableWorld.any { regex -> it.name matches regex } } 32 | PL.buildDebug { 33 | append("开始清理生物, 启用生物清理的世界: [") 34 | worlds.joinTo(this, ", ", transform = World::getName) 35 | append("]") 36 | } 37 | PL.debug { if (livingCfg.settings.name) "清理被命名的生物" else "不清理被命名的生物" } 38 | PL.debug { if (livingCfg.settings.lead) "清理拴绳拴住的生物" else "不清理拴绳拴住的生物" } 39 | PL.debug { if (livingCfg.settings.mount) "清理乘骑中的生物" else "不清理乘骑中的生物" } 40 | 41 | var time = System.currentTimeMillis() 42 | val result = worlds.map { it.cleanLiving() } 43 | time = System.currentTimeMillis() - time 44 | 45 | lastLiving = result.sumOf { it.first } 46 | PL.debug { "生物清理共${lastLiving}个, 耗时${time}ms" } 47 | 48 | if (noOnline) { 49 | if (noOnlineMessage) { 50 | val all = result.sumOf { it.second } 51 | val finish = livingCfg.finish 52 | if (finish.isNotBlank()) PL.broadcastMsg(finish.placeholder(mapOf("clean" to lastLiving, "all" to all))) 53 | } 54 | } else { 55 | val all = result.sumOf { it.second } 56 | val finish = livingCfg.finish 57 | if (finish.isNotBlank()) PL.broadcastMsg(finish.placeholder(mapOf("clean" to lastLiving, "all" to all))) 58 | } 59 | } 60 | 61 | /** 62 | * 清理指定世界的生物 63 | * 64 | * @return Pair(clean, all) 65 | */ 66 | fun World.cleanLiving(): Pair { 67 | val all = livingEntities.filterNot { it is Player }.toMutableList() 68 | val total = all.size 69 | 70 | PL.debug { "" } 71 | PL.debug { "开始清理世界${name}的生物" } 72 | PL.buildDebug { 73 | append("所有实体共").append(total).append("个: [") 74 | all.info().entries.joinTo(this, ", ") { (k, v) -> "$k: $v" } 75 | append("]") 76 | } 77 | 78 | // livingCfg.settings.name == false 时不清理命名的生物(从列表中移除) 79 | if (!livingCfg.settings.name) all.removeIf { it.customName != null } 80 | // livingCfg.settings.lead == false 时不清理拴绳拴住的生物(从列表中移除) 81 | if (!livingCfg.settings.lead) all.removeIf { it.isLeashed } 82 | // livingCfg.settings.mount == false 时不清理乘骑中的生物(从列表中移除) 83 | if (!livingCfg.settings.mount) all.removeIf { it.isInsideVehicle || it.passengers.isNotEmpty() } 84 | 85 | val groupBy = mutableMapOf>() 86 | for (entity in all) groupBy.getOrPut(entity.type.name) { mutableListOf() }.add(entity) 87 | 88 | // 黑名单 名字匹配的清理 名字不匹配的从列表中移除(不清理) 89 | if (livingCfg.black) groupBy.entries.removeIf { (type, list) -> 90 | // 首个匹配的正则 91 | val matchesRegex = type.isMatch(livingCfg.match) 92 | // 没有匹配的 -> noMatch = true -> remove -> 从列表中移除 -> 不清理 93 | val noMatch = matchesRegex == null 94 | PL.buildDebug { 95 | if (noMatch) append("不") 96 | append("清理").append(type).append("x").append(list.size) 97 | if (matchesRegex != null) append(", 命中规则: ").append(matchesRegex.pattern) 98 | } 99 | noMatch 100 | } 101 | // 白名单 名字匹配的从列表中移除(不清理) 名字不匹配的清理 102 | else groupBy.entries.removeIf { (type, list) -> 103 | val matchesRegex = type.isMatch(livingCfg.match) 104 | // 有匹配的 -> matches = true -> remove -> 从列表中移除 -> 不清理 105 | val matches = matchesRegex != null 106 | PL.buildDebug { 107 | if (matches) append("不") 108 | append("清理").append(type).append("x").append(list.size) 109 | if (matchesRegex != null) append(", 命中规则: ").append(matchesRegex.pattern) 110 | } 111 | matches 112 | } 113 | 114 | var count = 0 115 | groupBy.values.forEach { 116 | count += it.size 117 | it.forEach(LivingEntity::remove) 118 | } 119 | PL.debug { "世界${name}生物清理完成($count/${total})" } 120 | return count to total 121 | } -------------------------------------------------------------------------------- /src/main/kotlin/command/Clean.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.command.CommandSender 5 | import top.e404.eclean.PL 6 | import top.e404.eclean.clean.* 7 | import top.e404.eclean.clean.Clean 8 | import top.e404.eclean.clean.Trashcan.cleanTrash 9 | import top.e404.eclean.config.Lang 10 | import top.e404.eplugin.command.ECommand 11 | 12 | object Clean : ECommand( 13 | PL, 14 | "clean", 15 | "(?i)clean", 16 | false, 17 | "eclean.admin" 18 | ) { 19 | override val usage get() = Lang["command.usage.clean"] 20 | 21 | private val arg = listOf("entity", "drop", "chunk", "trash") 22 | override fun onTabComplete( 23 | sender: CommandSender, 24 | args: Array, 25 | complete: MutableList, 26 | ) { 27 | when (args.size) { 28 | 2 -> complete.addAll(arg) 29 | 3 -> Bukkit.getWorlds().forEach { complete.add(it.name) } 30 | } 31 | } 32 | 33 | override fun onCommand( 34 | sender: CommandSender, 35 | args: Array, 36 | ) { 37 | when (args.size) { 38 | 1 -> Clean.clean() 39 | 2 -> when (args[1].lowercase()) { 40 | "e", "entity" -> cleanLiving() 41 | "d", "drop" -> cleanDrop() 42 | "c", "chunk" -> cleanDenseEntities() 43 | "t", "trash" -> cleanTrash() 44 | else -> sender.sendMessage(usage) 45 | } 46 | 47 | 3 -> { 48 | val world = Bukkit.getWorld(args[2]) 49 | if (world == null) { 50 | PL.sendMsgWithPrefix(sender, Lang["message.invalid_world", "world" to args[2]]) 51 | return 52 | } 53 | val count = when (args[1].lowercase()) { 54 | "e", "entity" -> world.cleanLiving().run { "($first/$second)" } 55 | "d", "drop" -> world.cleanDrop().run { "($first/$second)" } 56 | "c", "chunk" -> world.cleanChunkDenseEntities() 57 | else -> { 58 | sender.sendMessage(usage) 59 | return 60 | } 61 | } 62 | PL.sendMsgWithPrefix(sender, Lang["command.clean_done", "count" to count]) 63 | } 64 | 65 | else -> sender.sendMessage(usage) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/kotlin/command/Commands.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import top.e404.eclean.PL 4 | import top.e404.eplugin.command.ECommandManager 5 | 6 | object Commands : ECommandManager( 7 | PL, 8 | "eclean", 9 | Debug, 10 | Reload, 11 | Clean, 12 | Stats, 13 | EntityStats, 14 | Trash, 15 | Players, 16 | Show 17 | ) 18 | -------------------------------------------------------------------------------- /src/main/kotlin/command/Debug.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.command.CommandSender 4 | import org.bukkit.entity.Player 5 | import top.e404.eclean.PL 6 | import top.e404.eclean.config.Config 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.command.AbstractDebugCommand 9 | 10 | /** 11 | * debug指令 12 | */ 13 | object Debug : AbstractDebugCommand( 14 | PL, 15 | "eclean.admin" 16 | ) { 17 | override val usage get() = Lang["command.usage.debug"] 18 | 19 | override fun onCommand( 20 | sender: CommandSender, 21 | args: Array, 22 | ) { 23 | if (sender !is Player) { 24 | if (Config.config.debug) { 25 | Config.config.debug = false 26 | plugin.sendMsgWithPrefix(sender, Lang["debug.console_disable"]) 27 | } else { 28 | Config.config.debug = true 29 | plugin.sendMsgWithPrefix(sender, Lang["debug.console_enable"]) 30 | } 31 | return 32 | } 33 | val senderName = sender.name 34 | if (senderName in plugin.debuggers) { 35 | plugin.debuggers.remove(senderName) 36 | plugin.sendMsgWithPrefix(sender, Lang["debug.player_disable"]) 37 | } else { 38 | plugin.debuggers.add(senderName) 39 | plugin.sendMsgWithPrefix(sender, Lang["debug.player_enable"]) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/command/EntityStats.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.command.CommandSender 5 | import org.bukkit.entity.EntityType 6 | import org.bukkit.entity.Player 7 | import top.e404.eclean.PL 8 | import top.e404.eclean.config.Lang 9 | import top.e404.eplugin.command.ECommand 10 | 11 | object EntityStats : ECommand( 12 | PL, 13 | "entity", 14 | "(?i)e|entity", 15 | false, 16 | "eclean.admin" 17 | ) { 18 | override val usage get() = Lang["command.usage.entity"] 19 | 20 | override fun onTabComplete( 21 | sender: CommandSender, 22 | args: Array, 23 | complete: MutableList, 24 | ) { 25 | when (args.size) { 26 | 2 -> EntityType.values().forEach { complete.add(it.name) } 27 | 3 -> Bukkit.getWorlds().forEach { complete.add(it.name) } 28 | } 29 | } 30 | 31 | override fun onCommand( 32 | sender: CommandSender, 33 | args: Array, 34 | ) { 35 | when (args.size) { 36 | 2 -> { 37 | if (!PL.isPlayer(sender)) return 38 | sender.sendEntityStats((sender as Player).world.name, args[1]) 39 | } 40 | 41 | 3 -> sender.sendEntityStats(args[2], args[1]) 42 | 4 -> { 43 | val min = args[3].toIntOrNull() 44 | if (min == null) { 45 | PL.sendMsgWithPrefix(sender, Lang["message.invalid_number", "number" to args[3]]) 46 | return 47 | } 48 | sender.sendEntityStats(args[2], args[1], min) 49 | } 50 | 51 | else -> sender.sendMessage(usage) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/kotlin/command/Players.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.command.CommandSender 5 | import top.e404.eclean.PL 6 | import top.e404.eclean.config.Lang 7 | import top.e404.eplugin.EPlugin.Companion.color 8 | import top.e404.eplugin.command.ECommand 9 | 10 | object Players : ECommand( 11 | PL, 12 | "players", 13 | "(?i)p|players?", 14 | false, 15 | "eclean.admin" 16 | ) { 17 | override val usage get() = Lang["command.usage.players"] 18 | 19 | override fun onCommand( 20 | sender: CommandSender, 21 | args: Array, 22 | ) { 23 | Bukkit.getOnlinePlayers().groupBy { it.world }.forEach { (world, list) -> 24 | val s = list.joinToString { "\n &b${it.name}&f: ${it.location.run { "$blockX $blockY $blockZ" }}" } 25 | sender.sendMessage("&6${world.name}:$s".color) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/command/Reload.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.command.CommandSender 4 | import top.e404.eclean.PL 5 | import top.e404.eclean.clean.Clean 6 | import top.e404.eclean.config.Config 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.command.ECommand 9 | 10 | object Reload : ECommand( 11 | PL, 12 | "reload", 13 | "(?i)r|reload", 14 | false, 15 | "eclean.admin" 16 | ) { 17 | override val usage get() = Lang["command.usage.reload"] 18 | 19 | override fun onCommand(sender: CommandSender, args: Array) { 20 | plugin.runTaskAsync { 21 | Lang.load(sender) 22 | Config.load(sender) 23 | plugin.runTask { 24 | Clean.schedule() 25 | plugin.sendMsgWithPrefix(sender, Lang["command.reload_done"]) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/command/Show.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.command.CommandSender 5 | import org.bukkit.entity.Entity 6 | import org.bukkit.entity.Player 7 | import top.e404.eclean.PL 8 | import top.e404.eclean.config.Config 9 | import top.e404.eclean.config.Lang 10 | import top.e404.eclean.menu.MenuManager 11 | import top.e404.eclean.menu.dense.DenseMenu 12 | import top.e404.eclean.menu.dense.EntityInfo 13 | import top.e404.eplugin.command.ECommand 14 | 15 | object Show : ECommand( 16 | PL, 17 | "show", 18 | "(?i)s|show", 19 | true, 20 | "eclean.admin" 21 | ) { 22 | override val usage get() = Lang["command.usage.show"] 23 | 24 | override fun onCommand(sender: CommandSender, args: Array) { 25 | sender as Player 26 | plugin.runTaskAsync { 27 | val data = Bukkit.getServer().worlds.flatMap { world -> 28 | world.loadedChunks.toList() 29 | }.flatMap { chunk -> 30 | chunk.entities.groupBy(Entity::getType).filter { (_, list) -> 31 | list.size > Config.config.chunk.count 32 | }.map { (type, list) -> 33 | EntityInfo(type, list.size, chunk) 34 | } 35 | }.sortedByDescending { it.amount }.toMutableList() 36 | plugin.runTask { 37 | MenuManager.openMenu(DenseMenu(data), sender) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/command/Stats.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.command.CommandSender 5 | import org.bukkit.entity.Player 6 | import top.e404.eclean.PL 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.EPlugin.Companion.color 9 | import top.e404.eplugin.command.ECommand 10 | 11 | object Stats : ECommand( 12 | PL, 13 | "stats", 14 | "(?i)s|stats", 15 | false, 16 | "eclean.admin" 17 | ) { 18 | override val usage get() = Lang["command.usage.stats"].color 19 | 20 | override fun onTabComplete( 21 | sender: CommandSender, 22 | args: Array, 23 | complete: MutableList, 24 | ) { 25 | if (args.size == 2) Bukkit.getWorlds().forEach { complete.add(it.name) } 26 | } 27 | 28 | override fun onCommand( 29 | sender: CommandSender, 30 | args: Array, 31 | ) { 32 | when (args.size) { 33 | 1 -> { 34 | if (!PL.isPlayer(sender)) return 35 | sender.sendWorldStats((sender as Player).world.name) 36 | } 37 | 38 | 2 -> sender.sendWorldStats(args[1]) 39 | else -> sender.sendMessage(usage) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/command/Trash.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.command.CommandSender 4 | import org.bukkit.entity.Player 5 | import top.e404.eclean.PL 6 | import top.e404.eclean.clean.Trashcan 7 | import top.e404.eclean.config.Config 8 | import top.e404.eclean.config.Lang 9 | import top.e404.eplugin.command.ECommand 10 | 11 | object Trash : ECommand( 12 | PL, 13 | "trash", 14 | "(?i)t|trash", 15 | true, 16 | "eclean.trash" 17 | ) { 18 | override val usage get() = Lang["command.usage.trash"] 19 | 20 | override fun onCommand(sender: CommandSender, args: Array) { 21 | sender as Player 22 | if (!Config.config.trashcan.enable) { 23 | plugin.sendMsgWithPrefix(sender, Lang["command.trash_disable"]) 24 | return 25 | } 26 | Trashcan.open(sender) 27 | plugin.sendMsgWithPrefix(sender, Lang["command.trash_open"]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/command/check.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.command 2 | 3 | import org.bukkit.Bukkit 4 | import org.bukkit.command.CommandSender 5 | import org.bukkit.entity.EntityType 6 | import top.e404.eclean.PL 7 | import top.e404.eclean.clean.info 8 | import top.e404.eclean.config.Lang 9 | import top.e404.eplugin.EPlugin.Companion.formatAsConst 10 | import top.e404.eplugin.util.mcVer 11 | 12 | fun CommandSender.sendWorldStats(worldName: String) { 13 | val world = Bukkit.getWorld(worldName) 14 | if (world == null) { 15 | PL.sendMsgWithPrefix(this, "&c不存在名为&e$worldName&c的世界") 16 | return 17 | } 18 | val list = world 19 | .entities 20 | .groupBy { it.type } 21 | .map { (k, v) -> k to v.size } 22 | .sortedByDescending { it.second } 23 | if (list.isEmpty()) { 24 | PL.sendMsgWithPrefix(this, Lang["command.stats.empty"]) 25 | return 26 | } 27 | val entity = list.joinToString(Lang["command.stats.spacing"]) { (k, v) -> 28 | Lang[ 29 | "command.stats.content", 30 | "type" to k, 31 | "count" to v.withColor() 32 | ] 33 | } 34 | PL.sendMsgWithPrefix( 35 | this, 36 | Lang[ 37 | "command.stats.world", 38 | "world" to worldName, 39 | "count" to world.loadedChunks.size, 40 | "force" to if (mcVer!!.major < 13) null else world.loadedChunks.count { it.isForceLoaded }, 41 | "entity" to entity 42 | ] 43 | ) 44 | } 45 | 46 | fun CommandSender.sendEntityStats(worldName: String, typeName: String, min: Int = 0) { 47 | val world = Bukkit.getWorld(worldName) 48 | if (world == null) { 49 | PL.sendMsgWithPrefix(this, "&c不存在名为&e${worldName}&c的世界") 50 | return 51 | } 52 | val type = try { 53 | EntityType.valueOf(typeName.formatAsConst()) 54 | } catch (t: Throwable) { 55 | PL.sendMsgWithPrefix(this, Lang["message.invalid_entity_type"]) 56 | return 57 | } 58 | val list = world 59 | .loadedChunks 60 | .map { it.info() to it.entities.count { e -> e.type == type } } 61 | .filter { it.second > min } 62 | .sortedByDescending { e -> e.second } 63 | if (list.isEmpty()) { 64 | PL.sendMsgWithPrefix(this, Lang["command.stats.empty"]) 65 | return 66 | } 67 | val entity = list.joinToString(Lang["command.stats.spacing"]) { (k, v) -> 68 | Lang[ 69 | "command.stats.content", 70 | "type" to k, 71 | "count" to v.withColor() 72 | ] 73 | } 74 | PL.sendMsgWithPrefix( 75 | this, 76 | Lang[ 77 | "command.stats.entity", 78 | "type" to typeName, 79 | "entity" to entity 80 | ] 81 | ) 82 | } 83 | 84 | private fun Int.withColor() = when { 85 | this > 60 -> "&c$this" 86 | this > 30 -> "&e$this" 87 | else -> "&a$this" 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/config/Config.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.config 2 | 3 | import com.charleskorn.kaml.Yaml 4 | import com.charleskorn.kaml.YamlConfiguration 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | import org.bukkit.Bukkit 8 | import org.bukkit.command.CommandSender 9 | import top.e404.eclean.PL 10 | import top.e404.eclean.clean.Trashcan 11 | import top.e404.eplugin.config.JarConfigDefault 12 | import top.e404.eplugin.config.KtxConfig 13 | import top.e404.eplugin.config.serialization.RegexSerialization 14 | 15 | object Config : KtxConfig( 16 | plugin = PL, 17 | path = "config.yml", 18 | default = JarConfigDefault(PL, "config.yml"), 19 | serializer = ConfigData.serializer(), 20 | format = Yaml(configuration = YamlConfiguration(strictMode = false)) 21 | ) { 22 | // 加载时处理清理task 23 | override fun onLoad(config: ConfigData, sender: CommandSender?) { 24 | if (Bukkit.isPrimaryThread()) { 25 | Trashcan.schedule() 26 | return 27 | } 28 | plugin.runTask { Trashcan.schedule() } 29 | } 30 | } 31 | 32 | @Serializable 33 | data class ConfigData( 34 | var debug: Boolean = false, 35 | var update: Boolean = true, 36 | var duration: Long, 37 | var message: MutableMap, 38 | var living: LivingConfig, 39 | var drop: DropConfig, 40 | var chunk: ChunkConfig, 41 | var trashcan: TrashcanConfig, 42 | @SerialName("no_online") 43 | var noOnline: NoOnlineConfig = NoOnlineConfig(), 44 | ) 45 | 46 | @Serializable 47 | data class DropConfig( 48 | var enable: Boolean = true, 49 | @SerialName("disable_world") 50 | var disableWorld: MutableList<@Serializable(RegexSerialization::class) Regex> = mutableListOf(), 51 | var finish: String = "", 52 | @SerialName("is_black") 53 | var black: Boolean = true, 54 | var enchant: Boolean = false, 55 | var lore: Boolean = false, 56 | @SerialName("written_book") 57 | var writtenBook: Boolean = false, 58 | var match: MutableList<@Serializable(RegexSerialization::class) Regex> = mutableListOf(), 59 | ) 60 | 61 | @Serializable 62 | data class LivingConfig( 63 | var enable: Boolean = true, 64 | @SerialName("disable_world") 65 | var disableWorld: MutableList<@Serializable(RegexSerialization::class) Regex> = mutableListOf(), 66 | var finish: String = "", 67 | var settings: Settings = Settings(), 68 | @SerialName("is_black") 69 | var black: Boolean = true, 70 | var match: MutableList<@Serializable(RegexSerialization::class) Regex> = mutableListOf(), 71 | ) 72 | 73 | @Serializable 74 | data class Settings( 75 | var name: Boolean = false, 76 | var lead: Boolean = false, 77 | var mount: Boolean = false, 78 | ) 79 | 80 | @Serializable 81 | data class ChunkConfig( 82 | var enable: Boolean = true, 83 | @SerialName("disable_world") 84 | var disableWorld: MutableList<@Serializable(RegexSerialization::class) Regex> = mutableListOf(), 85 | var finish: String = "", 86 | var settings: Settings = Settings(), 87 | var count: Int = 50, 88 | var format: String? = null, 89 | var limit: MutableMap<@Serializable(RegexSerialization::class) Regex, Int> = mutableMapOf(), 90 | ) 91 | 92 | @Serializable 93 | data class TrashcanConfig( 94 | var enable: Boolean = true, 95 | var collect: Boolean = true, 96 | var duration: Long? = 6000, 97 | ) 98 | 99 | @Serializable 100 | data class NoOnlineConfig( 101 | var clean: Boolean = true, 102 | var message: Boolean = true, 103 | ) 104 | -------------------------------------------------------------------------------- /src/main/kotlin/config/Lang.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.config 2 | 3 | import top.e404.eclean.PL 4 | import top.e404.eplugin.config.ELangManager 5 | 6 | object Lang : ELangManager(PL) -------------------------------------------------------------------------------- /src/main/kotlin/hook/PapiHook.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.hook 2 | 3 | import top.e404.eclean.PL 4 | import top.e404.eplugin.hook.EHookManager 5 | import top.e404.eplugin.hook.placeholderapi.PlaceholderAPIHook 6 | 7 | object HookManager : EHookManager(PL, PapiHook) 8 | 9 | object PapiHook : PlaceholderAPIHook(PL) -------------------------------------------------------------------------------- /src/main/kotlin/menu/MenuManager.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu 2 | 3 | import org.bukkit.Location 4 | import org.bukkit.entity.Player 5 | import org.bukkit.event.EventHandler 6 | import org.bukkit.event.player.PlayerQuitEvent 7 | import org.bukkit.scheduler.BukkitTask 8 | import top.e404.eclean.PL 9 | import top.e404.eplugin.menu.EMenuManager 10 | 11 | object MenuManager : EMenuManager(PL) { 12 | val temps = mutableMapOf() 13 | 14 | @EventHandler 15 | fun PlayerQuitEvent.onEvent() { 16 | // 30s内退出游戏则传送回之前的位置 17 | temps.remove(player)?.run { 18 | task.cancel() 19 | player.teleport(location) 20 | } 21 | } 22 | } 23 | 24 | 25 | data class Temp( 26 | val player: Player, 27 | val location: Location, 28 | val task: BukkitTask 29 | ) 30 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/dense/DenseMenu.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.dense 2 | 3 | import org.bukkit.Material 4 | import org.bukkit.Sound 5 | import org.bukkit.enchantments.Enchantment 6 | import org.bukkit.entity.Player 7 | import org.bukkit.event.inventory.InventoryClickEvent 8 | import org.bukkit.inventory.ItemFlag 9 | import top.e404.eclean.PL 10 | import top.e404.eclean.config.Lang 11 | import top.e404.eplugin.menu.menu.ChestMenu 12 | import top.e404.eplugin.menu.slot.MenuButton 13 | import top.e404.eplugin.util.buildItemStack 14 | 15 | class DenseMenu(data: MutableList) : ChestMenu(PL, 6, Lang["menu.dense.title"], false) { 16 | val zone = DenseZone(this, data) 17 | var temp = false 18 | private val prev = PrevButton(this) 19 | private val next = NextButton(this) 20 | 21 | init { 22 | initSlots( 23 | listOf( 24 | " ", 25 | " ", 26 | " ", 27 | " ", 28 | " ", 29 | " p t n ", 30 | ) 31 | ) { _, char -> 32 | when (char) { 33 | 'p' -> prev 34 | 'n' -> next 35 | 't' -> object : MenuButton(this) { 36 | private fun create() = buildItemStack( 37 | Material.PAPER, 38 | 1, 39 | Lang["menu.dense.temp.name"], 40 | Lang["menu.dense.temp.lore", "status" to Lang["menu.dense.temp.status.$temp"]].lines() 41 | ) { 42 | if (temp) { 43 | addEnchant(Enchantment.DURABILITY, 1, true) 44 | addItemFlags(ItemFlag.HIDE_ENCHANTS) 45 | } 46 | } 47 | 48 | override var item = create() 49 | 50 | override fun onClick(slot: Int, event: InventoryClickEvent): Boolean { 51 | temp = !temp 52 | val player = event.whoClicked as Player 53 | player.playSound(player.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1F, 1F) 54 | menu.updateIcon() 55 | return true 56 | } 57 | 58 | override fun updateItem() { 59 | item = create() 60 | } 61 | } 62 | 63 | else -> null 64 | } 65 | } 66 | zones.add(zone) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/dense/DenseZone.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.dense 2 | 3 | import org.bukkit.Location 4 | import org.bukkit.entity.Entity 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.inventory.InventoryClickEvent 7 | import top.e404.eclean.PL 8 | import top.e404.eclean.clean.info 9 | import top.e404.eclean.config.Lang 10 | import top.e404.eclean.menu.MenuManager 11 | import top.e404.eclean.menu.Temp 12 | import top.e404.eplugin.menu.zone.MenuButtonZone 13 | 14 | class DenseZone( 15 | override val menu: DenseMenu, 16 | override val data: MutableList 17 | ) : MenuButtonZone(menu, 0, 0, 9, 5, data) { 18 | override val inv = menu.inv 19 | override fun onClick(menuIndex: Int, zoneIndex: Int, itemIndex: Int, event: InventoryClickEvent): Boolean { 20 | val info = data.getOrNull(itemIndex) ?: return true 21 | val player = event.whoClicked as Player 22 | // 右键点击清理区块实体 23 | if (event.isRightClick) { 24 | val entities = info.chunk.entities.filter { it.type == info.type } 25 | PL.sendMsgWithPrefix( 26 | player, 27 | Lang[ 28 | "menu.dense.clean", 29 | "chunk" to info.chunk.info(), 30 | "type" to info.type.name, 31 | "count" to entities.size 32 | ] 33 | ) 34 | entities.forEach(Entity::remove) 35 | data.removeAt(itemIndex) 36 | menu.updateIcon() 37 | return true 38 | } 39 | // 左键点击传送到区块 40 | val x = info.chunk.x * 16 + 8 41 | val z = info.chunk.z * 16 + 8 42 | val y = info.chunk.world.getHighestBlockYAt(x, z) 43 | val oldLocation = player.location 44 | player.teleport(Location(info.chunk.world, x + 0.5, y + 1.0, z + 0.5)) 45 | if (!menu.temp) { 46 | PL.sendMsgWithPrefix(player, Lang["command.teleport.done"]) 47 | return true 48 | } 49 | val exists = MenuManager.temps.remove(player) 50 | if (exists != null) { 51 | MenuManager.temps[player] = Temp( 52 | exists.player, 53 | exists.location, 54 | PL.runTaskLater(600) { 55 | MenuManager.temps.remove(player) 56 | player.teleport(exists.location) 57 | PL.sendMsgWithPrefix(player, Lang["command.teleport.cover"]) 58 | } 59 | ) 60 | return true 61 | } 62 | PL.sendMsgWithPrefix(player, Lang["command.teleport.temp"]) 63 | MenuManager.temps[player] = Temp( 64 | player, 65 | oldLocation, 66 | PL.runTaskLater(600) { 67 | MenuManager.temps.remove(player) 68 | player.teleport(oldLocation) 69 | PL.sendMsgWithPrefix(player, Lang["command.teleport.back"]) 70 | } 71 | ) 72 | return true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/dense/EntityInfo.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.dense 2 | 3 | import org.bukkit.Chunk 4 | import org.bukkit.Material 5 | import org.bukkit.entity.EntityType 6 | import top.e404.eclean.clean.info 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.EPlugin.Companion.placeholder 9 | import top.e404.eplugin.menu.Displayable 10 | import top.e404.eplugin.util.buildItemStack 11 | 12 | class EntityInfo( 13 | val type: EntityType, 14 | val amount: Int, 15 | val chunk: Chunk 16 | ) : Displayable { 17 | private companion object { 18 | val materials = Material.values().filter { it.name.contains("WOOL") } 19 | } 20 | 21 | override fun update() {} 22 | override var needUpdate = false 23 | override val item = run { 24 | val placeholder = arrayOf>( 25 | "type" to type.name, 26 | "amount" to amount, 27 | "chunk" to chunk.info(), 28 | ) 29 | buildItemStack( 30 | materials.random(), 31 | 1, 32 | Lang.get("menu.dense.item.name", *placeholder), 33 | Lang["menu.dense.item.lore"].placeholder(*placeholder).lines() 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/dense/NextButton.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.dense 2 | 3 | import org.bukkit.Material 4 | import org.bukkit.Sound 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.inventory.InventoryClickEvent 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.menu.slot.MenuButton 9 | import top.e404.eplugin.util.buildItemStack 10 | import top.e404.eplugin.util.emptyItem 11 | import kotlin.math.max 12 | 13 | class NextButton(val viewMenu: DenseMenu) : MenuButton(viewMenu) { 14 | val zone get() = viewMenu.zone 15 | private val btn = 16 | buildItemStack(Material.ARROW, 1, Lang["menu.dense.next.name"], Lang["menu.dense.next.lore"].lines()) 17 | 18 | override var item = if (zone.hasNext) btn else emptyItem 19 | override fun onClick( 20 | slot: Int, 21 | event: InventoryClickEvent, 22 | ): Boolean { 23 | if (zone.hasNext) { 24 | val player = event.whoClicked as Player 25 | player.playSound(player.location, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1F, 1F) 26 | zone.nextPage() 27 | menu.updateIcon() 28 | } 29 | return true 30 | } 31 | 32 | override fun updateItem() = 33 | if (!zone.hasNext) item = emptyItem 34 | else item = btn.also { it.amount = max(1, zone.page + 2) } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/dense/PrevButton.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.dense 2 | 3 | import org.bukkit.Material 4 | import org.bukkit.Sound 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.inventory.InventoryClickEvent 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.menu.slot.MenuButton 9 | import top.e404.eplugin.util.buildItemStack 10 | import top.e404.eplugin.util.emptyItem 11 | import kotlin.math.max 12 | 13 | class PrevButton(viewMenu: DenseMenu) : MenuButton(viewMenu) { 14 | val zone = viewMenu.zone 15 | private val btn = 16 | buildItemStack(Material.ARROW, 1, Lang["menu.dense.prev.name"], Lang["menu.dense.prev.lore"].lines()) 17 | 18 | override var item = if (zone.hasPrev) btn else emptyItem 19 | override fun onClick( 20 | slot: Int, 21 | event: InventoryClickEvent, 22 | ): Boolean { 23 | if (zone.hasPrev) { 24 | val player = event.whoClicked as Player 25 | player.playSound(player.location, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1F, 1F) 26 | zone.prevPage() 27 | menu.updateIcon() 28 | } 29 | return true 30 | } 31 | 32 | override fun updateItem() = 33 | if (!zone.hasPrev) item = emptyItem 34 | else item = btn.also { it.amount = max(1, zone.page) } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/trashcan/NextButton.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.trashcan 2 | 3 | import org.bukkit.Material 4 | import org.bukkit.Sound 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.inventory.InventoryClickEvent 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.menu.slot.MenuButton 9 | import top.e404.eplugin.util.buildItemStack 10 | import top.e404.eplugin.util.emptyItem 11 | import kotlin.math.max 12 | 13 | class NextButton(val viewMenu: TrashcanMenu) : MenuButton(viewMenu) { 14 | val zone get() = viewMenu.zone 15 | private val btn = 16 | buildItemStack(Material.ARROW, 1, Lang["menu.trashcan.next.name"], Lang["menu.trashcan.next.lore"].lines()) 17 | 18 | override var item = if (zone.hasNext) btn else emptyItem 19 | override fun onClick( 20 | slot: Int, 21 | event: InventoryClickEvent, 22 | ): Boolean { 23 | if (zone.hasNext) { 24 | val player = event.whoClicked as Player 25 | player.playSound(player.location, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1F, 1F) 26 | zone.nextPage() 27 | menu.updateIcon() 28 | } 29 | return true 30 | } 31 | 32 | override fun updateItem() = 33 | if (!zone.hasNext) item = emptyItem 34 | else item = btn.also { it.amount = max(1, zone.page + 2) } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/trashcan/PrevButton.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.trashcan 2 | 3 | import org.bukkit.Material 4 | import org.bukkit.Sound 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.inventory.InventoryClickEvent 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.menu.slot.MenuButton 9 | import top.e404.eplugin.util.buildItemStack 10 | import top.e404.eplugin.util.emptyItem 11 | import kotlin.math.max 12 | 13 | class PrevButton(viewMenu: TrashcanMenu) : MenuButton(viewMenu) { 14 | val zone = viewMenu.zone 15 | private val btn = 16 | buildItemStack(Material.ARROW, 1, Lang["menu.trashcan.prev.name"], Lang["menu.trashcan.prev.lore"].lines()) 17 | 18 | override var item = if (zone.hasPrev) btn else emptyItem 19 | override fun onClick( 20 | slot: Int, 21 | event: InventoryClickEvent, 22 | ): Boolean { 23 | if (zone.hasPrev) { 24 | val player = event.whoClicked as Player 25 | player.playSound(player.location, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1F, 1F) 26 | zone.prevPage() 27 | menu.updateIcon() 28 | } 29 | return true 30 | } 31 | 32 | override fun updateItem() = 33 | if (!zone.hasPrev) item = emptyItem 34 | else item = btn.also { it.amount = max(1, zone.page) } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/trashcan/TrashInfo.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.trashcan 2 | 3 | import org.bukkit.inventory.ItemStack 4 | import top.e404.eclean.config.Lang 5 | import top.e404.eplugin.menu.Displayable 6 | import top.e404.eplugin.util.editItemMeta 7 | 8 | data class TrashInfo( 9 | val origin: ItemStack, 10 | var amount: Int, 11 | ) : Displayable { 12 | private val placeholders = arrayOf>("amount" to amount) 13 | 14 | override fun update() { 15 | placeholders[0] = "amount" to amount 16 | item = generateItem(placeholders) 17 | } 18 | 19 | override var needUpdate = false 20 | override var item = generateItem(placeholders) 21 | 22 | private fun generateItem(placeholders: Array>) = origin.clone().editItemMeta { 23 | lore = (lore ?: mutableListOf()).apply { 24 | addAll(Lang.get("menu.trashcan.item.lore", *placeholders).removeSuffix("\n").lines()) 25 | } 26 | }.apply { amount = 1 } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/menu/trashcan/TrashcanMenu.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.trashcan 2 | 3 | import org.bukkit.event.inventory.InventoryClickEvent 4 | import org.bukkit.inventory.ItemStack 5 | import top.e404.eclean.PL 6 | import top.e404.eclean.clean.Trashcan 7 | import top.e404.eclean.config.Lang 8 | import top.e404.eplugin.menu.menu.ChestMenu 9 | 10 | class TrashcanMenu : ChestMenu(PL, 6, Lang["menu.trashcan.title"], true) { 11 | val zone = TrashcanZone(this, Trashcan.trashValues) 12 | private val prev = PrevButton(this) 13 | private val next = NextButton(this) 14 | 15 | init { 16 | initSlots( 17 | listOf( 18 | " ", 19 | " ", 20 | " ", 21 | " ", 22 | " ", 23 | " p n ", 24 | ) 25 | ) { _, char -> 26 | when (char) { 27 | 'p' -> prev 28 | 'n' -> next 29 | else -> null 30 | } 31 | } 32 | zones.add(zone) 33 | } 34 | 35 | override fun onClickSelfInv(event: InventoryClickEvent) { 36 | super.onClickSelfInv(event) 37 | zone.onClickSelfInv(event) 38 | } 39 | 40 | override fun onShiftPutin(clicked: ItemStack, event: InventoryClickEvent): Boolean { 41 | super.onShiftPutin(clicked, event) 42 | zone.onShiftPutin(clicked, event) 43 | return true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/kotlin/menu/trashcan/TrashcanZone.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.menu.trashcan 2 | 3 | import org.bukkit.Material 4 | import org.bukkit.Sound 5 | import org.bukkit.entity.Player 6 | import org.bukkit.event.inventory.ClickType 7 | import org.bukkit.event.inventory.InventoryClickEvent 8 | import org.bukkit.inventory.ItemStack 9 | import top.e404.eclean.PL 10 | import top.e404.eclean.clean.Trashcan 11 | import top.e404.eclean.clean.Trashcan.sign 12 | import top.e404.eplugin.menu.zone.MenuButtonZone 13 | import top.e404.eplugin.util.emptyItem 14 | import top.e404.eplugin.util.splitByPage 15 | import kotlin.math.max 16 | import kotlin.math.min 17 | 18 | class TrashcanZone( 19 | override val menu: TrashcanMenu, 20 | override val data: MutableList 21 | ) : MenuButtonZone(menu, 0, 0, 9, 5, data) { 22 | override val inv = menu.inv 23 | 24 | override fun update() { 25 | if (page != 0 && page * pageSize >= data.size) page-- 26 | val byPage = data.splitByPage(pageSize, page) 27 | for (i in 0 until pageSize) { 28 | val displayable = byPage.getOrNull(i) 29 | // 不在列表中的设置为空 30 | if (displayable == null) { 31 | menu.inv.setItem(zone2menu(i)!!, emptyItem) 32 | continue 33 | } 34 | // 更新图标 35 | displayable.update() 36 | // 更新菜单物品 37 | menu.inv.setItem(zone2menu(i)!!, displayable.item) 38 | } 39 | } 40 | 41 | override fun onClick(menuIndex: Int, zoneIndex: Int, itemIndex: Int, event: InventoryClickEvent): Boolean { 42 | val player = event.whoClicked as Player 43 | val info = data.getOrNull(itemIndex) ?: return true 44 | // 计划拿取的物品数量 45 | val planTake = when (event.click) { 46 | // 左键 拿一个 47 | ClickType.LEFT, ClickType.DOUBLE_CLICK -> 1 48 | // shift + 左键 拿一组 49 | ClickType.SHIFT_LEFT -> info.item.maxStackSize 50 | // 右键 拿一半 51 | ClickType.RIGHT -> max(min(info.item.maxStackSize / 2, info.amount / 2), 1) 52 | // 其他点击方式 不拿 53 | else -> { 54 | player.playSound(player.location, Sound.ENTITY_BLAZE_DEATH, 1F, 1F) 55 | return true 56 | } 57 | }.let { min(it, info.amount) } 58 | 59 | // 要拿取的物品数量 60 | var waitForTake = planTake 61 | PL.debug { "玩家${player.name}计划从公共垃圾桶中拿取${info.origin.type}x${planTake}, 预计剩余${info.amount - planTake}" } 62 | val maxStackSize = info.origin.type.maxStackSize 63 | // 遍历背包 64 | for (i in (0 until 36)) { 65 | if (waitForTake == 0) break 66 | require(waitForTake > 0) 67 | 68 | val item = player.inventory.getItem(i) 69 | // 空槽位 70 | if (item == null || item.type == Material.AIR) { 71 | val count = min(waitForTake, maxStackSize) 72 | waitForTake -= count 73 | player.inventory.setItem(i, info.origin.clone().apply { amount = count }) 74 | continue 75 | } 76 | // 类型不一致 77 | if (!item.isSimilar(info.origin)) continue 78 | // full stack 79 | if (item.amount >= maxStackSize) continue 80 | // 同类型合并 81 | val count = min(waitForTake, maxStackSize - item.amount) 82 | waitForTake -= count 83 | player.inventory.setItem(i, item.clone().apply { amount += count }) 84 | } 85 | 86 | // 此时total的数量是info中剩余物品的数量 87 | 88 | // 所有拿取的数量 89 | val totalTake = planTake - waitForTake 90 | PL.debug { "玩家${player.name}实际从公共垃圾桶中拿取${info.origin.type}x${totalTake}, 实际剩余${info.amount - totalTake}" } 91 | 92 | // 从垃圾桶中移除拿取的部分 93 | info.amount -= totalTake 94 | require(info.amount >= 0) 95 | 96 | // 拿取了全部物品 97 | if (info.amount == 0) { 98 | Trashcan.trashData.remove(info.origin.sign()) 99 | Trashcan.trashValues.removeAt(itemIndex) 100 | } 101 | 102 | // 更新垃圾桶 103 | Trashcan.update() 104 | return true 105 | } 106 | 107 | fun onClickSelfInv(event: InventoryClickEvent) { 108 | val player = event.whoClicked as Player 109 | event.isCancelled = true 110 | val clicked = event.currentItem 111 | if (clicked == null || clicked.type == Material.AIR) return 112 | // 放入的物品数量 113 | val count = when (event.click) { 114 | // 左键 放入一个 115 | ClickType.LEFT, ClickType.DOUBLE_CLICK -> 1 116 | // shift + 左键 放入全部 117 | ClickType.SHIFT_LEFT -> clicked.amount 118 | // 右键 放入一半 119 | ClickType.RIGHT -> max(clicked.amount / 2, 1) 120 | 121 | // 其他点击方式 不放 122 | else -> { 123 | player.playSound(player.location, Sound.ENTITY_BLAZE_DEATH, 1F, 1F) 124 | return 125 | } 126 | } 127 | PL.debug { "玩家${player.name}向公共垃圾桶中放入${clicked.type}x${count}, 剩余${clicked.amount - count}" } 128 | // 全部放入 129 | if (count == clicked.amount) { 130 | event.currentItem = emptyItem 131 | Trashcan.addItem(clicked) 132 | Trashcan.update() 133 | return 134 | } 135 | // 放入指定数量的 136 | clicked.amount -= count 137 | event.currentItem = clicked 138 | Trashcan.addItem(clicked.clone().apply { amount = count }) 139 | Trashcan.update() 140 | } 141 | 142 | /** 143 | * shift将选择的ItemStack全部放入垃圾桶 144 | * 145 | * @param clicked 点击的物品 146 | * @return 147 | */ 148 | fun onShiftPutin(clicked: ItemStack, event: InventoryClickEvent) { 149 | if (clicked.type == Material.AIR) return 150 | Trashcan.addItem(clicked) 151 | Trashcan.update() 152 | event.whoClicked.inventory.setItem(event.slot, emptyItem) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/kotlin/papi/Papi.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.papi 2 | 3 | import org.bukkit.OfflinePlayer 4 | import org.bukkit.entity.Player 5 | import top.e404.eclean.PL 6 | import top.e404.eclean.clean.* 7 | import top.e404.eclean.config.Config 8 | import top.e404.eplugin.hook.placeholderapi.PapiExpansion 9 | import top.e404.eplugin.util.parseSecondAsDuration 10 | 11 | /** 12 | * Papi扩展 13 | * 14 | * - `%eclean_before_next%` - 距离下一次清理的时间, 单位秒 15 | * - `%eclean_before_next_formatted%` - 距离下一次清理的时间, 格式化的时间 16 | * - `%eclean_last_drop%` - 上次清理的掉落物数量 17 | * - `%eclean_last_living%` - 上次清理的生物数量 18 | * - `%eclean_last_chunk%` - 上次清理的密集实体数量 19 | * - `%eclean_trashcan_countdown%` - 垃圾桶清理倒计时, 单位秒 20 | * - `%eclean_trashcan_countdown_formatted%` - 垃圾桶清理倒计时, 格式化的时间 21 | */ 22 | object Papi : PapiExpansion(PL, "eclean") { 23 | override fun onPlaceholderRequest(player: Player?, params: String) = onRequest(player, params) 24 | 25 | override fun onRequest(player: OfflinePlayer?, params: String): String? { 26 | return when (params.lowercase()) { 27 | "before_next" -> (Config.config.duration - Clean.count).toString() 28 | "before_next_formatted" -> (Config.config.duration - Clean.count).parseSecondAsDuration() 29 | "last_drop" -> lastDrop.toString() 30 | "last_living" -> lastLiving.toString() 31 | "last_chunk" -> lastChunk.toString() 32 | "trashcan_countdown" -> Trashcan.countdown.toString() 33 | "trashcan_countdown_formatted" -> Trashcan.countdown.parseSecondAsDuration() 34 | else -> null 35 | } 36 | } 37 | 38 | private val placeholders = mutableListOf( 39 | "%eclean_before_next%", 40 | "%eclean_before_next_formatted%", 41 | "%eclean_last_drop%", 42 | "%eclean_last_living%", 43 | "%eclean_last_chunk%", 44 | "%eclean_trashcan_countdown%", 45 | "%eclean_trashcan_countdown_formatted%", 46 | ) 47 | 48 | override fun getPlaceholders() = placeholders 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/update/Update.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.update 2 | 3 | import top.e404.eclean.PL 4 | import top.e404.eclean.config.Config 5 | import top.e404.eplugin.update.EUpdater 6 | 7 | object Update : EUpdater( 8 | plugin = PL, 9 | url = "https://api.github.com/repos/4o4E/EClean/releases", 10 | mcbbs = "https://www.mcbbs.net/thread-1305548-1-1.html", 11 | github = "https://github.com/4o4E/EClean" 12 | ) { 13 | override fun enableUpdate() = Config.config.update 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/util/online.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.util 2 | 3 | import org.bukkit.Bukkit 4 | import top.e404.eclean.config.Config 5 | 6 | val noOnline get() = Bukkit.getOnlinePlayers().isEmpty() 7 | val noOnlineClean get() = Config.config.noOnline.clean 8 | val noOnlineMessage get() = Config.config.noOnline.message -------------------------------------------------------------------------------- /src/main/kotlin/util/util.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.util 2 | 3 | import org.bukkit.entity.Entity 4 | 5 | 6 | fun Collection.info(): Map { 7 | val map = mutableMapOf() 8 | for (entity in this) map.compute(entity.type.name) { _, v -> (v ?: 0) + 1 } 9 | return map 10 | } 11 | 12 | fun String.isMatch(list: List) = list.firstOrNull { it matches this } -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # 若设置为true则会在后台输出检查的详细信息 2 | debug: false 3 | 4 | # 检查更新 5 | update: true 6 | 7 | # 每次清理的间隔时长, 单位秒 8 | duration: 600 9 | 10 | # 清理前的消息 11 | # 格式 时长: 对应时长的消息 12 | # 如 60: 将在1分钟后清理实体 13 | message: 14 | 60: "&f将在1分钟后进行清理" 15 | 30: "&f将在30秒后进行清理" 16 | 10: "&f将在10秒后进行清理" 17 | 0: "&f正在清理" 18 | 19 | # 清理'有生命的'实体 20 | # 比如僵尸或者牛 21 | # 展示框, 雪球等就不包含在内 22 | living: 23 | # 设置为true以启用 24 | enable: true 25 | 26 | # 禁用的世界(不支持正则) 27 | disable_world: 28 | - "不清理的世界" 29 | 30 | # 清理结束后的通知(设置为""以禁用) 31 | finish: "&a生物清理完成, 已清理{clean}/{all}" 32 | 33 | settings: 34 | # 若设置为true则清理被命名的生物 35 | # 否则不清理清被命名的生物 36 | name: false 37 | # 若设置为true则清理拴绳拴住的生物 38 | # 否则不清理拴绳拴住的生物 39 | lead: false 40 | # 若设置为true则清理乘骑中的生物(比如船上的生物, 或者载着玩家的马匹) 41 | # 否则不清理乘骑中的生物 42 | mount: false 43 | 44 | # 若设置为true则按黑名单匹配(名字匹配的才清理) 45 | # 若设置为false则按白名单匹配(名字匹配的不清理) 46 | is_black: true 47 | 48 | # 清理的实体(支持正则) 49 | # 此处的实体是清理所有符合settings中条件的实体(不会剩下) 50 | # https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html 51 | match: 52 | - "BLAZE" 53 | - "EVOKER" 54 | - "GHAST" 55 | - "(GLOW_)?SQUID" 56 | - "PHANTOM" 57 | - "PILLAGER" 58 | 59 | # 清理掉落物 60 | drop: 61 | # 设置为true以启用 62 | enable: true 63 | 64 | # 禁用的世界(不支持正则) 65 | disable_world: 66 | - "不清理的世界" 67 | 68 | # 清理结束后的通知(设置为""以禁用) 69 | finish: "&a掉落物清理完成, 已清理{clean}/{all}" 70 | 71 | # 若设置为true则按黑名单匹配(名字匹配的才清理) 72 | # 若设置为false则按白名单匹配(名字匹配的不清理) 73 | is_black: false 74 | 75 | # 若设置为true则不清理带附魔的物品 76 | # 若设置为false则清理带附魔的物品 77 | enchant: false 78 | 79 | # 若设置为true则不清理带描述的物品 80 | # 若设置为false则清理带描述的物品 81 | lore: false 82 | 83 | # 若设置为true则不清理写过的书 84 | # 若设置为false则清理写过的书 (match规则匹配时才会清理) 85 | written_book: false 86 | 87 | 88 | # 掉落物类型(支持正则) 89 | # https://hub.spigotmc.org/javadocs/spigot/org/bukkit/material/package-summary.html 90 | match: 91 | - "DIAMOND[A-Z_]*" 92 | - "NETHERITE[A-Z_]*" 93 | - "[A-Z_]*SHULKER_BOX" 94 | - "[A-Z_]*(HEAD|SKULL)" 95 | - "SHULKER_SHELL" 96 | - "BEACON" 97 | - "(ENCHANTED_)?GOLDEN_APPLE" 98 | - "TRIDENT" 99 | - "TOTEM_OF_UNDYING" 100 | - "ENDER_CHEST" 101 | - "DRAGON_EGG" 102 | - "ELYTRA" 103 | 104 | # 区块检查(密集实体清理) 105 | chunk: 106 | # 设置为true以启用 107 | enable: true 108 | 109 | # 禁用的世界(不支持正则) 110 | disable_world: 111 | - "不清理的世界" 112 | 113 | # 清理结束后的通知(设置为""以禁用) 114 | finish: "&a密集实体清理完成, 清理{clean}个" 115 | 116 | settings: 117 | # 若设置为true则清理被命名的生物 118 | # 否则不清理清被命名的生物 119 | name: false 120 | # 若设置为true则清理拴绳拴住的生物 121 | # 否则不清理拴绳拴住的生物 122 | lead: false 123 | # 若设置为true则清理乘骑中的生物(比如船上的生物, 或者载着玩家的马匹) 124 | # 否则不清理乘骑中的生物 125 | mount: false 126 | 127 | # 未清理的实体提醒 128 | # 进行通知所需要的数量(区块中的某种实体数量超过此数量) 129 | count: 50 130 | 131 | # 发送消息的格式(有 eclean.admin 权限者会收到) 132 | format: "{chunk}中{entity}的数量较多({count})" 133 | 134 | # 每区块允许的实体上限(正则) 135 | # 匹配到的实体共用后面的上限 136 | # 超过上限的实体将会被清理 137 | # https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html 138 | limit: 139 | # 怪物 140 | ZOMBIE|SKELETON: 10 141 | SPIDER: 2 142 | # 动物 143 | CHICKEN|PIG|COW|SHEEP: 50 144 | # 投掷物 145 | EGG|ENDER_PEARL|EXPERIENCE_ORB|FIREBALL|FIREWORK|SMALL_FIREBALL|SNOWBALL|ARROW|SPECTRAL_ARROW|SPLASH_POTION: 10 146 | 147 | # 公共垃圾桶设置 148 | trashcan: 149 | # 设置为false则禁用公共垃圾桶 150 | enable: true 151 | # 设置为true则掉落物清理的掉落物会收集到垃圾桶中 152 | collect: true 153 | # 清空垃圾桶的时间间隔, 单位秒, 设置为空则不会主动清理(可能导致内存泄露占用大量内存) 154 | duration: 6000 155 | 156 | # 无在线玩家时的配置 157 | no_online: 158 | # 设置为false则没有在线玩家时不发送消息(倒计时和清理结果) 159 | message: true 160 | # 设置为false则没有在线玩家时不清理(但是会显示清理倒计时) 161 | clean: true -------------------------------------------------------------------------------- /src/main/resources/lang.yml: -------------------------------------------------------------------------------- 1 | prefix: "&7[&aEClean&7]" 2 | debug_prefix: "&7[&6ECleanDebug&7]" 3 | 4 | debug: 5 | console_enable: "&f已临时(修改config.yml永久设置)&a启用&bDebug&f, 下次重启前你将会收到&bDebug&f消息, 再使用一次此指令以&c禁用" 6 | console_disable: "&f已临时(修改config.yml永久设置)&c禁用&bDebug&f, 下次重启前你不会收到&bDebug&f消息, 再使用一次此指令以&a启用" 7 | player_enable: "&f已&a启用&bDebug&f, 你将会收到&bDebug&f消息, 再使用一次此指令以&c禁用" 8 | player_disable: "&f已&c禁用&bDebug&f, 你不会收到&bDebug&f消息, 再使用一次此指令以&a启用" 9 | 10 | warn: 11 | out_of_range: "清理前的消息`{message}`&e设置的时长超过清理间隔`{duration}`&e, 请在设置中修改(此消息将不会被发送)" 12 | invalid_number: "清理消息的时长只能是小于清理间隔的数字" 13 | 14 | hook: 15 | enable: "扫描到依赖{plugin}, 已启用相关支持" 16 | disable: "未扫描到依赖{plugin}, 已禁用" 17 | 18 | message: 19 | noperm: "&c无权限" 20 | non_player: "&c仅玩家可用" 21 | unknown_command: "&c未知指令" 22 | invalid_args: "&c无效参数" 23 | invalid_world: "&c不存在名为`{world}`的世界" 24 | invalid_number: "{number}不是有效数字" 25 | invalid_config: "&c配置文件`{file}`格式错误" 26 | invalid_entity_type: "&e{type}&c不是有效的实体类型" 27 | 28 | menu: 29 | dense: 30 | title: "&6密集实体检测" 31 | item: 32 | name: "&6{type}" 33 | lore: |- 34 | &f{amount} 35 | &f{chunk} 36 | &a左键点击传送 37 | &4右键点击清除该区块的所有该实体 38 | prev: 39 | name: "&6上一页" 40 | lore: |- 41 | &f点击前往上一页 42 | next: 43 | name: "&6下一页" 44 | lore: |- 45 | &f点击前往下一页 46 | temp: 47 | name: "&6点击切换临时传送功能" 48 | lore: |- 49 | &f传送完成30s后传送回当前位置 50 | &f临时传送非切换功能, 启用后仅下次传送生效 51 | &f当前状态为{status} 52 | status: 53 | true: "&a启用" 54 | false: "&c禁用" 55 | clean: "清理区块({chunk})的{type}, 共{count}个" 56 | trashcan: 57 | title: "&6临时垃圾桶" 58 | item: 59 | # 物品最后加的lore 60 | lore: |- 61 | &f共{amount}个 62 | &a左键点击拿取一个 63 | &a右键点击拿取半组 64 | &aShift+左键点击拿取一组 65 | prev: 66 | name: "&6上一页" 67 | lore: |- 68 | &f点击前往上一页 69 | next: 70 | name: "&6下一页" 71 | lore: |- 72 | &f点击前往下一页 73 | 74 | trash: 75 | title: "&6垃圾桶, &4关闭后垃圾桶内物品无法找回" 76 | 77 | command: 78 | reload_done: "&a重载完成" 79 | trash_disable: "&a垃圾桶功能已被禁用" 80 | trash_open: "&a已打开垃圾桶" 81 | trash_clean_done: "&a垃圾桶清理完成" 82 | clean_done: "&a共清理&6{count}&a个实体" 83 | teleport: 84 | done: "&a传送完成" 85 | temp: "&a传送完成, 将在30秒后传送回之前的位置" 86 | cover: "&a传送完成, 将在30秒后传送回上一个传送的返回位置" 87 | back: "&a已返回传送前的位置" 88 | stats: 89 | spacing: "&7, " 90 | content: "&f{type}: {count}个" 91 | empty: "&c无结果" 92 | 93 | # {force}占位符在1.12及以下版本不可用 94 | world: |- 95 | &f世界&a{world}&f共加载区块{count}个(强加载{force}个) 96 | &b实体统计信息: 97 | {entity} 98 | entity: |- 99 | &f实体&e{type}&f的统计信息 100 | {entity} 101 | usage: 102 | debug: "&a/eclean debug &f切换debug消息的接受与否" 103 | reload: "&a/eclean reload &f重载插件" 104 | trash: "&a/eclean trash &f打开垃圾桶" 105 | players: "&a/eclean players &f展示玩家及其所在的位置" 106 | show: "&a/eclean show &f打开密集实体统计信息菜单" 107 | stats: |- 108 | &a/eclean stats &f统计当前所在世界的实体和区块统计 109 | &a/eclean stats <世界名> &f统计实体和区块统计 110 | entity: |- 111 | &a/eclean entity <实体名> &f统计当前世界每个区块的指定实体 112 | &a/eclean entity <实体名> <世界名>&f 统计指定世界个区块的指定实体 113 | &a/eclean entity <实体名> <世界名> <纳入统计所需数量> &f统计指定世界个区块的指定实体并隐藏数量不超过指定数量的内容 114 | clean: |- 115 | &a/eclean clean &f立刻执行一次清理(执行清理通知, 按照配置文件中的规则) 116 | &a/eclean clean entity &f立刻执行一次实体清理(执行清理通知, 按照配置文件中的规则) 117 | &a/eclean clean entity <世界名> &f立刻在指定世界执行一次实体清理(&c不&f执行清理通知, 按照配置文件中的规则) 118 | &a/eclean clean drop &f立刻执行一次掉落物清理(执行清理通知, 按照配置文件中的规则) 119 | &a/eclean clean drop <世界名> &f立刻在指定世界执行一次掉落物清理(&c不&f执行清理通知, 按照配置文件中的规则) 120 | &a/eclean clean chunk &f立刻执行一次密集实体清理(执行清理通知, 按照配置文件中的规则) 121 | &a/eclean clean chunk <世界名> &f立刻在指定世界执行一次密集实体清理(&c不&f执行清理通知, 按照配置文件中的规则) 122 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: EClean 2 | version: ${version} 3 | main: top.e404.eclean.EClean 4 | api-version: 1.13 5 | softdepend: 6 | - PlaceholderAPI 7 | authors: [ 404E ] 8 | commands: 9 | eclean: 10 | description: 插件主命令 11 | aliases: 12 | - ecl 13 | permissions: 14 | eclean.admin: 15 | default: op 16 | description: 允许使用插件管理指令 17 | eclean.trash: 18 | default: op 19 | description: 允许打开垃圾桶 -------------------------------------------------------------------------------- /src/test/kotlin/ECleanTest.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.test 2 | 3 | import be.seeseemelk.mockbukkit.MockBukkit 4 | import org.junit.jupiter.api.BeforeAll 5 | import org.junit.jupiter.api.DisplayName 6 | import org.junit.jupiter.api.Nested 7 | import top.e404.eclean.EClean 8 | import top.e404.eclean.test.clean.ChunkCleanTest 9 | import top.e404.eclean.test.clean.DropCleanTest 10 | import top.e404.eclean.test.clean.LivingCleanTest 11 | import top.e404.eclean.unit 12 | import trash.TrashcanTest 13 | 14 | @DisplayName("清理单元测试") 15 | class ECleanTest { 16 | companion object { 17 | @JvmStatic 18 | @BeforeAll 19 | fun init() { 20 | unit = true 21 | server = MockBukkit.mock() 22 | plugin = MockBukkit.load(EClean::class.java) 23 | world = server.addSimpleWorld("world") 24 | player = server.addPlayer("mock") 25 | consoleOut 26 | } 27 | 28 | @JvmStatic 29 | @BeforeAll 30 | fun finalize() { 31 | MockBukkit.unmock() 32 | } 33 | } 34 | 35 | @Nested 36 | @DisplayName("区块清理单元测试") 37 | inner class TestChunkClean : ChunkCleanTest() 38 | 39 | @Nested 40 | @DisplayName("掉落物清理单元测试") 41 | inner class TestDropClean : DropCleanTest() 42 | 43 | @Nested 44 | @DisplayName("生物清理单元测试") 45 | inner class TestLivingClean : LivingCleanTest() 46 | 47 | @Nested 48 | @DisplayName("垃圾桶单元测试") 49 | inner class TestTrashcan : TrashcanTest() 50 | } -------------------------------------------------------------------------------- /src/test/kotlin/clean/ChunkCleanTest.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.test.clean 2 | 3 | import be.seeseemelk.mockbukkit.entity.LivingEntityMock 4 | import org.bukkit.Location 5 | import org.bukkit.entity.Entity 6 | import org.bukkit.entity.EntityType 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.DisplayName 9 | import org.junit.jupiter.api.Nested 10 | import org.junit.jupiter.api.Test 11 | import top.e404.eclean.clean.cleanDenseEntities 12 | import top.e404.eclean.clean.lastChunk 13 | import top.e404.eclean.config.Config 14 | import top.e404.eclean.test.* 15 | 16 | abstract class ChunkCleanTest { 17 | 18 | @BeforeEach 19 | fun enable() { 20 | resetConfig() 21 | Config.config.chunk.enable = true 22 | } 23 | 24 | @Nested 25 | @DisplayName("实体名清理设置") 26 | inner class TestCleanEntityWithCustomName { 27 | @Test 28 | @DisplayName("启用") 29 | fun onEnable() { 30 | // 设置为true则清理被命名的生物 31 | Config.config.chunk.settings.name = true 32 | val limit = 5 33 | Config.config.chunk.limit[Regex("ZOMBIE")] = limit 34 | 35 | val chunk = world.getChunkAt(0, 0) 36 | chunk.load() 37 | val location = Location(world, 8.0, 8.0, 8.0) 38 | val entities = world.spawnEntities(location, EntityType.ZOMBIE, 16) { _, zombie -> 39 | @Suppress("DEPRECATION") 40 | zombie.customName = "custom name" 41 | } 42 | 43 | cleanDenseEntities() 44 | 45 | val valid = entities.count(Entity::isValid) 46 | assert(valid == limit) { "启用清理命名的实体时, 命名的实体应当被清理($valid != $limit)\n$consoleOut" } 47 | } 48 | 49 | @Test 50 | @DisplayName("禁用") 51 | fun onDisable() { 52 | // 设置为true则清理被命名的生物 53 | Config.config.chunk.settings.name = false 54 | Config.config.chunk.limit[Regex("ZOMBIE")] = 5 55 | 56 | val chunk = world.getChunkAt(0, 0) 57 | chunk.load() 58 | val location = Location(world, 8.0, 8.0, 8.0) 59 | val entities = world.spawnEntities(location, EntityType.ZOMBIE, 16) { _, zombie -> 60 | @Suppress("DEPRECATION") 61 | zombie.customName = "custom name" 62 | } 63 | 64 | cleanDenseEntities() 65 | assert(entities.all(Entity::isValid)) { "禁用清理命名的实体时, 命名的实体不应当被清理\n$consoleOut" } 66 | } 67 | } 68 | 69 | @Nested 70 | @DisplayName("拴绳清理设置") 71 | inner class TestCleanEntityWithLead { 72 | @Test 73 | @DisplayName("启用") 74 | fun onEnable() { 75 | // 设置为true则清理被拴绳拴住的生物 76 | Config.config.chunk.settings.lead = true 77 | val limit = 5 78 | Config.config.chunk.limit = mutableMapOf(Regex("SHEEP") to limit) 79 | 80 | val chunk = world.getChunkAt(0, 0) 81 | chunk.load() 82 | val location = Location(world, 8.0, 8.0, 8.0) 83 | val entities = world.spawnEntities(location, EntityType.SHEEP, 16) { _, sheep -> 84 | sheep as LivingEntityMock 85 | sheep.setLeashHolder(sheep) 86 | } 87 | 88 | cleanDenseEntities() 89 | val valid = entities.count(Entity::isValid) 90 | assert(valid == limit) { "启用清理拴绳拴住的实体时, 拴绳拴住的实体应当被清理($valid != $limit)\n$consoleOut" } 91 | } 92 | 93 | @Test 94 | @DisplayName("禁用") 95 | fun onDisable() { 96 | // 设置为true则清理被拴绳拴住的生物 97 | Config.config.chunk.settings.lead = false 98 | Config.config.chunk.limit = mutableMapOf(Regex("SHEEP") to 5) 99 | 100 | val chunk = world.getChunkAt(0, 0) 101 | chunk.load() 102 | val location = Location(world, 8.0, 8.0, 8.0) 103 | val entities = world.spawnEntities(location, EntityType.SHEEP, 16) { _, sheep -> 104 | sheep as LivingEntityMock 105 | sheep.setLeashHolder(sheep) 106 | } 107 | 108 | cleanDenseEntities() 109 | assert(entities.all(Entity::isValid)) { "禁用清理拴绳拴住的实体时, 拴绳拴住的实体不应当被清理\n$consoleOut" } 110 | } 111 | } 112 | 113 | @Nested 114 | @DisplayName("乘骑清理设置") 115 | inner class TestCleanEntityWithMount { 116 | @Test 117 | @DisplayName("启用") 118 | fun onEnable() { 119 | // 设置为true则清理乘骑中的生物 120 | Config.config.chunk.settings.mount = true 121 | val limit = 5 122 | Config.config.chunk.limit = mutableMapOf(Regex("HORSE") to limit) 123 | 124 | val chunk = world.getChunkAt(0, 0) 125 | chunk.load() 126 | val location = Location(world, 8.0, 8.0, 8.0) 127 | val entities = world.spawnEntities(location, EntityType.HORSE, 16) { index, horse -> 128 | horse as LivingEntityMock 129 | if (index < 8) horse.addPassenger(player) 130 | else player.addPassenger(horse) 131 | } 132 | 133 | cleanDenseEntities() 134 | val valid = entities.count(Entity::isValid) 135 | assert(valid == limit) { "启用清理乘骑中的实体时, 乘骑中的实体应当被清理($valid != $limit)\n$consoleOut" } 136 | } 137 | 138 | @Test 139 | @DisplayName("禁用") 140 | fun onDisable() { 141 | // 设置为true则清理乘骑中的生物 142 | Config.config.chunk.settings.mount = false 143 | Config.config.chunk.limit = mutableMapOf(Regex("HORSE") to 5) 144 | 145 | val chunk = world.getChunkAt(0, 0) 146 | chunk.load() 147 | val location = Location(world, 8.0, 8.0, 8.0) 148 | val entities = world.spawnEntities(location, EntityType.HORSE, 16) { index, horse -> 149 | horse as LivingEntityMock 150 | if (index < 8) horse.addPassenger(player) 151 | else player.addPassenger(horse) 152 | } 153 | 154 | cleanDenseEntities() 155 | assert(entities.all(Entity::isValid)) { "禁用清理乘骑中的实体时, 乘骑中的实体不应当被清理\n$consoleOut" } 156 | } 157 | } 158 | 159 | @Test 160 | @DisplayName("papi") 161 | fun testPapi() { 162 | val limit = 5 163 | val count = 16 164 | Config.config.chunk.limit = mutableMapOf(Regex("ZOMBIE") to limit) 165 | 166 | val chunk = world.getChunkAt(0, 0) 167 | chunk.load() 168 | val location = Location(world, 8.0, 8.0, 8.0) 169 | val entities = world.spawnEntities(location, EntityType.ZOMBIE, count) 170 | 171 | cleanDenseEntities() 172 | val valid = entities.count(Entity::isValid) 173 | assert(valid == limit) 174 | assert(lastChunk == count - limit) { "papi展示最后一次区块清理的实体数时不正确\n$consoleOut" } 175 | } 176 | } -------------------------------------------------------------------------------- /src/test/kotlin/clean/DropCleanTest.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.test.clean 2 | 3 | import org.bukkit.Location 4 | import org.bukkit.Material 5 | import org.bukkit.enchantments.Enchantment 6 | import org.bukkit.entity.Entity 7 | import org.bukkit.inventory.ItemStack 8 | import org.bukkit.inventory.meta.BookMeta 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.DisplayName 11 | import org.junit.jupiter.api.Nested 12 | import org.junit.jupiter.api.Test 13 | import top.e404.eclean.clean.cleanDrop 14 | import top.e404.eclean.clean.lastDrop 15 | import top.e404.eclean.config.Config 16 | import top.e404.eclean.test.consoleOut 17 | import top.e404.eclean.test.dropItems 18 | import top.e404.eclean.test.resetConfig 19 | import top.e404.eclean.test.world 20 | import top.e404.eplugin.util.editItemMeta 21 | 22 | abstract class DropCleanTest { 23 | 24 | @BeforeEach 25 | fun enable() { 26 | resetConfig() 27 | Config.config.drop.enable = true 28 | } 29 | 30 | @Nested 31 | @DisplayName("附魔物品清理设置") 32 | inner class TestCleanEntityWithEnchant { 33 | @Test 34 | @DisplayName("启用") 35 | fun onEnable() { 36 | // 设置为true则不清理被附魔的物品 37 | Config.config.drop.enchant = true 38 | Config.config.drop.match = mutableListOf(Regex("DIAMOND.*")) 39 | 40 | val location = Location(world, 8.0, 8.0, 8.0) 41 | val entities = world.dropItems(location, 2) { index -> 42 | ItemStack(Material.DIAMOND_CHESTPLATE).apply { 43 | if (index == 0) addUnsafeEnchantment(Enchantment.DURABILITY, 1) 44 | } 45 | } 46 | 47 | cleanDrop() 48 | 49 | assert(entities.count(Entity::isValid) == 1) { "禁用清理附魔的物品时, 附魔的物品应当不被清理\n$consoleOut" } 50 | } 51 | 52 | @Test 53 | @DisplayName("禁用") 54 | fun onDisable() { 55 | // 设置为true则不清理被附魔的物品 56 | Config.config.drop.enchant = false 57 | Config.config.drop.match = mutableListOf(Regex("DIAMOND.*")) 58 | 59 | val location = Location(world, 8.0, 8.0, 8.0) 60 | val entities = world.dropItems(location, 2) { index -> 61 | ItemStack(Material.DIAMOND_CHESTPLATE).apply { 62 | if (index == 0) addUnsafeEnchantment(Enchantment.DURABILITY, 1) 63 | } 64 | } 65 | 66 | cleanDrop() 67 | 68 | assert(entities.none(Entity::isValid)) { "禁用清理附魔的物品时, 匹配物品应当被全部清理\n$consoleOut" } 69 | } 70 | } 71 | 72 | @Nested 73 | @DisplayName("写过的书清理设置") 74 | inner class TestCleanWrittenBook { 75 | @Test 76 | @DisplayName("启用") 77 | fun onEnable() { 78 | // 设置为true则不清理写过的书 79 | Config.config.drop.writtenBook = true 80 | Config.config.drop.match = mutableListOf(Regex("WRITABLE_BOOK")) 81 | 82 | val location = Location(world, 8.0, 8.0, 8.0) 83 | val entities = world.dropItems(location, 2) { index -> 84 | ItemStack(Material.WRITABLE_BOOK).apply { 85 | if (index == 0) editItemMeta { 86 | this as BookMeta 87 | @Suppress("DEPRECATION") 88 | this.pages = mutableListOf("a", "b") 89 | } 90 | } 91 | } 92 | 93 | cleanDrop() 94 | 95 | assert(entities.count(Entity::isValid) == 1) { "禁用清理写过的书时, 写过的书应当不被清理\n$consoleOut" } 96 | } 97 | 98 | @Test 99 | @DisplayName("禁用") 100 | fun onDisable() { 101 | // 设置为true则不清理写过的书 102 | Config.config.drop.writtenBook = false 103 | Config.config.drop.match = mutableListOf(Regex("WRITABLE_BOOK")) 104 | 105 | val location = Location(world, 8.0, 8.0, 8.0) 106 | val entities = world.dropItems(location, 2) { index -> 107 | ItemStack(Material.WRITABLE_BOOK).apply { 108 | if (index == 0) editItemMeta { 109 | this as BookMeta 110 | @Suppress("DEPRECATION") 111 | this.pages = mutableListOf("a", "b") 112 | } 113 | } 114 | } 115 | 116 | cleanDrop() 117 | 118 | assert(entities.none(Entity::isValid)) { "禁用清理写过的书时, 匹配物品应当被全部清理\n$consoleOut" } 119 | } 120 | } 121 | 122 | @Nested 123 | @DisplayName("黑白名单设置") 124 | inner class TestBlackWhiteList { 125 | @Test 126 | @DisplayName("黑名单") 127 | fun blackList() { 128 | // 设置为true则按黑名单匹配(名字匹配的才清理) 129 | Config.config.drop.black = true 130 | Config.config.drop.match = mutableListOf(Regex("DIAMOND.*")) 131 | 132 | val location = Location(world, 8.0, 8.0, 8.0) 133 | val shouldClean = world.dropItems(location, 2) { index -> 134 | when (index) { 135 | 0 -> ItemStack(Material.DIAMOND_CHESTPLATE) 136 | 1 -> ItemStack(Material.DIAMOND_SWORD) 137 | else -> throw Exception() 138 | } 139 | } 140 | val shouldNotClean = world.dropItems(location, 2) { index -> 141 | when (index) { 142 | 0 -> ItemStack(Material.STONE) 143 | 1 -> ItemStack(Material.BOW) 144 | else -> throw Exception() 145 | } 146 | } 147 | 148 | cleanDrop() 149 | 150 | assert(shouldClean.none(Entity::isValid)) { "黑名单模式中匹配的实体应该全部清理\n$consoleOut" } 151 | assert(shouldNotClean.all(Entity::isValid)) { "黑名单模式中不匹配的实体应该全部不清理\n$consoleOut" } 152 | } 153 | 154 | @Test 155 | @DisplayName("白名单") 156 | fun whiteList() { 157 | // 设置为false则按白名单匹配(名字匹配的不清理) 158 | Config.config.drop.black = false 159 | Config.config.drop.match = mutableListOf(Regex("DIAMOND.*")) 160 | 161 | val location = Location(world, 8.0, 8.0, 8.0) 162 | val shouldNotClean = world.dropItems(location, 2) { index -> 163 | when (index) { 164 | 0 -> ItemStack(Material.DIAMOND_CHESTPLATE) 165 | 1 -> ItemStack(Material.DIAMOND_SWORD) 166 | else -> throw Exception() 167 | } 168 | } 169 | val shouldClean = world.dropItems(location, 2) { index -> 170 | when (index) { 171 | 0 -> ItemStack(Material.STONE) 172 | 1 -> ItemStack(Material.BOW) 173 | else -> throw Exception() 174 | } 175 | } 176 | 177 | cleanDrop() 178 | 179 | assert(shouldClean.none(Entity::isValid)) { "白名单模式中不匹配的实体应该全部清理\n$consoleOut" } 180 | assert(shouldNotClean.all(Entity::isValid)) { "白名单模式中匹配的实体应该全部不清理\n$consoleOut" } 181 | } 182 | } 183 | 184 | @Test 185 | @DisplayName("papi") 186 | fun testPapi() { 187 | val count = 2 188 | Config.config.drop.match = mutableListOf(Regex("DIAMOND")) 189 | 190 | val location = Location(world, 8.0, 8.0, 8.0) 191 | world.dropItems(location, count) { _ -> ItemStack(Material.DIAMOND) } 192 | 193 | cleanDrop() 194 | 195 | assert(lastDrop == count) { "papi展示最后一次掉落物清理的实体数时不正确\n$consoleOut" } 196 | } 197 | } -------------------------------------------------------------------------------- /src/test/kotlin/clean/LivingCleanTest.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.test.clean 2 | 3 | import be.seeseemelk.mockbukkit.entity.LivingEntityMock 4 | import org.bukkit.Location 5 | import org.bukkit.entity.Entity 6 | import org.bukkit.entity.EntityType 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.DisplayName 9 | import org.junit.jupiter.api.Nested 10 | import org.junit.jupiter.api.Test 11 | import top.e404.eclean.clean.cleanLiving 12 | import top.e404.eclean.clean.lastLiving 13 | import top.e404.eclean.config.Config 14 | import top.e404.eclean.test.* 15 | 16 | abstract class LivingCleanTest { 17 | 18 | @BeforeEach 19 | fun enable() { 20 | resetConfig() 21 | Config.config.living.enable = true 22 | } 23 | 24 | @Nested 25 | @DisplayName("实体名清理设置") 26 | inner class TestCleanEntityWithCustomName { 27 | @Test 28 | @DisplayName("启用") 29 | fun onEnable() { 30 | // 设置为true则清理被命名的生物 31 | Config.config.living.settings.name = true 32 | Config.config.living.match = mutableListOf(Regex("ZOMBIE")) 33 | 34 | val chunk = world.getChunkAt(0, 0) 35 | chunk.load() 36 | val location = Location(world, 8.0, 8.0, 8.0) 37 | val entities = world.spawnEntities(location, EntityType.ZOMBIE, 16) { _, zombie -> 38 | @Suppress("DEPRECATION") 39 | zombie.customName = "custom name" 40 | } 41 | 42 | cleanLiving() 43 | 44 | assert(entities.none(Entity::isValid)) { "启用清理命名的实体时, 命名的实体应当被全部清理\n$consoleOut" } 45 | } 46 | 47 | @Test 48 | @DisplayName("禁用") 49 | fun onDisable() { 50 | // 设置为true则清理被命名的生物 51 | Config.config.living.settings.name = false 52 | Config.config.living.match = mutableListOf(Regex("ZOMBIE")) 53 | 54 | val chunk = world.getChunkAt(0, 0) 55 | chunk.load() 56 | val location = Location(world, 8.0, 8.0, 8.0) 57 | val entities = world.spawnEntities(location, EntityType.ZOMBIE, 16) { _, zombie -> 58 | @Suppress("DEPRECATION") 59 | zombie.customName = "custom name" 60 | } 61 | 62 | cleanLiving() 63 | assert(entities.all(Entity::isValid)) { "禁用清理命名的实体时, 命名的实体不应当被清理\n$consoleOut" } 64 | } 65 | } 66 | 67 | @Nested 68 | @DisplayName("拴绳清理设置") 69 | inner class TestCleanEntityWithLead { 70 | @Test 71 | @DisplayName("启用") 72 | fun onEnable() { 73 | // 设置为true则清理被拴绳拴住的生物 74 | Config.config.living.settings.lead = true 75 | Config.config.living.match = mutableListOf(Regex("SHEEP")) 76 | 77 | val chunk = world.getChunkAt(0, 0) 78 | chunk.load() 79 | val location = Location(world, 8.0, 8.0, 8.0) 80 | val entities = world.spawnEntities(location, EntityType.SHEEP, 16) { _, sheep -> 81 | sheep as LivingEntityMock 82 | sheep.setLeashHolder(sheep) 83 | } 84 | 85 | cleanLiving() 86 | assert(entities.none(Entity::isValid)) { "启用清理拴绳拴住的实体时, 拴绳拴住的实体应当被清理\n$consoleOut" } 87 | } 88 | 89 | @Test 90 | @DisplayName("禁用") 91 | fun onDisable() { 92 | // 设置为true则清理被拴绳拴住的生物 93 | Config.config.living.settings.lead = false 94 | Config.config.living.match = mutableListOf(Regex("SHEEP")) 95 | 96 | val chunk = world.getChunkAt(0, 0) 97 | chunk.load() 98 | val location = Location(world, 8.0, 8.0, 8.0) 99 | val entities = world.spawnEntities(location, EntityType.SHEEP, 16) { _, sheep -> 100 | sheep as LivingEntityMock 101 | sheep.setLeashHolder(sheep) 102 | } 103 | 104 | cleanLiving() 105 | assert(entities.all(Entity::isValid)) { "禁用清理拴绳拴住的实体时, 拴绳拴住的实体不应当被清理\n$consoleOut" } 106 | } 107 | } 108 | 109 | @Nested 110 | @DisplayName("乘骑清理设置") 111 | inner class TestCleanEntityWithMount { 112 | @Test 113 | @DisplayName("启用") 114 | fun onEnable() { 115 | // 设置为true则清理乘骑中的生物 116 | Config.config.living.settings.mount = true 117 | Config.config.living.match = mutableListOf(Regex("HORSE")) 118 | 119 | val chunk = world.getChunkAt(0, 0) 120 | chunk.load() 121 | val location = Location(world, 8.0, 8.0, 8.0) 122 | val entities = world.spawnEntities(location, EntityType.HORSE, 16) { index, horse -> 123 | horse as LivingEntityMock 124 | if (index < 8) horse.addPassenger(player) 125 | else player.addPassenger(horse) 126 | } 127 | 128 | cleanLiving() 129 | assert(entities.none(Entity::isValid)) { "启用清理乘骑中的实体时, 乘骑中的实体应当被清理\n$consoleOut" } 130 | } 131 | 132 | @Test 133 | @DisplayName("禁用") 134 | fun onDisable() { 135 | // 设置为true则清理乘骑中的生物 136 | Config.config.living.settings.mount = false 137 | Config.config.living.match = mutableListOf(Regex("HORSE")) 138 | 139 | val chunk = world.getChunkAt(0, 0) 140 | chunk.load() 141 | val location = Location(world, 8.0, 8.0, 8.0) 142 | val entities = world.spawnEntities(location, EntityType.HORSE, 16) { index, horse -> 143 | horse as LivingEntityMock 144 | if (index < 8) horse.addPassenger(player) 145 | else player.addPassenger(horse) 146 | } 147 | 148 | cleanLiving() 149 | assert(entities.all(Entity::isValid)) { "禁用清理乘骑中的实体时, 乘骑中的实体不应当被清理\n$consoleOut" } 150 | } 151 | } 152 | 153 | @Nested 154 | @DisplayName("黑白名单设置") 155 | inner class TestBlackWhiteList { 156 | @Test 157 | @DisplayName("黑名单") 158 | fun blackList() { 159 | // 设置为true则按黑名单匹配(名字匹配的才清理) 160 | Config.config.living.black = true 161 | Config.config.living.match = mutableListOf(Regex("ZOMBIE.*")) 162 | 163 | val chunk = world.getChunkAt(0, 0) 164 | chunk.load() 165 | val location = Location(world, 8.0, 8.0, 8.0) 166 | val shouldClean = listOf( 167 | world.spawnEntity(location, EntityType.ZOMBIE), 168 | world.spawnEntity(location, EntityType.ZOMBIE_HORSE), 169 | ) 170 | val shouldNotClean = listOf( 171 | world.spawnEntity(location, EntityType.SHEEP), 172 | world.spawnEntity(location, EntityType.COW), 173 | ) 174 | 175 | cleanLiving() 176 | 177 | assert(shouldClean.none(Entity::isValid)) { "黑名单模式中匹配的实体应该全部清理\n$consoleOut" } 178 | assert(shouldNotClean.all(Entity::isValid)) { "黑名单模式中不匹配的实体应该全部不清理\n$consoleOut" } 179 | } 180 | 181 | @Test 182 | @DisplayName("白名单") 183 | fun whiteList() { 184 | // 设置为false则按白名单匹配(名字匹配的不清理) 185 | Config.config.living.black = false 186 | Config.config.living.match = mutableListOf(Regex("ZOMBIE.*")) 187 | 188 | val chunk = world.getChunkAt(0, 0) 189 | chunk.load() 190 | val location = Location(world, 8.0, 8.0, 8.0) 191 | val shouldClean = listOf( 192 | world.spawnEntity(location, EntityType.SHEEP), 193 | world.spawnEntity(location, EntityType.COW), 194 | ) 195 | val shouldNotClean = listOf( 196 | world.spawnEntity(location, EntityType.ZOMBIE), 197 | world.spawnEntity(location, EntityType.ZOMBIE_HORSE), 198 | ) 199 | 200 | cleanLiving() 201 | 202 | assert(shouldClean.none(Entity::isValid)) { "白名单模式中不匹配的实体应该全部清理\n$consoleOut" } 203 | assert(shouldNotClean.all(Entity::isValid)) { "白名单模式中匹配的实体应该全部不清理\n$consoleOut" } 204 | } 205 | } 206 | 207 | @Test 208 | @DisplayName("papi") 209 | fun testPapi() { 210 | val count = 2 211 | Config.config.living.match = mutableListOf(Regex("ZOMBIE")) 212 | 213 | val chunk = world.getChunkAt(0, 0) 214 | chunk.load() 215 | val location = Location(world, 8.0, 8.0, 8.0) 216 | world.spawnEntities(location, EntityType.ZOMBIE, count) 217 | 218 | cleanLiving() 219 | 220 | assert(lastLiving == count) { "papi展示最后一次生物清理的实体数时不正确\n$consoleOut" } 221 | } 222 | } -------------------------------------------------------------------------------- /src/test/kotlin/package.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.test -------------------------------------------------------------------------------- /src/test/kotlin/trash/TrashcanTest.kt: -------------------------------------------------------------------------------- 1 | package trash 2 | 3 | import be.seeseemelk.mockbukkit.inventory.SimpleInventoryViewMock 4 | import org.bukkit.Bukkit 5 | import org.bukkit.Location 6 | import org.bukkit.Material 7 | import org.bukkit.event.inventory.ClickType 8 | import org.bukkit.event.inventory.InventoryAction 9 | import org.bukkit.event.inventory.InventoryClickEvent 10 | import org.bukkit.event.inventory.InventoryType 11 | import org.bukkit.inventory.ItemStack 12 | import org.junit.jupiter.api.* 13 | import top.e404.eclean.clean.Trashcan 14 | import top.e404.eclean.clean.cleanDrop 15 | import top.e404.eclean.config.Config 16 | import top.e404.eclean.menu.MenuManager 17 | import top.e404.eclean.test.* 18 | import top.e404.eplugin.menu.menu.InventoryMenu 19 | import kotlin.test.assertNotNull 20 | 21 | abstract class TrashcanTest { 22 | 23 | @BeforeEach 24 | fun setup() { 25 | resetConfig() 26 | Config.config.trashcan.enable = true 27 | Config.config.trashcan.duration = null 28 | 29 | Trashcan.trashData.clear() 30 | Trashcan.trashValues.clear() 31 | } 32 | 33 | @AfterEach 34 | fun cleanUp() { 35 | player.inventory.clear() 36 | MenuManager.closeMenu(player) 37 | Trashcan.trashData.clear() 38 | Trashcan.trashValues.clear() 39 | } 40 | 41 | @Nested 42 | @DisplayName("清理时收集掉落物到垃圾桶") 43 | inner class TestCollectItemWhenClean { 44 | @Test 45 | @DisplayName("启用") 46 | fun enable() { 47 | Config.config.drop.enable = true 48 | Config.config.drop.match = mutableListOf(Regex(".*")) 49 | Config.config.trashcan.collect = true 50 | 51 | val chunk = world.getChunkAt(0, 0) 52 | chunk.load() 53 | val location = Location(world, 8.0, 8.0, 8.0) 54 | val amount = 64 55 | world.dropItem(location, ItemStack(Material.STONE, amount)) 56 | 57 | cleanDrop() 58 | 59 | assert(Trashcan.trashValues.size == 1) { "清理的掉落物未放入公共垃圾箱\n$consoleOut" } 60 | assert(Trashcan.trashValues[0].amount == amount) { "放入公共垃圾箱的物品数量不正确\n$consoleOut" } 61 | } 62 | 63 | @Test 64 | @DisplayName("禁用") 65 | fun disable() { 66 | Config.config.drop.enable = true 67 | Config.config.drop.match = mutableListOf(Regex(".*")) 68 | Config.config.trashcan.collect = false 69 | 70 | val chunk = world.getChunkAt(0, 0) 71 | chunk.load() 72 | val location = Location(world, 8.0, 8.0, 8.0) 73 | val amount = 64 74 | world.dropItem(location, ItemStack(Material.STONE, amount)) 75 | 76 | cleanDrop() 77 | 78 | assert(Trashcan.trashValues.isEmpty()) { 79 | "清理的掉落物不应放入公共垃圾箱\n$consoleOut" 80 | } 81 | } 82 | } 83 | 84 | private fun leftClick(slot: Int): Pair { 85 | Trashcan.open(player) 86 | val menu = MenuManager.menus[player]!! 87 | val inventoryClickEvent = InventoryClickEvent( 88 | SimpleInventoryViewMock( 89 | player, 90 | menu.inv, 91 | player.inventory, 92 | InventoryType.CHEST 93 | ), 94 | InventoryType.SlotType.CONTAINER, 95 | slot, 96 | ClickType.LEFT, 97 | InventoryAction.COLLECT_TO_CURSOR 98 | ) 99 | Bukkit.getPluginManager().callEvent(inventoryClickEvent) 100 | return menu to inventoryClickEvent 101 | } 102 | 103 | private fun rightClick(slot: Int): Pair { 104 | Trashcan.open(player) 105 | val menu = MenuManager.menus[player]!! 106 | val inventoryClickEvent = InventoryClickEvent( 107 | SimpleInventoryViewMock( 108 | player, 109 | menu.inv, 110 | player.inventory, 111 | InventoryType.CHEST 112 | ), 113 | InventoryType.SlotType.CONTAINER, 114 | slot, 115 | ClickType.RIGHT, 116 | InventoryAction.PICKUP_HALF 117 | ) 118 | Bukkit.getPluginManager().callEvent(inventoryClickEvent) 119 | return menu to inventoryClickEvent 120 | } 121 | 122 | private fun shiftLeftClick(slot: Int): Pair { 123 | Trashcan.open(player) 124 | val menu = MenuManager.menus[player]!! 125 | val inventoryClickEvent = InventoryClickEvent( 126 | SimpleInventoryViewMock( 127 | player, 128 | menu.inv, 129 | player.inventory, 130 | InventoryType.CHEST 131 | ), 132 | InventoryType.SlotType.CONTAINER, 133 | slot, 134 | ClickType.SHIFT_LEFT, 135 | InventoryAction.MOVE_TO_OTHER_INVENTORY 136 | ) 137 | Bukkit.getPluginManager().callEvent(inventoryClickEvent) 138 | return menu to inventoryClickEvent 139 | } 140 | 141 | @Nested 142 | @DisplayName("从垃圾桶中拿取物品") 143 | inner class TestTakeItemFromTrashcan { 144 | @Test 145 | @DisplayName("拿取1个") 146 | fun takeOne() { 147 | Trashcan.addItem(ItemStack(Material.STONE, 2)) 148 | 149 | val (menu, event) = leftClick(0) 150 | assert(event.isCancelled) { "玩家点击菜单时的操作应被取消\n$consoleOut" } 151 | val item = menu.inv.getItem(0) 152 | assertNotNull(item) { "拿取后菜单中应还有1个物品\n$consoleOut" } 153 | assert(item.type != Material.AIR) { "物品不应为空\n$consoleOut" } 154 | assert(Trashcan.trashValues.size == 1) { "垃圾桶中应有1种物品\n$consoleOut" } 155 | assert(Trashcan.trashValues.size == 1) { "垃圾桶中应有1种物品\n$consoleOut" } 156 | assert(Trashcan.trashValues[0].amount == 1) { "垃圾桶中应剩余1个物品\n$consoleOut" } 157 | 158 | val playerItem = player.inventory.getItem(0) 159 | assertNotNull(playerItem) { "拿取后玩家背包中应有该物品\n$consoleOut" } 160 | assert(playerItem.type == Material.STONE) { "该物品应类型相同\n$consoleOut" } 161 | assert(playerItem.amount == 1) { "该物品数量应为1\n$consoleOut" } 162 | } 163 | 164 | @Test 165 | @DisplayName("拿取一半") 166 | fun takeHalf() { 167 | Trashcan.addItem(ItemStack(Material.STONE, 64)) 168 | 169 | val (menu, event) = rightClick(0) 170 | assert(event.isCancelled) { "玩家点击菜单时的操作应被取消\n$consoleOut" } 171 | val item = menu.inv.getItem(0) 172 | assertNotNull(item) { "菜单中物品应不为空\n$consoleOut" } 173 | assert(item.type == Material.STONE) { "菜单中物品应类型不变\n$consoleOut" } 174 | assert(Trashcan.trashValues.size == 1) { "垃圾桶中应剩余1种\n$consoleOut" } 175 | assert(Trashcan.trashValues[0].amount == 32) { "垃圾桶中应有32个\n$consoleOut" } 176 | val playerItem = player.inventory.getItem(0) 177 | assertNotNull(playerItem) { "背包中物品应不为空\n$consoleOut" } 178 | assert(playerItem.type == Material.STONE) { "背包中物品应类型相同\n$consoleOut" } 179 | assert(playerItem.amount == 32) { "背包中应有32个\n$consoleOut" } 180 | } 181 | 182 | @Test 183 | @DisplayName("拿取一组") 184 | fun takeFullStack() { 185 | Trashcan.addItems(listOf(ItemStack(Material.STONE, 64), ItemStack(Material.STONE, 64))) 186 | 187 | val (menu, event) = shiftLeftClick(0) 188 | assert(event.isCancelled) { "玩家点击菜单时的操作应被取消\n$consoleOut" } 189 | val item = menu.inv.getItem(0) 190 | assertNotNull(item) { "菜单中物品应不为空\n$consoleOut" } 191 | assert(item.type == Material.STONE) { "菜单中物品应类型不变\n${Trashcan.trashValues[0]}\\n$consoleOut" } 192 | assert(Trashcan.trashValues.size == 1) { "垃圾桶中应剩余1种\n${Trashcan.trashValues[0]}\\n$consoleOut" } 193 | assert(Trashcan.trashValues[0].amount == 64) { "垃圾桶中应有64个\n${Trashcan.trashValues[0]}\n$consoleOut" } 194 | val playerItem = player.inventory.getItem(0) 195 | assertNotNull(playerItem) { "背包中物品应不为空\n$consoleOut" } 196 | assert(playerItem.type == Material.STONE) { "背包中物品应类型相同\n${playerItem}\n$consoleOut" } 197 | assert(playerItem.amount == 64) { "背包中应有64个\n${playerItem}\n$consoleOut" } 198 | } 199 | 200 | @Test 201 | @DisplayName("拿取全部") 202 | fun takeAll() { 203 | Trashcan.addItems(listOf(ItemStack(Material.STONE, 64))) 204 | 205 | val (menu, event) = shiftLeftClick(0) 206 | assert(event.isCancelled) { "玩家点击菜单时的操作应被取消\n$consoleOut" } 207 | val item = menu.inv.getItem(0) 208 | assert(item == null || item.type == Material.AIR) { "垃圾桶菜单中应没有物品\n$consoleOut" } 209 | assert(Trashcan.trashValues.isEmpty()) { "垃圾桶中应没有物品\n$consoleOut" } 210 | val playerItem = player.inventory.getItem(0) 211 | assertNotNull(playerItem) { "背包中物品应不为空\n$consoleOut" } 212 | assert(playerItem.type == Material.STONE) { "背包中物品应类型相同\n$consoleOut" } 213 | assert(playerItem.amount == 64) { "背包中应有64个\n$consoleOut" } 214 | } 215 | } 216 | 217 | @Nested 218 | @DisplayName("向垃圾桶中放入物品") 219 | inner class TestPutItemToTrashcan { 220 | @Test 221 | @DisplayName("放入1个") 222 | fun putOne() { 223 | player.inventory.setItem(0, ItemStack(Material.STONE, 64)) 224 | 225 | val (menu, event) = leftClick(81) 226 | assert(event.isCancelled) { "玩家点击菜单时的操作应被取消\n$consoleOut" } 227 | val item = menu.inv.getItem(0) 228 | assertNotNull(item) { "菜单中物品应不为空\n$consoleOut" } 229 | assert(item.type == Material.STONE) { "菜单中物品应类型不变\n${Trashcan.trashValues[0]}\\n$consoleOut" } 230 | assert(Trashcan.trashValues.size == 1) { "垃圾桶中应剩余1种\n${Trashcan.trashValues[0]}\\n$consoleOut" } 231 | assert(Trashcan.trashValues[0].amount == 1) { "垃圾桶中应有1个\n${Trashcan.trashValues[0]}\n$consoleOut" } 232 | val playerItem = player.inventory.getItem(0) 233 | assertNotNull(playerItem) { "背包中物品应不为空\n$consoleOut" } 234 | assert(playerItem.type == Material.STONE) { "背包中物品应类型相同\n${playerItem}\n$consoleOut" } 235 | assert(playerItem.amount == 63) { "背包中应有63个\n${playerItem}\n$consoleOut" } 236 | } 237 | 238 | @Test 239 | @DisplayName("放入一半") 240 | fun putHalf() { 241 | player.inventory.setItem(0, ItemStack(Material.STONE, 64)) 242 | 243 | val (menu, event) = rightClick(81) 244 | assert(event.isCancelled) { "玩家点击菜单时的操作应被取消\n$consoleOut" } 245 | val item = menu.inv.getItem(0) 246 | assertNotNull(item) { "菜单中物品应不为空\n$consoleOut" } 247 | assert(item.type == Material.STONE) { "菜单中物品应类型不变\n${Trashcan.trashValues[0]}\\n$consoleOut" } 248 | assert(Trashcan.trashValues.size == 1) { "垃圾桶中应剩余1种\n${Trashcan.trashValues[0]}\\n$consoleOut" } 249 | assert(Trashcan.trashValues[0].amount == 32) { "垃圾桶中应有32个\n${Trashcan.trashValues[0]}\n$consoleOut" } 250 | val playerItem = player.inventory.getItem(0) 251 | assertNotNull(playerItem) { "背包中物品应不为空\n$consoleOut" } 252 | assert(playerItem.type == Material.STONE) { "背包中物品应类型相同\n${playerItem}\n$consoleOut" } 253 | assert(playerItem.amount == 32) { "背包中应有32个\n${playerItem}\n$consoleOut" } 254 | } 255 | 256 | @Test 257 | @DisplayName("放入一组") 258 | fun put() { 259 | player.inventory.setItem(0, ItemStack(Material.STONE, 64)) 260 | player.inventory.setItem(1, ItemStack(Material.STONE, 64)) 261 | 262 | val (menu, event) = shiftLeftClick(81) 263 | assert(event.isCancelled) { "玩家点击菜单时的操作应被取消\n$consoleOut" } 264 | val item = menu.inv.getItem(0) 265 | assertNotNull(item) { "菜单中物品应不为空\n$consoleOut" } 266 | assert(item.type == Material.STONE) { "菜单中物品应类型不变\n${Trashcan.trashValues[0]}\\n$consoleOut" } 267 | assert(Trashcan.trashValues.size == 1) { "垃圾桶中应剩余1种\n${Trashcan.trashValues[0]}\\n$consoleOut" } 268 | assert(Trashcan.trashValues[0].amount == 64) { "垃圾桶中应有64个\n${Trashcan.trashValues[0]}\n$consoleOut" } 269 | val item1 = player.inventory.getItem(0) 270 | assert(item1 == null || item1.type == Material.AIR) { 271 | "放入一组之后第1个应为空的\n$consoleOut" 272 | } 273 | val item2 = player.inventory.getItem(1) 274 | assert(item2 != null && item2.amount == 64) { 275 | "放入一组之后第二个应为64个\n$consoleOut" 276 | } 277 | } 278 | } 279 | } -------------------------------------------------------------------------------- /src/test/kotlin/util.kt: -------------------------------------------------------------------------------- 1 | package top.e404.eclean.test 2 | 3 | import be.seeseemelk.mockbukkit.ServerMock 4 | import be.seeseemelk.mockbukkit.WorldMock 5 | import be.seeseemelk.mockbukkit.entity.PlayerMock 6 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer 7 | import org.bukkit.Location 8 | import org.bukkit.entity.Entity 9 | import org.bukkit.entity.EntityType 10 | import org.bukkit.inventory.ItemStack 11 | import top.e404.eclean.EClean 12 | import top.e404.eclean.config.* 13 | 14 | lateinit var server: ServerMock 15 | lateinit var plugin: EClean 16 | lateinit var world: WorldMock 17 | lateinit var player: PlayerMock 18 | 19 | fun WorldMock.spawnEntities( 20 | location: Location, 21 | type: EntityType, 22 | count: Int, 23 | edit: (index: Int, spawned: Entity) -> Unit = { _, _ -> } 24 | ) = (0 until count).map { index -> 25 | val spawned = spawnEntity(location, type) 26 | edit(index, spawned) 27 | spawned 28 | } 29 | 30 | fun WorldMock.dropItems( 31 | location: Location, 32 | count: Int, 33 | generator: (index: Int) -> ItemStack 34 | ) = (0 until count).map { index -> 35 | val spawned = dropItem(location, generator(index)) 36 | spawned 37 | } 38 | 39 | private val serializer = LegacyComponentSerializer.builder().build() 40 | private val colorRegex = Regex("§[\\da-fk-or]") 41 | val consoleOut 42 | get() = buildString { 43 | while (true) { 44 | val component = server.consoleSender.nextComponentMessage() ?: break 45 | appendLine( 46 | serializer.serialize(component) 47 | .replace(colorRegex, "") 48 | .replace("[ECleanDebug]", "[DEBUG]") 49 | .replace("[EClean]", "[INFO ]") 50 | ) 51 | } 52 | } 53 | 54 | val enableDebug = System.getProperty("eclean.debug") != null 55 | fun resetConfig() { 56 | world.entities.forEach(Entity::remove) 57 | Config.config = ConfigData( 58 | debug = enableDebug, 59 | update = false, 60 | duration = Long.MAX_VALUE, 61 | message = mutableMapOf(), 62 | living = LivingConfig(enable = false), 63 | drop = DropConfig(enable = false), 64 | chunk = ChunkConfig(enable = false), 65 | trashcan = TrashcanConfig(), 66 | noOnline = NoOnlineConfig() 67 | ) 68 | // 清空控制台输出 69 | consoleOut 70 | } --------------------------------------------------------------------------------