├── .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 | [](https://github.com/4o4E/EClean/releases/latest)
13 | [](https://github.com/4o4E/EClean/releases)
14 |
15 | [](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 | }
--------------------------------------------------------------------------------