40 Commits
1.0 ... 1.8

Author SHA1 Message Date
c7eba7d902 README.md aktualisiert 2026-03-22 23:06:08 +00:00
77c9e1fcfc README.md aktualisiert 2026-03-22 18:46:44 +00:00
4d1901c03a README.md aktualisiert 2026-03-18 17:28:10 +00:00
99a817d33d Update from Git Manager GUI 2026-03-17 10:11:41 +01:00
3322a500e1 README.md aktualisiert 2026-03-01 11:13:27 +00:00
7e197a9ac6 Upload dependency-reduced-pom.xml via GUI 2026-02-28 15:31:02 +00:00
63988befeb Update from Git Manager GUI 2026-02-28 16:31:00 +01:00
120245c2ad Upload pom.xml via GUI 2026-02-28 15:30:59 +00:00
65f9621c84 Upload dependency-reduced-pom.xml via GUI 2026-01-25 19:28:57 +00:00
68a4a670ee Update from Git Manager GUI 2026-01-25 20:28:55 +01:00
bae5e29db3 Upload pom.xml via GUI 2026-01-25 19:28:54 +00:00
137ba95a24 Dateien nach "/" hochladen 2025-08-22 11:14:57 +00:00
a1e415d6f0 src/main/resources/plugin.yml aktualisiert 2025-08-22 11:12:26 +00:00
b6b44d25c5 src/main/java/viper/MotionSensorGUI.java aktualisiert 2025-08-22 11:11:59 +00:00
dc173691a4 Dateien nach "src/main/java/viper" hochladen 2025-08-13 16:36:29 +00:00
f42e91f253 src/main/java/viper/DataManager.java aktualisiert 2025-08-13 16:35:56 +00:00
50f4827ac1 src/main/java/viper/ConfigManager.java aktualisiert 2025-08-13 16:35:45 +00:00
221151dfd2 src/main/java/viper/ButtonListener.java aktualisiert 2025-08-13 16:35:33 +00:00
b1de85aa22 src/main/java/viper/ButtonControl.java aktualisiert 2025-08-13 16:35:23 +00:00
85e78307b5 src/main/resources/config.yml aktualisiert 2025-08-13 16:34:24 +00:00
3dedf24f92 Dateien nach "src/main/java/viper" hochladen 2025-08-11 15:32:15 +00:00
f05c63307a src/main/java/viper/ConfigManager.java aktualisiert 2025-08-11 15:31:31 +00:00
edb366acd6 src/main/java/viper/ButtonListener.java aktualisiert 2025-08-11 15:31:19 +00:00
8e753d1479 src/main/java/viper/ButtonControl.java aktualisiert 2025-08-11 15:31:06 +00:00
a7af54e61c src/main/resources/plugin.yml aktualisiert 2025-08-11 15:30:51 +00:00
33759bb44b src/main/resources/lang.yml aktualisiert 2025-08-11 15:30:41 +00:00
b8715ced69 src/main/resources/config.yml aktualisiert 2025-08-11 15:30:29 +00:00
302b188f50 pom.xml aktualisiert 2025-08-11 15:30:10 +00:00
0e138d3163 pom.xml aktualisiert 2025-08-09 09:53:10 +00:00
dd95261f78 src/main/resources/plugin.yml aktualisiert 2025-08-09 09:52:53 +00:00
3d2688c98b src/main/resources/lang.yml aktualisiert 2025-08-09 09:52:42 +00:00
0859bd1304 src/main/resources/config.yml aktualisiert 2025-08-09 09:52:30 +00:00
e4dc8e120f src/main/java/viper/DataManager.java aktualisiert 2025-08-09 09:52:13 +00:00
c59c131bcc src/main/java/viper/ConfigManager.java aktualisiert 2025-08-09 09:52:00 +00:00
82701d2cbe src/main/java/viper/ButtonListener.java aktualisiert 2025-08-09 09:51:48 +00:00
10c17850d1 src/main/java/viper/ButtonControl.java aktualisiert 2025-08-09 09:51:32 +00:00
4b85a3fbba LICENSE gelöscht 2025-08-06 07:15:00 +00:00
c33c520433 README.md aktualisiert 2025-08-06 07:14:50 +00:00
72918437ca README.md aktualisiert 2025-08-06 07:12:26 +00:00
4312eb14d3 Dateien nach "/" hochladen 2025-08-06 07:10:28 +00:00
17 changed files with 3432 additions and 836 deletions

232
LICENSE
View File

@@ -1,232 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and other kinds of works.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
“This License” refers to version 3 of the GNU General Public License.
“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
A “covered work” means either the unmodified Program or a work based on the Program.
To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
Button-Control
Copyright (C) 2025 M_Viper
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
Button-Control Copyright (C) 2025 M_Viper
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.

510
README.md
View File

@@ -1,85 +1,503 @@
# 🔘 ButtonControl
# 📖 ButtonControl Vollständige Anleitung
Ein leistungsstarkes Minecraft-Plugin zum Steuern von Türen und Redstone-Lampen über Buttons und Tageslichtsensoren.
Ideal für smarte Beleuchtung und Türsysteme in Survival-, Citybuild- oder Roleplay-Servern.
> **Version:** 1.8 · **Autor:** M_Viper · **Spigot:** [spigotmc.org/resources/127702](https://www.spigotmc.org/resources/127702/)
---
## 📦 Features
## 📋 Inhaltsverzeichnis
- 🎛️ **Steuer-Button** steuert beliebige Türen oder Lampen
- 🌞 **Tageslichtsensor-Erkennung** automatische Lampensteuerung bei Tag/Nacht
- 🔌 **Verbindungssystem** verknüpfe mehrere Blöcke mit einem Steuergerät
- 🧱 **Custom Rezepte** eigene Crafting-Rezepte für Steuer-Button & Tageslichtsensor
- 🔍 **Kommando `/bc info`** zeigt Plugin-Infos direkt im Spiel an
- ✅ Vollständig kompatibel mit **Minecraft 1.21.5 1.21.8**
1. [Was ist ButtonControl?](#was-ist-buttoncontrol)
2. [Controller-Typen im Überblick](#controller-typen-im-überblick)
3. [Steuerbare Blöcke](#steuerbare-blöcke)
4. [Schritt-für-Schritt: Erste Schritte](#schritt-für-schritt-erste-schritte)
5. [Rezepte Controller herstellen](#rezepte--controller-herstellen)
6. [Controller platzieren & verbinden](#controller-platzieren--verbinden)
7. [Bewegungsmelder konfigurieren](#bewegungsmelder-konfigurieren)
8. [Geheimwand (Secret Wall)](#geheimwand-secret-wall)
9. [Zeitplan konfigurieren](#zeitplan-konfigurieren)
10. [Controller umbenennen](#controller-umbenennen)
11. [Trust-System](#trust-system)
12. [Alle Befehle im Überblick](#alle-befehle-im-überblick)
13. [Berechtigungen](#berechtigungen)
14. [Konfigurationsdateien](#konfigurationsdateien)
15. [Häufige Fragen & Probleme](#häufige-fragen--probleme)
---
## 🛠️ Installation
## Was ist ButtonControl?
1. Lade die neueste `.jar`-Datei herunter
2. Lege sie in den `plugins/`-Ordner deines Servers
3. Starte oder reloade deinen Server
4. Fertig! 🎉
ButtonControl erlaubt es Spielern, **Türen, Eisentüren, Zauntore, Falltüren, Redstone- und Kupferlampen, Gitter, Creaking Heart, Spender/Werfer, Notenblöcke und Glocken** mit einem selbst hergestellten Controller zu steuern ohne Redstone-Kabel, ohne Mechanismen.
**Mögliche Controller-Typen:**
- Holz- und Steinbuttons aller Arten
- Tageslichtsensoren (öffnen/schließen automatisch nach Tageszeit)
- Bewegungsmelder (Tripwire Hook reagiert auf Spieler **und** Mobs)
- Teppich-Sensoren (reagieren **nur** auf Spieler)
- Schilder (wandmontierte Controller)
---
## 🎮 Befehle
## Controller-Typen im Überblick
| Befehl | Beschreibung |
|----------------|------------------------------------------|
| `/bc info` | Zeigt Informationen über das Plugin an |
| Symbol | Controller | Auslöser | Besonderheit |
|--------|-----------|---------|--------------|
| 🔘 | **Steuer-Button** | Rechtsklick | Manuelles Öffnen/Schließen |
| ☀️ | **Steuer-Tageslichtsensor** | Tag/Nacht-Wechsel | Automatisch, kein Klick nötig |
| 🪝 | **Steuer-Bewegungsmelder** | Spieler + Mobs in der Nähe | Einstellbarer Radius & Verzögerung |
| 🟫 | **Steuer-Teppich** | Nur Spieler in der Nähe | Mobs lösen ihn **nicht** aus |
| 🪧 | **Steuer-Schild** | Rechtsklick | Wandmontiert, unsichtbarer Controller |
---
## 🧪 Getestete Versionen
## Steuerbare Blöcke
- ✅ Minecraft: **1.21.5** **1.21.8**
- ✅ Java 17+
- ✅ Paper / Spigot / Purpur
Folgende Blöcke können mit einem Controller verbunden werden:
| Block | Funktion | Anmerkung |
|-------|---------|-----------|
| Alle Holztüren | Öffnen / Schließen | Inkl. Doppeltüren (beide Hälften werden automatisch erkannt) |
| **Eisentür** | Öffnen / Schließen | Kein Redstone-Signal nötig funktioniert direkt |
| Alle Holz-Falltüren | Öffnen / Schließen | |
| **Eisenfalltür** | Öffnen / Schließen | Wie Eisentür, kein Redstone nötig |
| Alle Zauntore | Öffnen / Schließen | |
| Redstone-Lampe + Kupferlampen | Ein / Ausschalten | Unterstützt normale, verwitterte und gewachste Kupferlampen |
| Creaking Heart (Knarrherz) | Aktivieren / Deaktivieren | Bleibt aktiv bis manuell ausgeschaltet |
| Gitter (`*_GRATE`) + Eisenstangen | Öffnen / Schließen | Öffnen = temporär frei (AIR), Schließen = Originalmaterial wird wiederhergestellt |
| Spender (Dispenser) | Auslösen | Kann per Zeitplan als Show laufen |
| Werfer (Dropper) | Auslösen | Kann per Zeitplan als Show laufen |
| Notenblock | Klingelton abspielen | Instrument pro Spieler einstellbar |
| Glocke | Läuten | |
---
## 📐 Crafting-Rezepte
## Schritt-für-Schritt: Erste Schritte
### 🪛 Steuer-Button
### 1. Controller herstellen
Stelle einen Controller in der Werkbank her (siehe [Rezepte](#rezepte--controller-herstellen)).
| | | |
|---|---|---|
| | 🔘 | |
| | 🔘 | |
| | 🔘 | |
### 2. Controller platzieren
Halte den hergestellten Controller in der Hand und **platziere ihn** wie einen normalen Block. Du erhältst die Nachricht: `§aController platziert.`
### 🌞 Steuer-Tageslichtsensor
### 3. Blöcke verbinden
Halte den Controller weiterhin **in der Hand** (nicht platziert!) und **klicke mit Rechtsklick** auf einen Zielblock (Tür, Lampe usw.). Du erhältst: `§aBlock verbunden.`
| | | |
|---|---|---|
| | 🌞 | |
| | 🌞 | |
| | 🌞 | |
> Du kannst denselben Controller mit mehreren Blöcken verbinden einfach nacheinander alle Zielblöcke anklicken.
> 🔘 = Stone Button
> 🌞 = Daylight Detector
### 4. Controller benutzen
Klicke mit **Rechtsklick** auf den platzierten Controller. Alle verbundenen Blöcke werden gleichzeitig umgeschaltet.
---
## 👤 Autor
## Rezepte Controller herstellen
**M_Viper**
Plugin-Entwicklung & Idee
Alle Rezepte folgen demselben Muster: **3× dasselbe Material in der mittleren Spalte** der Werkbank.
```
[ ] [X] [ ]
[ ] [X] [ ]
[ ] [X] [ ]
```
| Ergebnis | Zutat (X) |
|---------|---------|
| Steuer-Button (Eiche) | Eichen-Button |
| Steuer-Button (Stein) | Stein-Button |
| Steuer-Button (jede Holzart) | Entsprechender Button |
| Steuer-Tageslichtsensor | Tageslichtsensor |
| Steuer-Notenblock | Notenblock |
| Steuer-Bewegungsmelder | Tripwire Hook |
| Steuer-Schild | Eichenschild |
| Steuer-Teppich (Weiß) | Weißer Teppich |
| Steuer-Teppich (alle Farben) | Entsprechender Teppich |
> Alle 16 Teppichfarben können als Sensor verwendet werden sie verhalten sich identisch, nur die Farbe unterscheidet sich.
---
## 📄 Lizenz
## Controller platzieren & verbinden
Dieses Plugin ist **Open Source** und steht unter der **MIT License**.
Du darfst es frei nutzen, verändern und teilen Credits willkommen. 🤝
### Platzieren
1. Halte den fertigen Controller in der Hand
2. Platziere ihn wie einen normalen Block auf einer Fläche
3.`Controller platziert.`
### Verbinden
1. Halte den **nicht platzierten** Controller in der Hand
2. Klicke mit **Rechtsklick** auf einen Zielblock
3.`Block verbunden.`
### Grenzen pro Controller
| Block-Typ | Standard-Limit |
|-----------|---------------|
| Türen (inkl. Eisentür) | 20 |
| Zauntore | 20 |
| Falltüren (inkl. Eisen) | 20 |
| Redstone- und Kupferlampen | 50 |
| Spender | 20 |
| Werfer | 20 |
| Notenblöcke | 10 |
| Glocken | 5 |
> Limits können vom Server-Admin in `config.yml` angepasst werden.
### Controller abbauen
Schlage den Controller ab. Nur der **Besitzer** oder ein Admin darf ihn abbauen.
Beim Abbau werden jetzt alle zugehörigen Daten automatisch entfernt (sowohl in `data.yml` als auch in MySQL):
- Verbindungen
- Trust/Public-Status
- Zeitplan
- Bewegungsmelder-Einstellungen
- Secret-Wall-Blöcke, Delay und Animation
> ⚠️ Wenn ein verbundener Block (Tür, Lampe usw.) abgebaut wird, entfernt ButtonControl den Eintrag **automatisch** aus der Liste.
### Verbundene Blöcke anzeigen
Sieh einen platzierten Controller an (max. 5 Blöcke Entfernung) und tippe:
```
/bc list
```
Du siehst alle verbundenen Blöcke mit Typ, Koordinaten und Welt sowie den aktuellen Zeitplan.
---
## 💡 Weitere Ideen?
## Bewegungsmelder konfigurieren
Wenn du Vorschläge oder Bugs hast öffne ein [Issue](https://github.com/dein-benutzername/ButtonControl/issues) oder einen Pull Request!
### Tripwire Hook (Standard-Bewegungsmelder)
- Erkennt **Spieler und Mobs** in einem einstellbaren Radius
- Öffnet verbundene Blöcke sobald jemand in der Nähe ist
- Schließt sie automatisch nach einer konfigurierbaren Verzögerung
### Teppich-Sensor
- Funktioniert genauso wie der Tripwire Hook
- Erkennt jedoch **nur Spieler** Tiere, Monster und andere Mobs lösen ihn **nicht** aus
- Ideal für Eingänge wo Tiere nicht versehentlich Türen öffnen sollen
### GUI öffnen
Klicke mit **Rechtsklick** auf einen platzierten Bewegungsmelder oder Teppich-Sensor.
```
┌─────────────────────────────┐
│ Bewegungsmelder-Einstellungen │
│ │
│ [🧭 Radius] [ ] [🕐 Verzögerung] │
│ │
│ [💚 Speichern] │
└─────────────────────────────┘
```
| Taste | Aktion |
|-------|--------|
| **Linksklick** auf Kompass | Radius +0,5 Blöcke |
| **Rechtsklick** auf Kompass | Radius 0,5 Blöcke |
| **Linksklick** auf Uhr | Verzögerung +1 Sekunde |
| **Rechtsklick** auf Uhr | Verzögerung 1 Sekunde |
| **Klick** auf Smaragd | Speichern & Schließen |
**Wertebereiche:**
- Radius: 0,5 20,0 Blöcke
- Verzögerung: 1 30 Sekunden
Secret-Wall-Verhalten mit Sensoren:
- Bewegungsmelder/Teppich kann auch eine Secret Wall öffnen
- Bei Erkennung: Wall öffnet
- Nach Ablauf der Verzögerung ohne Erkennung: Wall schließt
---
## Geheimwand (Secret Wall)
Mit Secret Walls kannst du Blöcke eines Eingangs temporär „wegfahren“ lassen und automatisch wiederherstellen.
### Einrichtung
1. Controller ansehen und auswählen:
```
/bc secret select
```
2. Geheimblöcke nacheinander hinzufügen (jeweils Block ansehen):
```
/bc secret add
```
3. Optional Animation setzen:
```
/bc secret animation <instant|wave|reverse|center>
```
4. Optional Wiederherstellungszeit setzen:
```
/bc secret delay <sekunden>
```
5. Status prüfen:
```
/bc secret info
```
### Animationen
- `instant`: alle Blöcke gleichzeitig
- `wave`: der Reihe nach
- `reverse`: umgekehrte Reihenfolge
- `center`: von der Mitte nach außen (und beim Schließen außen nach innen)
Hinweis:
- Secret Walls funktionieren auch ohne normale verbundene Blöcke.
- Tageslichtsensoren und Bewegungsmelder können Secret Walls automatisch öffnen/schließen.
---
## Zeitplan konfigurieren
Der Zeitplan erlaubt es, verbundene Blöcke **automatisch zu einer bestimmten Ingame-Uhrzeit** zu öffnen und zu schließen ohne dass jemand klicken muss.
**Beispielanwendungen:**
- Dorftor öffnet automatisch morgens um 07:00, schließt abends um 19:00
- Laternenpfahl-Lampen schalten sich nachts ein, tagsüber aus
- Geschäfts-Eingang öffnet nur zu "Öffnungszeiten"
- Feuerwerk-Show startet abends automatisch und endet nachts (mit Werfern/Spendern)
### GUI öffnen
Sieh den Controller an und tippe:
```
/bc schedule
```
```
┌─────────────────────────────────┐
│ Zeitplan-Einstellungen │
│ │
│ [⏱ Delay] [⚖ Modus] [🔧 An/Aus] │
│ [🟢 Öffnungszeit] [🔴 Schließzeit] │
│ │
│ [💚 Speichern] │
└─────────────────────────────────┘
```
| Taste | Aktion |
|-------|--------|
| **Linksklick** auf Zeit-Item | +1 Stunde |
| **Rechtsklick** auf Zeit-Item | 1 Stunde |
| **Shift + Linksklick** | +15 Minuten |
| **Shift + Rechtsklick** | 15 Minuten |
| **Link/Rechtsklick** auf Delay | ±1 Tick (Shift: ±5) |
| **Klick** auf Modus | `gleichzeitig` / `nacheinander` umschalten |
| **Klick** auf Hebel/Strauch | Zeitplan ein-/ausschalten |
| **Klick** auf Smaragd | Speichern & Schließen |
> ⚠️ Die Zeiten sind **Ingame-Zeiten** (ein Minecraft-Tag = 20 Minuten Echtzeit).
> Beispiel: "07:00" = Minecraft-Sonnenaufgang, "19:00" = Sonnenuntergang.
> Hinweis zu Werfer/Spender: Wenn ein Controller einen aktiven Zeitplan hat, lösen verbundene Werfer/Spender während des Zeitfensters automatisch aus.
> - Modus `gleichzeitig`: Alle verbundenen Werfer/Spender schießen pro Zyklus zusammen.
> - Modus `nacheinander`: Pro Zyklus schießt ein Gerät, dann rotiert es zum nächsten.
> - Die Delay-Anzeige zeigt Ticks und Sekunden (z.B. `20 Ticks (1.00s)`).
**Über Mitternacht:** Zeitpläne die über Mitternacht gehen (z.B. Öffnen 22:00, Schließen 04:00) werden korrekt erkannt.
---
## Controller umbenennen
Du kannst jedem Controller einen eigenen Namen geben, der bei `/bc list` angezeigt wird.
```
/bc rename <Name>
```
**Beispiele:**
```
/bc rename Haupteingang
/bc rename Scheunentür Nordseite
/bc rename Licht Wohnraum
```
> Maximale Länge: 32 Zeichen. Leerzeichen sind erlaubt. Farbcodes mit § werden unterstützt.
---
## Trust-System
Mit dem Trust-System kannst du anderen Spielern erlauben, **deinen Controller zu benutzen** ohne dass sie ihn verwalten oder abbauen dürfen.
### Spieler hinzufügen
Sieh den Controller an und tippe:
```
/bc trust <Spielername>
```
Der Spieler darf nun den Controller benutzen (auch wenn er offline ist, wenn er vorher schon einmal auf dem Server war).
### Spieler entfernen
```
/bc untrust <Spielername>
```
### Controller öffentlich machen
```
/bc public
```
Jeder Spieler auf dem Server kann den Controller nun benutzen kein Trust nötig.
### Controller privat machen
```
/bc private
```
Nur du (und vertraute Spieler) können den Controller benutzen.
### Aktuellen Status anzeigen
```
/bc list
```
Zeigt unter anderem ob der Controller öffentlich oder privat ist.
---
## Alle Befehle im Überblick
| Befehl | Beschreibung | Berechtigung |
|--------|-------------|-------------|
| `/bc info` | Plugin-Version und Statistiken anzeigen | Jeder |
| `/bc list` | Verbundene Blöcke des angesehenen Controllers anzeigen | Besitzer / Trusted / Admin |
| `/bc rename <Name>` | Controller umbenennen (Controller ansehen) | Besitzer / Admin |
| `/bc schedule` | Zeitplan-GUI öffnen (Controller ansehen) | Besitzer / Admin |
| `/bc secret select` | Secret-Controller auswählen (alternativ Blickerkennung) | Besitzer / Admin |
| `/bc secret add` | Angesehenen Block als Geheimblock hinzufügen | Besitzer / Admin |
| `/bc secret remove` | Angesehenen Geheimblock entfernen | Besitzer / Admin |
| `/bc secret clear` | Alle Geheimblöcke des Controllers löschen | Besitzer / Admin |
| `/bc secret delay <Sekunden>` | Auto-Restore-Zeit für Secret Wall setzen | Besitzer / Admin |
| `/bc secret animation <instant\|wave\|reverse\|center>` | Secret-Wall-Animation setzen | Besitzer / Admin |
| `/bc secret info` | Secret-Wall-Konfiguration anzeigen | Besitzer / Admin |
| `/bc note <Instrument>` | Notenblock-Instrument ändern | `buttoncontrol.note` |
| `/bc trust <Spieler>` | Spieler darf Controller benutzen | Besitzer / Admin |
| `/bc untrust <Spieler>` | Berechtigung entziehen | Besitzer / Admin |
| `/bc public` | Controller für alle freigeben | Besitzer / Admin |
| `/bc private` | Controller nur für Besitzer & Trusted | Besitzer / Admin |
| `/bc reload` | Konfiguration neu laden | `buttoncontrol.reload` |
### Verfügbare Instrumente für `/bc note`
```
PIANO BASS_DRUM SNARE_DRUM STICKS
BASS_GUITAR FLUTE BELL CHIME
GUITAR XYLOPHONE IRON_XYLOPHONE COW_BELL
DIDGERIDOO BIT BANJO PLING
```
**Beispiel:**
```
/bc note FLUTE
```
---
## Berechtigungen
| Permission | Beschreibung | Standard |
|-----------|-------------|---------|
| `buttoncontrol.admin` | Zugriff auf **alle** Controller (Bypass) | OP |
| `buttoncontrol.reload` | `/bc reload` ausführen | OP |
| `buttoncontrol.note` | Instrument mit `/bc note` ändern | Alle |
| `buttoncontrol.trust` | Trust-System verwenden | Alle |
| `buttoncontrol.update` | Update-Benachrichtigungen erhalten | OP |
### Admin-Bypass (`buttoncontrol.admin`)
Admins mit dieser Berechtigung können:
- Jeden Controller benutzen (auch private)
- Jeden Controller verwalten (trust, rename, schedule, public/private)
- Jeden Controller abbauen
- Alle verbundenen Blöcke einsehen (`/bc list`)
---
## Konfigurationsdateien
### `config.yml` Hauptkonfiguration
```yaml
# Maximale Anzahl verbundener Blöcke pro Controller
max-doors: 20 # Holz- und Eisentüren
max-lamps: 50 # Redstone-Lampen
max-noteblocks: 10 # Notenblöcke
max-gates: 20 # Zauntore
max-trapdoors: 20 # Holz- und Eisenfalltüren
max-bells: 5 # Glocken
max-dispensers: 20 # Spender
max-droppers: 20 # Werfer
# Notenblock-Einstellungen
default-note: "PIANO" # Standard-Instrument
double-note-enabled: true # Zweiter Ton aktiviert
double-note-delay-ms: 1000 # Abstand zwischen den Tönen (ms)
# Bewegungsmelder-Standardwerte (überschreibbar per GUI pro Sensor)
motion-detection-radius: 5.0 # Erkennungsradius in Blöcken
motion-close-delay-ms: 5000 # Verzögerung vor dem Schließen (ms)
motion-trigger-cooldown-ms: 2000 # Mindestzeit zwischen zwei Auslösungen
timed-container-interval-ticks: 40 # Legacy-Fallback für alte Zeitpläne ohne gespeicherten Delay-Wert
timed-container-shot-delay-ticks: 2 # Standard-Delay zwischen Schüssen im Zeitplan
timed-container-trigger-mode: simultaneous # Standardmodus: simultaneous oder sequential
# Sounds beim Öffnen/Schließen
sounds:
enabled: true
door-open: BLOCK_WOODEN_DOOR_OPEN
door-close: BLOCK_WOODEN_DOOR_CLOSE
iron-door-open: BLOCK_IRON_DOOR_OPEN
iron-door-close: BLOCK_IRON_DOOR_CLOSE
lamp-on: BLOCK_LEVER_CLICK
lamp-off: BLOCK_LEVER_CLICK
```
### `lang.yml` Nachrichten anpassen
Alle Spielernachrichten können in `lang.yml` geändert werden. Farbcodes mit `§` sind überall unterstützt.
```yaml
tueren-geoeffnet: "§aTüren wurden geöffnet."
controller-platziert: "§aController platziert."
# ... usw.
```
### `data.yml` Spielerdaten
Diese Datei wird **automatisch** verwaltet und sollte nicht manuell bearbeitet werden. Sie enthält alle Controller-Positionen, Verbindungen, Trust-Einstellungen, Zeitpläne, Bewegungsmelder-Settings und Secret-Wall-Daten.
---
## Häufige Fragen & Probleme
**❓ Ich habe einen Controller platziert, aber beim Klicken passiert nichts.**
→ Du musst erst Blöcke verbinden. Halte den Controller **in der Hand** (nicht den platzierten Block anklicken) und klicke auf Türen, Lampen usw.
**❓ Ich kann den Controller eines anderen Spielers nicht benutzen.**
→ Der Controller ist privat. Bitte den Besitzer, dich per `/bc trust <DeinName>` hinzuzufügen oder den Controller mit `/bc public` zu öffnen.
**❓ Ich kann den Controller nicht abbauen.**
→ Nur der Besitzer (oder ein Admin mit `buttoncontrol.admin`) darf einen Controller abbauen.
**❓ Die Eisentür öffnet sich nicht.**
→ Stelle sicher, dass die Eisentür tatsächlich mit dem Controller verbunden ist. Halte den Controller in der Hand und klicke auf die Eisentür (untere Hälfte). Die obere Hälfte wird automatisch mitgenommen.
**❓ Der Bewegungsmelder schließt nicht nach der Zeit.**
→ Prüfe den Wert `motion-close-delay-ms` in der GUI (Rechtsklick auf den Sensor) oder in `config.yml`. Standardmäßig sind es 5 Sekunden.
**❓ Der Teppich-Sensor reagiert auf Mobs.**
→ Nur der **Steuer-Teppich** (hergestellt mit dem Rezept) erkennt nur Spieler. Ein normaler Teppich ist kein Controller.
**❓ Der Zeitplan funktioniert nicht.**
→ Stelle sicher, dass der Zeitplan in der ScheduleGUI **aktiviert** ist (grüner Hebel, nicht Strauch). Öffne die GUI mit `/bc schedule` und prüfe den Ein/Aus-Status.
**❓ Werfer/Spender schießen zu langsam oder zu schnell im Zeitplan.**
→ Stelle den Delay direkt in `/bc schedule` ein (GUI zeigt Ticks + Sekunden). Für globale Standardwerte passe `timed-container-shot-delay-ticks` in `config.yml` an und führe `/bc reload` aus.
**❓ 2 Werfer/Spender am selben Controller laufen nicht gleich.**
→ Öffne `/bc schedule` und stelle den Modus auf `gleichzeitig`. Im Modus `nacheinander` rotieren die Geräte absichtlich.
**`/bc list` zeigt "Keine Blöcke verbunden".**
→ Entweder wurde der Controller noch nie mit Blöcken verbunden, oder alle verbundenen Blöcke wurden abgebaut (werden automatisch entfernt).
**❓ Wie sehe ich ob ein Controller einen Zeitplan hat?**
`/bc list` zeigt ganz unten den aktiven Zeitplan mit Öffnungs- und Schließzeit an.
**❓ Kann ich mehrere Controller auf dieselbe Tür zeigen lassen?**
→ Ja. Du kannst z.B. einen Button-Controller und einen Bewegungsmelder mit derselben Tür verbinden.
---
*Diese Anleitung bezieht sich auf ButtonControl v1.8. Für ältere Versionen können einzelne Funktionen abweichen.*

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>viper</groupId>
<artifactId>ButtonControl</artifactId>
<version>1.4</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
</properties>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- bStats Monitoring -->
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>de.viper.bstats</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

87
pom.xml Normal file
View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>viper</groupId>
<artifactId>ButtonControl</artifactId>
<version>1.6</version>
<packaging>jar</packaging>
<name>ButtonControl</name>
<repositories>
<!-- Spigot-Repository -->
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
</repositories>
<dependencies>
<!-- Spigot API (bereitgestellt vom Server) -->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.21.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- bStats Bukkit -->
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.2</version>
</dependency>
<!-- org.json für UpdateChecker JSON-Parsing -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- Shade Plugin zum Relocaten von bStats -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>viper.shaded.bstats</shadedPattern>
</relocation>
</relocations>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -7,130 +7,647 @@ import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.Lightable;
import org.bukkit.block.data.type.NoteBlock;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.Note;
import org.bukkit.Note.Tone;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ButtonControl extends JavaPlugin {
private ConfigManager configManager;
private DataManager dataManager;
// Bewegungsmelder-State
private final Map<String, Long> lastMotionDetections = new HashMap<>();
private final Set<String> activeSensors = new HashSet<>();
// Zeitgesteuerte Automation verhindert mehrfaches Auslösen pro Zustandswechsel
private final Map<String, Boolean> timedControllerLastState = new HashMap<>();
// Actionbar-Status pro Spieler für die Namensanzeige
private final Map<java.util.UUID, String> lastControllerActionbar = new HashMap<>();
@Override
public void onEnable() {
configManager = new ConfigManager(this);
dataManager = new DataManager(this);
dataManager = new DataManager(this);
getServer().getPluginManager().registerEvents(new ButtonListener(this, configManager, dataManager), this);
// Update-Checker beim Start
new UpdateChecker(this, 127702).getVersion(version -> {
String current = getDescription().getVersion();
if (isNewerVersion(strip(version), strip(current))) {
getLogger().info("Update verfügbar: v" + version);
Bukkit.getScheduler().runTask(this, () ->
Bukkit.getOnlinePlayers().stream()
.filter(p -> p.hasPermission("buttoncontrol.update"))
.forEach(p -> sendUpdateMessage(p, current, version)));
} else {
getLogger().info("ButtonControl ist auf dem neuesten Stand (v" + current + ").");
}
});
// Update beim Joinen
getServer().getPluginManager().registerEvents(new org.bukkit.event.Listener() {
@org.bukkit.event.EventHandler
public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event) {
Player player = event.getPlayer();
if (!player.hasPermission("buttoncontrol.update")) return;
new UpdateChecker(ButtonControl.this, 127702).getVersion(version -> {
String current = getDescription().getVersion();
if (isNewerVersion(strip(version), strip(current)))
sendUpdateMessage(player, current, version);
});
}
}, this);
if (getCommand("bc") != null) {
getCommand("bc").setExecutor(this);
getCommand("bc").setTabCompleter(new ButtonTabCompleter());
}
getServer().getPluginManager().registerEvents(
new ButtonListener(this, configManager, dataManager), this);
registerRecipes();
MetricsHandler.startMetrics(this);
// Scheduler zum Prüfen der Tageslichtsensoren alle 10 Sekunden (20 Ticks = 1 Sekunde)
getServer().getScheduler().runTaskTimer(this, this::checkDaylightSensors, 0L, 20L * 10);
getServer().getScheduler().runTaskTimer(this, this::checkDaylightSensors, 0L, 20L * 10);
getServer().getScheduler().runTaskTimer(this, this::checkMotionSensors, 0L, 10L);
getServer().getScheduler().runTaskTimer(this, this::checkTimedControllers, 0L, 20L * 5);
getServer().getScheduler().runTaskTimer(this, this::updateControllerNameActionBar, 0L, 5L);
getLogger().info("ButtonControl v" + getDescription().getVersion() + " wurde erfolgreich aktiviert!");
}
@Override
public void onDisable() {
if (dataManager != null) {
dataManager.shutdown();
}
}
// -----------------------------------------------------------------------
// Update-Hilfe
// -----------------------------------------------------------------------
private String strip(String v) {
return v.replaceFirst("(?i)^(version\\s*|v\\.?\\s*)", "").trim();
}
private void sendUpdateMessage(Player player, String current, String latest) {
player.sendMessage("");
player.sendMessage("§8§m-----------------------------------------");
player.sendMessage(" §6§lButtonControl §7- §e§lUpdate verfügbar!");
player.sendMessage("");
player.sendMessage(" §7Aktuelle Version: §c" + current);
player.sendMessage(" §7Neue Version: §a" + latest);
player.sendMessage("");
player.sendMessage(" §eDownload: §bhttps://www.spigotmc.org/resources/127702/");
player.sendMessage("§8§m-----------------------------------------");
player.sendMessage("");
}
private boolean isNewerVersion(String latest, String current) {
try {
String[] lp = latest.split("\\.");
String[] cp = current.split("\\.");
int len = Math.max(lp.length, cp.length);
for (int i = 0; i < len; i++) {
int l = i < lp.length ? Integer.parseInt(lp[i]) : 0;
int c = i < cp.length ? Integer.parseInt(cp[i]) : 0;
if (l > c) return true;
if (l < c) return false;
}
return false;
} catch (NumberFormatException e) {
return !latest.equalsIgnoreCase(current);
}
}
// -----------------------------------------------------------------------
// Rezepte
// -----------------------------------------------------------------------
private void registerRecipes() {
ItemStack controlButton = new ItemStack(Material.STONE_BUTTON);
ItemMeta buttonMeta = controlButton.getItemMeta();
buttonMeta.setDisplayName("§6Steuer-Button");
controlButton.setItemMeta(buttonMeta);
// Alle Holz/Stein-Buttons
for (Material mat : Material.values()) {
if (!mat.name().endsWith("_BUTTON")) continue;
registerColumnRecipe(
"control_" + mat.name().toLowerCase(), mat,
"§6Steuer-" + friendlyName(mat, "_BUTTON"),
Arrays.asList("§7Ein universeller Controller.",
"§7Verbindet Türen, Lampen und mehr.")
);
}
NamespacedKey buttonKey = new NamespacedKey(this, "control_button");
ShapedRecipe buttonRecipe = new ShapedRecipe(buttonKey, controlButton);
buttonRecipe.shape("123", "456", "789");
buttonRecipe.setIngredient('2', Material.STONE_BUTTON);
buttonRecipe.setIngredient('5', Material.STONE_BUTTON);
buttonRecipe.setIngredient('8', Material.STONE_BUTTON);
Bukkit.addRecipe(buttonRecipe);
// Tageslichtsensor
registerColumnRecipe("control_daylight", Material.DAYLIGHT_DETECTOR,
"§6Steuer-Tageslichtsensor",
Arrays.asList("§7Öffnet/schließt nach Tageszeit."));
ItemStack controlDaylight = new ItemStack(Material.DAYLIGHT_DETECTOR);
ItemMeta daylightMeta = controlDaylight.getItemMeta();
daylightMeta.setDisplayName("§6Steuer-Tageslichtsensor");
controlDaylight.setItemMeta(daylightMeta);
// Notenblock
registerColumnRecipe("control_noteblock", Material.NOTE_BLOCK,
"§6Steuer-Notenblock",
Arrays.asList("§7Spielt einen Klingelton ab."));
NamespacedKey daylightKey = new NamespacedKey(this, "control_daylight");
ShapedRecipe daylightRecipe = new ShapedRecipe(daylightKey, controlDaylight);
daylightRecipe.shape("123", "456", "789");
daylightRecipe.setIngredient('2', Material.DAYLIGHT_DETECTOR);
daylightRecipe.setIngredient('5', Material.DAYLIGHT_DETECTOR);
daylightRecipe.setIngredient('8', Material.DAYLIGHT_DETECTOR);
Bukkit.addRecipe(daylightRecipe);
// Bewegungsmelder
registerColumnRecipe("control_motion", Material.TRIPWIRE_HOOK,
"§6Steuer-Bewegungsmelder",
Arrays.asList("§7Erkennt Spieler und Mobs in der Nähe."));
// NEU: Schild-Controller
registerColumnRecipe("control_sign", Material.OAK_SIGN,
"§6Steuer-Schild",
Arrays.asList("§7Wandmontierbarer Controller.",
"§7Funktioniert wie ein Button."));
// NEU: Teppich-Sensoren (alle 16 Farben) NUR Spieler
for (Material mat : Material.values()) {
if (!mat.name().endsWith("_CARPET")) continue;
registerColumnRecipe(
"control_carpet_" + mat.name().toLowerCase(), mat,
"§6Steuer-Teppich §8(" + friendlyName(mat, "_CARPET") + "§8)",
Arrays.asList("§7Erkennt NUR Spieler (keine Mobs).",
"§7Bodenbasierter Bewegungsmelder.")
);
}
}
// Prüft alle platzierten Tageslichtsensoren und schaltet Lampen bei Tag aus und bei Nacht an
private void registerColumnRecipe(String keyName, Material mat, String displayName, List<String> lore) {
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(displayName);
meta.setLore(new ArrayList<>(lore));
item.setItemMeta(meta);
}
NamespacedKey key = new NamespacedKey(this, keyName);
if (Bukkit.getRecipe(key) != null) Bukkit.removeRecipe(key);
ShapedRecipe recipe = new ShapedRecipe(key, item);
recipe.shape(" X ", " X ", " X ");
recipe.setIngredient('X', mat);
Bukkit.addRecipe(recipe);
}
/** IRON_DOOR → "§7Iron Door" | OAK_BUTTON → "§7Oak Button" */
String friendlyName(Material mat, String stripSuffix) {
String[] parts = mat.name().replace(stripSuffix, "").split("_");
StringBuilder sb = new StringBuilder("§7");
for (String p : parts) {
if (sb.length() > 2) sb.append(" ");
sb.append(Character.toUpperCase(p.charAt(0)))
.append(p.substring(1).toLowerCase());
}
return sb.toString();
}
// -----------------------------------------------------------------------
// Tageslichtsensor
// -----------------------------------------------------------------------
public void checkDaylightSensors() {
List<String> allControllers = dataManager.getAllPlacedControllers();
for (String controllerLoc : allControllers) {
String buttonId = dataManager.getButtonIdForPlacedController(controllerLoc);
for (String loc : dataManager.getAllPlacedControllers()) {
String buttonId = dataManager.getButtonIdForPlacedController(loc);
if (buttonId == null) continue;
Location location = parseLocation(loc);
if (location == null) continue;
if (location.getBlock().getType() != Material.DAYLIGHT_DETECTOR) continue;
String[] parts = controllerLoc.split(",");
if (parts.length != 4) continue;
World world = getServer().getWorld(parts[0]);
if (world == null) continue;
Location loc = new Location(world,
Integer.parseInt(parts[1]),
Integer.parseInt(parts[2]),
Integer.parseInt(parts[3]));
Block block = loc.getBlock();
if (block.getType() != Material.DAYLIGHT_DETECTOR) continue;
long time = loc.getWorld().getTime();
long time = location.getWorld().getTime();
boolean isDay = time >= 0 && time < 13000;
List<String> connected = dataManager.getConnectedBlocks(buttonId);
if (connected == null) continue;
List<String> connectedBlocks = dataManager.getConnectedBlocks(buttonId);
if (connectedBlocks == null) continue;
for (String targetLocStr : connectedBlocks) {
String[] targetParts = targetLocStr.split(",");
if (targetParts.length != 4) continue;
World targetWorld = getServer().getWorld(targetParts[0]);
if (targetWorld == null) continue;
Location targetLoc = new Location(targetWorld,
Integer.parseInt(targetParts[1]),
Integer.parseInt(targetParts[2]),
Integer.parseInt(targetParts[3]));
Block targetBlock = targetLoc.getBlock();
if (targetBlock.getType() == Material.REDSTONE_LAMP) {
Lightable lamp = (Lightable) targetBlock.getBlockData();
for (String ts : connected) {
Location tl = parseLocation(ts);
if (tl == null) continue;
Block tb = tl.getBlock();
if (tb.getType() == Material.REDSTONE_LAMP) {
Lightable lamp = (Lightable) tb.getBlockData();
lamp.setLit(!isDay);
targetBlock.setBlockData(lamp);
tb.setBlockData(lamp);
}
}
}
}
// Befehlsverarbeitung
// -----------------------------------------------------------------------
// Zeitgesteuerte Automation (NEU)
// -----------------------------------------------------------------------
/**
* Prüft alle 5 Sekunden ob ein Zeitplan (open-time / close-time) für einen Controller
* aktiv ist und öffnet/schließt die verbundenen Blöcke bei Wechsel.
*
* Ingame-Zeit: 0 = Sonnenaufgang, 6000 = Mittag, 13000 = Sonnenuntergang, 18000 = Mitternacht
* Anzeige: ticksToTime() wandelt in "HH:MM" um (Tag beginnt um 06:00).
*/
public void checkTimedControllers() {
for (String controllerLoc : dataManager.getAllPlacedControllers()) {
String buttonId = dataManager.getButtonIdForPlacedController(controllerLoc);
if (buttonId == null) continue;
long openTime = dataManager.getScheduleOpenTime(buttonId);
long closeTime = dataManager.getScheduleCloseTime(buttonId);
if (openTime < 0 || closeTime < 0) continue;
Location loc = parseLocation(controllerLoc);
if (loc == null) continue;
long worldTime = loc.getWorld().getTime() % 24000;
boolean shouldBeOpen;
if (openTime <= closeTime) {
// Normales Intervall: z.B. öffnen 6000, schließen 18000
shouldBeOpen = worldTime >= openTime && worldTime < closeTime;
} else {
// Über Mitternacht: z.B. öffnen 20000, schließen 4000
shouldBeOpen = worldTime >= openTime || worldTime < closeTime;
}
Boolean lastState = timedControllerLastState.get(controllerLoc);
if (lastState != null && lastState == shouldBeOpen) continue;
timedControllerLastState.put(controllerLoc, shouldBeOpen);
List<String> connected = dataManager.getConnectedBlocks(buttonId);
if (connected != null && !connected.isEmpty()) {
setOpenables(connected, shouldBeOpen);
}
}
}
// -----------------------------------------------------------------------
// Controller-Name bei Blickkontakt (Actionbar)
// -----------------------------------------------------------------------
private void updateControllerNameActionBar() {
if (!configManager.getConfig().getBoolean("controller-name-display.enabled", true)) {
clearAllControllerActionBars();
return;
}
int maxDistance = Math.max(1,
configManager.getConfig().getInt("controller-name-display.max-look-distance", 8));
String format = configManager.getConfig().getString(
"controller-name-display.format", "§6Controller: §f%s");
for (Player player : getServer().getOnlinePlayers()) {
String message = null;
Block target = player.getTargetBlockExact(maxDistance);
if (isValidController(target)) {
String targetLoc = toLoc(target);
String buttonId = dataManager.getButtonIdForLocation(targetLoc);
if (buttonId != null) {
boolean canSee = dataManager.canAccess(buttonId, player.getUniqueId())
|| player.hasPermission("buttoncontrol.admin");
String name = dataManager.getControllerName(buttonId);
if (canSee && name != null && !name.trim().isEmpty()) {
message = String.format(format, name);
}
}
}
java.util.UUID uuid = player.getUniqueId();
String previous = lastControllerActionbar.get(uuid);
if (message == null) {
if (previous != null) {
sendActionBar(player, " ");
lastControllerActionbar.remove(uuid);
}
} else if (!message.equals(previous)) {
sendActionBar(player, message);
lastControllerActionbar.put(uuid, message);
}
}
}
private void clearAllControllerActionBars() {
if (lastControllerActionbar.isEmpty()) return;
for (Player player : getServer().getOnlinePlayers()) {
if (lastControllerActionbar.containsKey(player.getUniqueId())) {
sendActionBar(player, " ");
}
}
lastControllerActionbar.clear();
}
private void sendActionBar(Player player, String message) {
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message));
}
// -----------------------------------------------------------------------
// Bewegungsmelder
// -----------------------------------------------------------------------
public void checkMotionSensors() {
long now = System.currentTimeMillis();
for (String controllerLoc : dataManager.getAllPlacedControllers()) {
Location loc = parseLocation(controllerLoc);
if (loc == null) continue;
Material bType = loc.getBlock().getType();
boolean isTripwire = bType == Material.TRIPWIRE_HOOK;
boolean isCarpet = bType.name().endsWith("_CARPET");
if (!isTripwire && !isCarpet) continue;
String buttonId = dataManager.getButtonIdForPlacedController(controllerLoc);
if (buttonId == null) continue;
double radius = dataManager.getMotionSensorRadius(controllerLoc);
if (radius == -1) radius = configManager.getConfig().getDouble("motion-detection-radius", 5.0);
long delay = dataManager.getMotionSensorDelay(controllerLoc);
if (delay == -1) delay = configManager.getConfig().getLong("motion-close-delay-ms", 5000L);
final double r = radius;
boolean detected;
if (isCarpet) {
// NEU: Teppich erkennt NUR Spieler
detected = !loc.getWorld()
.getNearbyEntities(loc, r, r, r, e -> e instanceof Player).isEmpty();
} else {
// Tripwire: alle lebenden Entitäten
detected = !loc.getWorld()
.getNearbyEntities(loc, r, r, r,
e -> e instanceof org.bukkit.entity.LivingEntity).isEmpty();
}
List<String> connected = dataManager.getConnectedBlocks(buttonId);
if (connected == null || connected.isEmpty()) continue;
if (detected) {
if (!activeSensors.contains(controllerLoc)) {
setOpenables(connected, true);
activeSensors.add(controllerLoc);
}
lastMotionDetections.put(controllerLoc, now);
} else {
Long last = lastMotionDetections.get(controllerLoc);
if (last != null && now - last >= delay) {
setOpenables(connected, false);
lastMotionDetections.remove(controllerLoc);
activeSensors.remove(controllerLoc);
}
}
}
}
void setOpenables(List<String> connectedBlocks, boolean open) {
for (String locStr : connectedBlocks) {
Location tl = parseLocation(locStr);
if (tl == null) continue;
Block tb = tl.getBlock();
if (tb.getBlockData() instanceof org.bukkit.block.data.Openable) {
org.bukkit.block.data.Openable o = (org.bukkit.block.data.Openable) tb.getBlockData();
o.setOpen(open);
tb.setBlockData(o);
}
}
}
// -----------------------------------------------------------------------
// Befehle
// -----------------------------------------------------------------------
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (command.getName().equalsIgnoreCase("bc") && args.length > 0 && args[0].equalsIgnoreCase("info")) {
sender.sendMessage("§6[ButtonControl] §7Informationen zum Plugin:");
sender.sendMessage("§eVersion: §f" + getDescription().getVersion());
sender.sendMessage("§eErsteller: §fM_Viper");
sender.sendMessage("§ePlugin: §fButtonControl");
sender.sendMessage("§eGetestet für Minecraft: §f1.21.5 - 1.21.8");
sender.sendMessage("§eWeitere Infos: §fTüren & Lampen mit Buttons oder Tageslichtsensoren steuern");
if (!command.getName().equalsIgnoreCase("bc")) return false;
if (args.length == 0) {
sender.sendMessage("§6[BC] §7/bc <info|reload|note|list|rename|schedule|trust|untrust|public|private>");
return true;
}
return false;
String sub = args[0].toLowerCase();
// INFO
if (sub.equals("info")) {
sender.sendMessage("§6§lButtonControl §7v" + getDescription().getVersion() + " §8by §7M_Viper");
sender.sendMessage("§7Features: §fTüren · Lampen · Notenblöcke · Sensoren · Teppiche · Schilder · Zeitpläne");
sender.sendMessage("§7Controller aktiv: §f" + dataManager.getAllPlacedControllers().size());
return true;
}
// RELOAD
if (sub.equals("reload")) {
if (!sender.hasPermission("buttoncontrol.reload")) {
sender.sendMessage(configManager.getMessage("keine-berechtigung")); return true;
}
configManager.reloadConfig();
dataManager.reloadData();
timedControllerLastState.clear();
clearAllControllerActionBars();
sender.sendMessage(configManager.getMessage("konfiguration-neugeladen"));
return true;
}
// NOTE
if (sub.equals("note") && sender instanceof Player) {
Player player = (Player) sender;
if (args.length < 2) { player.sendMessage("§7/bc note <Instrument>"); return true; }
try {
org.bukkit.Instrument.valueOf(args[1].toUpperCase());
dataManager.setPlayerInstrument(player.getUniqueId(), args[1].toUpperCase());
player.sendMessage(String.format(configManager.getMessage("instrument-gesetzt"), args[1].toUpperCase()));
} catch (Exception e) {
player.sendMessage(configManager.getMessage("ungueltiges-instrument"));
}
return true;
}
// Alle folgenden Befehle erfordern Spieler + angescauten Controller
if (!(sender instanceof Player)) {
sender.sendMessage("§cNur Spieler können diesen Befehl verwenden.");
return true;
}
Player player = (Player) sender;
if (sub.equals("list") || sub.equals("rename") || sub.equals("schedule")
|| sub.equals("trust") || sub.equals("untrust")
|| sub.equals("public") || sub.equals("private")) {
Block target = player.getTargetBlockExact(5);
if (!isValidController(target)) {
player.sendMessage(configManager.getMessage("kein-controller-im-blick")); return true;
}
String targetLoc = toLoc(target);
String buttonId = dataManager.getButtonIdForLocation(targetLoc);
if (buttonId == null) {
player.sendMessage(configManager.getMessage("keine-bloecke-verbunden")); return true;
}
boolean isAdmin = player.hasPermission("buttoncontrol.admin");
boolean isOwner = dataManager.isOwner(buttonId, player.getUniqueId());
switch (sub) {
case "list":
if (!dataManager.canAccess(buttonId, player.getUniqueId()) && !isAdmin) {
player.sendMessage(configManager.getMessage("keine-berechtigung-controller")); return true;
}
sendListInfo(player, buttonId);
break;
case "rename":
if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; }
if (args.length < 2) { player.sendMessage("§7/bc rename <Name>"); return true; }
String newName = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
if (newName.length() > 32) { player.sendMessage("§cName zu lang (max. 32 Zeichen)."); return true; }
dataManager.setControllerName(buttonId, newName);
player.sendMessage(String.format(configManager.getMessage("controller-umbenannt"), newName));
break;
case "schedule":
if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; }
new ScheduleGUI(this, player, buttonId).open();
break;
case "trust":
if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; }
if (args.length < 2) { player.sendMessage("§7/bc trust <Spieler>"); return true; }
org.bukkit.OfflinePlayer tp = Bukkit.getOfflinePlayer(args[1]);
if (!tp.hasPlayedBefore() && !tp.isOnline()) {
player.sendMessage(configManager.getMessage("spieler-nicht-gefunden")); return true;
}
dataManager.addTrustedPlayer(buttonId, tp.getUniqueId());
player.sendMessage(String.format(configManager.getMessage("trust-hinzugefuegt"), args[1]));
break;
case "untrust":
if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; }
if (args.length < 2) { player.sendMessage("§7/bc untrust <Spieler>"); return true; }
dataManager.removeTrustedPlayer(buttonId, Bukkit.getOfflinePlayer(args[1]).getUniqueId());
player.sendMessage(String.format(configManager.getMessage("trust-entfernt"), args[1]));
break;
default: // public / private
if (!isOwner && !isAdmin) { player.sendMessage(configManager.getMessage("nur-besitzer-abbauen")); return true; }
boolean pub = sub.equals("public");
dataManager.setPublic(buttonId, pub);
player.sendMessage(String.format(configManager.getMessage("status-geandert"),
pub ? "§aÖffentlich" : "§cPrivat"));
break;
}
}
return true;
}
public ConfigManager getConfigManager() {
return configManager;
private void sendListInfo(Player player, String buttonId) {
String name = dataManager.getControllerName(buttonId);
String header = name != null
? "§6§l" + name
: "§6§lController §8§o(ID: " + buttonId.substring(0, 8) + "...)";
player.sendMessage(header);
List<String> connected = dataManager.getConnectedBlocks(buttonId);
if (connected == null || connected.isEmpty()) {
player.sendMessage(" §cKeine Blöcke verbunden.");
} else {
player.sendMessage("§7Verbundene Blöcke §8(" + connected.size() + ")§7:");
for (int i = 0; i < connected.size(); i++) {
String ls = connected.get(i);
Location l = parseLocation(ls);
String typeLabel = l != null ? "§e" + l.getBlock().getType().name() : "§8unbekannt";
String[] p = ls.split(",");
String coords = p.length == 4
? "§8(" + p[1] + "§7, §8" + p[2] + "§7, §8" + p[3] + " §7in §f" + p[0] + "§8)" : "";
player.sendMessage(" §8" + (i + 1) + ". " + typeLabel + " " + coords);
}
}
player.sendMessage("§7Status: " + (dataManager.isPublic(buttonId) ? "§aÖffentlich" : "§cPrivat"));
long openT = dataManager.getScheduleOpenTime(buttonId);
long closeT = dataManager.getScheduleCloseTime(buttonId);
if (openT >= 0 && closeT >= 0) {
player.sendMessage("§7Zeitplan: §aÖffnet §7um §e" + ticksToTime(openT)
+ " §7· §cSchließt §7um §e" + ticksToTime(closeT));
} else {
player.sendMessage("§7Zeitplan: §8Nicht gesetzt §7(§e/bc schedule§7)");
}
}
public DataManager getDataManager() {
return dataManager;
// -----------------------------------------------------------------------
// Utility
// -----------------------------------------------------------------------
/** Wandelt Minecraft-Ticks (023999) in "HH:MM" um. Ingame-Tag startet um 06:00. */
public String ticksToTime(long ticks) {
long shifted = (ticks + 6000) % 24000;
long hours = shifted / 1000;
long minutes = (shifted % 1000) * 60 / 1000;
return String.format("%02d:%02d", hours, minutes);
}
/** Wandelt "HH:MM" zurück in Minecraft-Ticks */
public long timeToTicks(int hours, int minutes) {
long totalMinutes = hours * 60L + minutes;
long ticks = (totalMinutes * 1000L / 60L - 6000 + 24000) % 24000;
return ticks;
}
public boolean isValidController(Block b) {
if (b == null) return false;
Material m = b.getType();
return m.name().endsWith("_BUTTON")
|| m == Material.DAYLIGHT_DETECTOR
|| m == Material.TRIPWIRE_HOOK
|| m.name().endsWith("_SIGN")
|| m.name().endsWith("_CARPET");
}
public String toLoc(Block b) {
return b.getWorld().getName() + "," + b.getX() + "," + b.getY() + "," + b.getZ();
}
public Location parseLocation(String locStr) {
String[] parts = locStr.split(",");
if (parts.length != 4) return null;
World world = getServer().getWorld(parts[0]);
if (world == null) return null;
try {
return new Location(world,
Integer.parseInt(parts[1]),
Integer.parseInt(parts[2]),
Integer.parseInt(parts[3]));
} catch (NumberFormatException e) { return null; }
}
public void playDoorbellSound(Location loc, String instrument) {
Block block = loc.getBlock();
if (block.getType() != Material.NOTE_BLOCK) return;
NoteBlock noteBlock = (NoteBlock) block.getBlockData();
try {
org.bukkit.Instrument inst = org.bukkit.Instrument.valueOf(instrument.toUpperCase());
noteBlock.setInstrument(inst);
noteBlock.setNote(new Note(0, Tone.C, false));
block.setBlockData(noteBlock);
loc.getWorld().playSound(loc, inst.getSound(), 1.0f, 1.0f);
if (configManager.getConfig().getBoolean("double-note-enabled", true)) {
long delayTicks = (long)(configManager.getConfig().getInt("double-note-delay-ms", 1000) / 50.0);
getServer().getScheduler().runTaskLater(this, () -> {
if (block.getType() == Material.NOTE_BLOCK)
loc.getWorld().playSound(loc, inst.getSound(), 1.0f, 1.0f);
}, delayTicks);
}
} catch (IllegalArgumentException e) {
getLogger().warning("Ungültiges Instrument: " + instrument);
}
}
public ConfigManager getConfigManager() { return configManager; }
public DataManager getDataManager() { return dataManager; }
}

View File

@@ -1,16 +1,21 @@
package viper;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Door;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.Openable;
import org.bukkit.block.data.Lightable;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
@@ -24,192 +29,410 @@ public class ButtonListener implements Listener {
private final DataManager dataManager;
public ButtonListener(ButtonControl plugin, ConfigManager configManager, DataManager dataManager) {
this.plugin = plugin;
this.plugin = plugin;
this.configManager = configManager;
this.dataManager = dataManager;
this.dataManager = dataManager;
}
// -----------------------------------------------------------------------
// Interact Benutzung + Verbinden
// -----------------------------------------------------------------------
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
String playerUUID = event.getPlayer().getUniqueId().toString();
// Doppelt-Feuern bei Items verhindern (Haupt- und Nebenhand)
if (event.getHand() != EquipmentSlot.HAND) return;
ItemStack item = event.getItem();
Block block = event.getClickedBlock();
Player player = event.getPlayer();
UUID playerUUID = player.getUniqueId();
ItemStack item = event.getItem();
Block block = event.getClickedBlock();
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && block != null &&
(block.getType() == Material.STONE_BUTTON || block.getType() == Material.DAYLIGHT_DETECTOR)) {
String blockLocation = block.getWorld().getName() + "," + block.getX() + "," + block.getY() + "," + block.getZ();
String buttonId = dataManager.getButtonIdForPlacedController(playerUUID, blockLocation);
// ── 1. Bereits platzierter Controller ──────────────────────────────
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && block != null) {
String blockLocation = plugin.toLoc(block);
String buttonId = dataManager.getButtonIdForLocation(blockLocation);
if (buttonId != null) {
event.setCancelled(true);
List<String> connectedBlocks = dataManager.getConnectedBlocks(playerUUID, buttonId);
if (connectedBlocks != null && !connectedBlocks.isEmpty()) {
boolean anyDoorOpened = false;
boolean anyDoorClosed = false;
boolean anyLampOn = false;
boolean anyLampOff = false;
for (String loc : connectedBlocks) {
String[] parts = loc.split(",");
Location location = new Location(plugin.getServer().getWorld(parts[0]),
Integer.parseInt(parts[1]),
Integer.parseInt(parts[2]),
Integer.parseInt(parts[3]));
Block targetBlock = location.getBlock();
if (isDoor(targetBlock.getType())) {
Door door = (Door) targetBlock.getBlockData();
boolean wasOpen = door.isOpen();
door.setOpen(!wasOpen);
targetBlock.setBlockData(door);
if (!wasOpen) {
anyDoorOpened = true;
} else {
anyDoorClosed = true;
}
} else if (targetBlock.getType() == Material.REDSTONE_LAMP) {
Lightable lamp = (Lightable) targetBlock.getBlockData();
boolean wasLit = lamp.isLit();
lamp.setLit(!wasLit);
targetBlock.setBlockData(lamp);
if (!wasLit) {
anyLampOn = true;
} else {
anyLampOff = true;
}
}
}
if (anyDoorOpened) {
event.getPlayer().sendMessage(configManager.getMessage("doors-open"));
}
if (anyDoorClosed) {
event.getPlayer().sendMessage(configManager.getMessage("doors-closed"));
}
if (anyLampOn) {
event.getPlayer().sendMessage(configManager.getMessage("lamps-on"));
}
if (anyLampOff) {
event.getPlayer().sendMessage(configManager.getMessage("lamps-off"));
}
} else {
event.getPlayer().sendMessage(configManager.getMessage("no-blocks-connected"));
// Admin-Bypass
if (!dataManager.canAccess(buttonId, playerUUID)
&& !player.hasPermission("buttoncontrol.admin")) {
player.sendMessage(configManager.getMessage("keine-berechtigung-controller"));
event.setCancelled(true);
return;
}
// Tripwire & Teppich → MotionSensorGUI öffnen
if (block.getType() == Material.TRIPWIRE_HOOK
|| block.getType().name().endsWith("_CARPET")) {
event.setCancelled(true);
new MotionSensorGUI(plugin, player, blockLocation, buttonId).open();
return;
}
// Schild → nur mit Shift+Klick Blöcke toggeln,
// normaler Klick öffnet den Schildeditor (Vanilla)
if (block.getType().name().endsWith("_SIGN")
|| block.getType().name().endsWith("_BUTTON")
|| block.getType() == Material.DAYLIGHT_DETECTOR) {
if (player.isSneaking()) {
// Shift+Klick → Vanilla-Verhalten zulassen (Schildeditor etc.)
return;
}
event.setCancelled(true);
List<String> connectedBlocks = dataManager.getConnectedBlocks(buttonId);
if (connectedBlocks != null && !connectedBlocks.isEmpty()) {
toggleConnectedBlocks(player, playerUUID, connectedBlocks);
} else {
player.sendMessage(configManager.getMessage("keine-bloecke-verbunden"));
}
}
return;
}
return;
}
if (item == null || (!item.getType().equals(Material.STONE_BUTTON) && !item.getType().equals(Material.DAYLIGHT_DETECTOR))) {
return;
}
// ── 2. Verbinden mit Controller-Item in der Hand ───────────────────
if (item == null || !item.hasItemMeta()) return;
String displayName = item.getItemMeta().getDisplayName();
if (!displayName.contains("§6Steuer-")) return;
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || block == null) return;
if (!item.hasItemMeta() || !item.getItemMeta().getDisplayName().contains("§6Steuer-")) {
return;
}
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || block == null) {
return;
}
if (isDoor(block.getType()) || block.getType() == Material.REDSTONE_LAMP) {
if (isInteractableTarget(block.getType())) {
event.setCancelled(true);
String buttonId = item.getItemMeta().hasLore() ? item.getItemMeta().getLore().get(0) : UUID.randomUUID().toString();
List<String> connectedBlocks = dataManager.getConnectedBlocks(playerUUID, buttonId);
if (connectedBlocks == null) {
connectedBlocks = new ArrayList<>();
}
// Doppeltür: immer untersten Block speichern
Block targetBlock = getBottomDoorBlock(block);
int maxDoors = configManager.getMaxDoors();
int maxLamps = configManager.getMaxLamps();
int doorCount = (int) connectedBlocks.stream().filter(loc -> isDoor(getMaterialFromLocation(loc))).count();
int lampCount = (int) connectedBlocks.stream().filter(loc -> getMaterialFromLocation(loc) == Material.REDSTONE_LAMP).count();
if (isDoor(block.getType()) && doorCount >= maxDoors) {
event.getPlayer().sendMessage(configManager.getMessage("max-doors-reached"));
return;
}
if (block.getType() == Material.REDSTONE_LAMP && lampCount >= maxLamps) {
event.getPlayer().sendMessage(configManager.getMessage("max-lamps-reached"));
return;
}
String blockLocation = block.getWorld().getName() + "," + block.getX() + "," + block.getY() + "," + block.getZ();
if (!connectedBlocks.contains(blockLocation)) {
connectedBlocks.add(blockLocation);
dataManager.setConnectedBlocks(playerUUID, buttonId, connectedBlocks);
ItemMeta meta = item.getItemMeta();
String buttonId = extractButtonId(meta);
if (buttonId == null) {
buttonId = UUID.randomUUID().toString();
updateButtonLore(item, buttonId);
event.getPlayer().sendMessage(configManager.getMessage("block-connected"));
} else {
event.getPlayer().sendMessage(configManager.getMessage("block-already-connected"));
}
List<String> connectedBlocks = dataManager.getConnectedBlocks(buttonId);
if (connectedBlocks == null) connectedBlocks = new ArrayList<>();
String targetLocStr = plugin.toLoc(targetBlock);
if (connectedBlocks.contains(targetLocStr)) {
player.sendMessage(configManager.getMessage("block-bereits-verbunden"));
return;
}
if (checkLimits(player, targetBlock.getType(), connectedBlocks)) {
connectedBlocks.add(targetLocStr);
dataManager.setConnectedBlocks(playerUUID.toString(), buttonId, connectedBlocks);
player.sendMessage(configManager.getMessage("block-verbunden"));
}
}
}
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
String playerUUID = event.getPlayer().getUniqueId().toString();
ItemStack item = event.getItemInHand();
Block block = event.getBlockPlaced();
if (item == null || (!item.getType().equals(Material.STONE_BUTTON) && !item.getType().equals(Material.DAYLIGHT_DETECTOR))) {
return;
}
if (!item.hasItemMeta() || !item.getItemMeta().getDisplayName().contains("§6Steuer-")) {
return;
}
String buttonId = item.getItemMeta().hasLore() ? item.getItemMeta().getLore().get(0) : UUID.randomUUID().toString();
String blockLocation = block.getWorld().getName() + "," + block.getX() + "," + block.getY() + "," + block.getZ();
dataManager.addPlacedController(playerUUID, blockLocation, buttonId);
event.getPlayer().sendMessage(configManager.getMessage("controller-placed"));
}
// -----------------------------------------------------------------------
// Block-Break: Controller abbauen
// -----------------------------------------------------------------------
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
String playerUUID = event.getPlayer().getUniqueId().toString();
Block block = event.getBlock();
String blockLocation = block.getWorld().getName() + "," + block.getX() + "," + block.getY() + "," + block.getZ();
String buttonId = dataManager.getButtonIdForPlacedController(playerUUID, blockLocation);
Block block = event.getBlock();
String blockLocation = plugin.toLoc(block);
String buttonId = dataManager.getButtonIdForLocation(blockLocation);
if (buttonId != null) {
dataManager.removePlacedController(playerUUID, blockLocation);
dataManager.setConnectedBlocks(playerUUID, buttonId, null);
event.getPlayer().sendMessage(configManager.getMessage("controller-removed"));
if (!dataManager.isOwner(buttonId, event.getPlayer().getUniqueId())
&& !event.getPlayer().hasPermission("buttoncontrol.admin")) {
event.getPlayer().sendMessage(configManager.getMessage("nur-besitzer-abbauen"));
event.setCancelled(true);
return;
}
dataManager.removeController(blockLocation);
event.getPlayer().sendMessage(configManager.getMessage("controller-entfernt"));
}
}
private boolean isDoor(Material material) {
return material.toString().endsWith("_DOOR");
// -----------------------------------------------------------------------
// Block-Break: Verbundener Block abgebaut → Eintrag bereinigen (NEU)
// -----------------------------------------------------------------------
@EventHandler
public void onConnectedBlockBreak(BlockBreakEvent event) {
Block block = event.getBlock();
if (!isInteractableTarget(block.getType())) return;
// Bei Türen normalisieren auf Unterblock
Block bottomBlock = getBottomDoorBlock(block);
String locStr = plugin.toLoc(bottomBlock);
if (dataManager.removeFromAllConnectedBlocks(locStr)) {
event.getPlayer().sendMessage(configManager.getMessage("block-verbindung-entfernt"));
}
}
private Material getMaterialFromLocation(String locString) {
String[] parts = locString.split(",");
if (parts.length != 4) return null;
Location loc = new Location(plugin.getServer().getWorld(parts[0]),
Integer.parseInt(parts[1]),
Integer.parseInt(parts[2]),
Integer.parseInt(parts[3]));
return loc.getBlock().getType();
// -----------------------------------------------------------------------
// Controller platzieren
// -----------------------------------------------------------------------
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
ItemStack item = event.getItemInHand();
if (item == null || !item.hasItemMeta()) return;
if (!item.getItemMeta().getDisplayName().contains("§6Steuer-")) return;
Block block = event.getBlockPlaced();
String buttonId = extractButtonId(item.getItemMeta());
if (buttonId == null) {
buttonId = UUID.randomUUID().toString();
updateButtonLore(item, buttonId);
}
dataManager.registerController(
plugin.toLoc(block), event.getPlayer().getUniqueId(), buttonId);
event.getPlayer().sendMessage(configManager.getMessage("controller-platziert"));
}
// -----------------------------------------------------------------------
// Toggle-Logik
// -----------------------------------------------------------------------
private void toggleConnectedBlocks(Player player, UUID playerUUID, List<String> connectedBlocks) {
boolean anyDoorOpened = false, anyDoorClosed = false;
boolean anyGateOpened = false, anyGateClosed = false;
boolean anyTrapOpened = false, anyTrapClosed = false;
boolean anyIronDoorOpened = false, anyIronDoorClosed = false;
boolean anyIronTrapOpened = false, anyIronTrapClosed = false;
boolean anyLampOn = false, anyLampOff = false;
boolean anyNoteBlockPlayed = false;
boolean anyBellPlayed = false;
boolean soundsEnabled = configManager.getConfig().getBoolean("sounds.enabled", true);
for (String locStr : connectedBlocks) {
Location location = parseLocation(locStr);
if (location == null) continue;
Block targetBlock = location.getBlock();
Material mat = targetBlock.getType();
// ── Eisentür (NEU) ──────────────────────────────────────────────
// Eisentüren implementieren Openable in der Bukkit-API.
// Wir setzen den Zustand direkt kein Redstone-Signal nötig.
if (mat == Material.IRON_DOOR) {
if (targetBlock.getBlockData() instanceof Openable) {
Openable op = (Openable) targetBlock.getBlockData();
boolean wasOpen = op.isOpen();
op.setOpen(!wasOpen);
targetBlock.setBlockData(op);
if (soundsEnabled) {
String soundKey = wasOpen ? "sounds.iron-door-close" : "sounds.iron-door-open";
playConfigSound(location, soundKey,
wasOpen ? "BLOCK_IRON_DOOR_CLOSE" : "BLOCK_IRON_DOOR_OPEN");
}
if (!wasOpen) anyIronDoorOpened = true; else anyIronDoorClosed = true;
}
continue;
}
// ── Eisenfalltür (NEU) ─────────────────────────────────────────
if (mat == Material.IRON_TRAPDOOR) {
if (targetBlock.getBlockData() instanceof Openable) {
Openable op = (Openable) targetBlock.getBlockData();
boolean wasOpen = op.isOpen();
op.setOpen(!wasOpen);
targetBlock.setBlockData(op);
if (soundsEnabled) {
String soundKey = wasOpen ? "sounds.iron-door-close" : "sounds.iron-door-open";
playConfigSound(location, soundKey,
wasOpen ? "BLOCK_IRON_TRAPDOOR_CLOSE" : "BLOCK_IRON_TRAPDOOR_OPEN");
}
if (!wasOpen) anyIronTrapOpened = true; else anyIronTrapClosed = true;
}
continue;
}
// ── Holztür / Zauntore / Falltüren ────────────────────────────
if (isDoor(mat) || isGate(mat) || isTrapdoor(mat)) {
if (targetBlock.getBlockData() instanceof Openable) {
Openable op = (Openable) targetBlock.getBlockData();
boolean wasOpen = op.isOpen();
op.setOpen(!wasOpen);
targetBlock.setBlockData(op);
if (soundsEnabled) {
String soundKey = wasOpen ? "sounds.door-close" : "sounds.door-open";
String fallback = wasOpen ? "BLOCK_WOODEN_DOOR_CLOSE" : "BLOCK_WOODEN_DOOR_OPEN";
playConfigSound(location, soundKey, fallback);
}
if (isDoor(mat)) { if (!wasOpen) anyDoorOpened = true; else anyDoorClosed = true; }
else if (isGate(mat)) { if (!wasOpen) anyGateOpened = true; else anyGateClosed = true; }
else { if (!wasOpen) anyTrapOpened = true; else anyTrapClosed = true; }
}
}
// ── Redstone-Lampe ────────────────────────────────────────────
else if (mat == Material.REDSTONE_LAMP) {
Lightable lamp = (Lightable) targetBlock.getBlockData();
boolean wasLit = lamp.isLit();
lamp.setLit(!wasLit);
targetBlock.setBlockData(lamp);
if (soundsEnabled) {
playConfigSound(location,
wasLit ? "sounds.lamp-off" : "sounds.lamp-on",
"BLOCK_LEVER_CLICK");
}
if (!wasLit) anyLampOn = true; else anyLampOff = true;
}
// ── Notenblock ────────────────────────────────────────────────
else if (mat == Material.NOTE_BLOCK) {
String instrument = dataManager.getPlayerInstrument(playerUUID);
if (instrument == null)
instrument = configManager.getConfig().getString("default-note", "PIANO");
plugin.playDoorbellSound(location, instrument);
anyNoteBlockPlayed = true;
}
// ── Glocke ───────────────────────────────────────────────────
else if (mat == Material.BELL) {
targetBlock.getWorld().playSound(location, Sound.BLOCK_BELL_USE, 3.0f, 1.0f);
anyBellPlayed = true;
}
}
// Feedback-Nachrichten
if (anyDoorOpened) player.sendMessage(configManager.getMessage("tueren-geoeffnet"));
if (anyDoorClosed) player.sendMessage(configManager.getMessage("tueren-geschlossen"));
if (anyIronDoorOpened) player.sendMessage(configManager.getMessage("eisentueren-geoeffnet"));
if (anyIronDoorClosed) player.sendMessage(configManager.getMessage("eisentueren-geschlossen"));
if (anyIronTrapOpened) player.sendMessage(configManager.getMessage("eisenfallturen-geoeffnet"));
if (anyIronTrapClosed) player.sendMessage(configManager.getMessage("eisenfallturen-geschlossen"));
if (anyGateOpened) player.sendMessage(configManager.getMessage("gates-geoeffnet"));
if (anyGateClosed) player.sendMessage(configManager.getMessage("gates-geschlossen"));
if (anyTrapOpened) player.sendMessage(configManager.getMessage("fallturen-geoeffnet"));
if (anyTrapClosed) player.sendMessage(configManager.getMessage("fallturen-geschlossen"));
if (anyLampOn) player.sendMessage(configManager.getMessage("lampen-eingeschaltet"));
if (anyLampOff) player.sendMessage(configManager.getMessage("lampen-ausgeschaltet"));
if (anyNoteBlockPlayed) player.sendMessage(configManager.getMessage("notenblock-ausgeloest"));
if (anyBellPlayed) player.sendMessage(configManager.getMessage("glocke-gelaeutet"));
}
/**
* Spielt einen Sound ab dessen Name aus der config.yml gelesen wird.
* Ist der Key nicht gesetzt oder der Sound-Name ungültig, wird der Fallback verwendet.
*/
private void playConfigSound(Location loc, String configKey, String fallback) {
String soundName = configManager.getConfig().getString(configKey, fallback);
try {
Sound sound = Sound.valueOf(soundName.toUpperCase());
loc.getWorld().playSound(loc, sound, 1.0f, 1.0f);
} catch (IllegalArgumentException e) {
try {
Sound sound = Sound.valueOf(fallback.toUpperCase());
loc.getWorld().playSound(loc, sound, 1.0f, 1.0f);
} catch (IllegalArgumentException ignored) { }
}
}
// -----------------------------------------------------------------------
// Limits
// -----------------------------------------------------------------------
private boolean checkLimits(Player player, Material type, List<String> connected) {
if (type == Material.IRON_DOOR) {
if (connected.stream().filter(l -> getMaterialAt(l) == Material.IRON_DOOR).count()
>= configManager.getMaxDoors()) {
player.sendMessage(configManager.getMessage("max-tueren-erreicht")); return false;
}
} else if (type == Material.IRON_TRAPDOOR) {
if (connected.stream().filter(l -> getMaterialAt(l) == Material.IRON_TRAPDOOR).count()
>= configManager.getMaxTrapdoors()) {
player.sendMessage(configManager.getMessage("max-fallturen-erreicht")); return false;
}
} else if (isDoor(type)) {
if (connected.stream().filter(l -> isDoor(getMaterialAt(l))).count()
>= configManager.getMaxDoors()) {
player.sendMessage(configManager.getMessage("max-tueren-erreicht")); return false;
}
} else if (isGate(type)) {
if (connected.stream().filter(l -> isGate(getMaterialAt(l))).count()
>= configManager.getMaxGates()) {
player.sendMessage(configManager.getMessage("max-gates-erreicht")); return false;
}
} else if (isTrapdoor(type)) {
if (connected.stream().filter(l -> isTrapdoor(getMaterialAt(l))).count()
>= configManager.getMaxTrapdoors()) {
player.sendMessage(configManager.getMessage("max-fallturen-erreicht")); return false;
}
} else if (type == Material.REDSTONE_LAMP) {
if (connected.stream().filter(l -> getMaterialAt(l) == Material.REDSTONE_LAMP).count()
>= configManager.getMaxLamps()) {
player.sendMessage(configManager.getMessage("max-lampen-erreicht")); return false;
}
} else if (type == Material.NOTE_BLOCK) {
if (connected.stream().filter(l -> getMaterialAt(l) == Material.NOTE_BLOCK).count()
>= configManager.getMaxNoteBlocks()) {
player.sendMessage(configManager.getMessage("max-notenbloecke-erreicht")); return false;
}
} else if (type == Material.BELL) {
if (connected.stream().filter(l -> getMaterialAt(l) == Material.BELL).count()
>= configManager.getMaxBells()) {
player.sendMessage(configManager.getMessage("max-glocken-erreicht")); return false;
}
}
return true;
}
// -----------------------------------------------------------------------
// Hilfsmethoden
// -----------------------------------------------------------------------
/** Gibt bei zweiteiligen Türen immer den untersten Block zurück. */
private Block getBottomDoorBlock(Block block) {
Material mat = block.getType();
if (!isDoor(mat) && mat != Material.IRON_DOOR) return block;
if (block.getBlockData() instanceof Bisected) {
Bisected b = (Bisected) block.getBlockData();
if (b.getHalf() == Bisected.Half.TOP) return block.getRelative(0, -1, 0);
}
return block;
}
private String extractButtonId(ItemMeta meta) {
if (meta == null || !meta.hasLore() || meta.getLore().isEmpty()) return null;
String first = meta.getLore().get(0);
return first.startsWith("§8ID: ") ? first.replace("§8ID: ", "") : null;
}
private void updateButtonLore(ItemStack item, String buttonId) {
ItemMeta meta = item.getItemMeta();
if (meta != null) {
List<String> lore = meta.hasLore() ? meta.getLore() : new ArrayList<>();
if (!lore.contains(buttonId)) {
lore.add(buttonId);
meta.setLore(lore);
item.setItemMeta(meta);
}
List<String> lore = new ArrayList<>();
lore.add("§8ID: " + buttonId);
lore.add("§7Ein universeller Controller für");
meta.setLore(lore);
item.setItemMeta(meta);
}
}
private boolean isButton(Material m) { return m.name().endsWith("_BUTTON"); }
private boolean isDoor(Material m) { return m.name().endsWith("_DOOR") && m != Material.IRON_DOOR; }
private boolean isGate(Material m) { return m.name().endsWith("_FENCE_GATE"); }
private boolean isTrapdoor(Material m) { return m.name().endsWith("_TRAPDOOR") && m != Material.IRON_TRAPDOOR; }
private boolean isInteractableTarget(Material m) {
return isDoor(m) || isGate(m) || isTrapdoor(m)
|| m == Material.IRON_DOOR || m == Material.IRON_TRAPDOOR
|| m == Material.REDSTONE_LAMP || m == Material.NOTE_BLOCK || m == Material.BELL;
}
private Material getMaterialAt(String locString) {
Location l = parseLocation(locString);
return l != null ? l.getBlock().getType() : Material.AIR;
}
private Location parseLocation(String s) {
String[] p = s.split(",");
if (p.length != 4) return null;
try {
return new Location(Bukkit.getWorld(p[0]),
Integer.parseInt(p[1]), Integer.parseInt(p[2]), Integer.parseInt(p[3]));
} catch (Exception e) { return null; }
}
}

View File

@@ -0,0 +1,53 @@
package viper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ButtonTabCompleter implements TabCompleter {
private final List<String> commands = Arrays.asList(
"info", "reload", "note", "list", "rename", "schedule",
"trust", "untrust", "public", "private"
);
private final List<String> instruments = Arrays.asList(
"PIANO", "BASS_DRUM", "SNARE_DRUM", "STICKS", "BASS_GUITAR",
"FLUTE", "BELL", "CHIME", "GUITAR", "XYLOPHONE",
"IRON_XYLOPHONE", "COW_BELL", "DIDGERIDOO", "BIT", "BANJO", "PLING"
);
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (!(sender instanceof Player)) return Collections.emptyList();
List<String> completions = new ArrayList<>();
if (args.length == 1) {
StringUtil.copyPartialMatches(args[0], commands, completions);
} else if (args.length == 2) {
switch (args[0].toLowerCase()) {
case "note":
StringUtil.copyPartialMatches(args[1], instruments, completions);
break;
case "trust":
case "untrust":
return null; // Bukkit schlägt automatisch Online-Spieler vor
case "rename":
completions.add("<Name>");
break;
}
}
Collections.sort(completions);
return completions;
}
}

View File

@@ -3,8 +3,8 @@ package viper;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class ConfigManager {
private final ButtonControl plugin;
@@ -21,45 +21,176 @@ public class ConfigManager {
private void loadConfig() {
configFile = new File(plugin.getDataFolder(), "config.yml");
if (!configFile.exists()) {
plugin.saveResource("config.yml", false);
}
if (!configFile.exists()) plugin.saveResource("config.yml", false);
config = YamlConfiguration.loadConfiguration(configFile);
mergeDefaults(config, "config.yml", configFile);
setConfigDefaults();
}
private void loadLang() {
langFile = new File(plugin.getDataFolder(), "lang.yml");
if (!langFile.exists()) {
plugin.saveResource("lang.yml", false);
}
if (!langFile.exists()) plugin.saveResource("lang.yml", false);
lang = YamlConfiguration.loadConfiguration(langFile);
mergeDefaults(lang, "lang.yml", langFile);
setLangDefaults();
}
public int getMaxDoors() {
return config.getInt("max-doors", 20);
/**
* FIX: Null-Check VOR dem try-Block verhindert NPE wenn Ressource nicht im JAR.
*/
private void mergeDefaults(FileConfiguration file, String resourceName, File targetFile) {
InputStream is = plugin.getResource(resourceName);
if (is == null) {
plugin.getLogger().warning(resourceName + " nicht im JAR gefunden.");
return;
}
try (InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
FileConfiguration defaults = YamlConfiguration.loadConfiguration(reader);
boolean changed = false;
for (String key : defaults.getKeys(true)) {
if (!file.contains(key)) {
file.set(key, defaults.get(key));
changed = true;
}
}
if (changed) {
file.save(targetFile);
}
} catch (IOException e) {
plugin.getLogger().severe("Merge-Fehler " + resourceName + ": " + e.getMessage());
}
}
public int getMaxLamps() {
return config.getInt("max-lamps", 50);
private void setConfigDefaults() {
def(config, "max-doors", 20);
def(config, "max-lamps", 50);
def(config, "max-noteblocks", 10);
def(config, "max-gates", 20);
def(config, "max-trapdoors", 20);
def(config, "max-bells", 5);
def(config, "default-note", "PIANO");
def(config, "double-note-enabled", true);
def(config, "double-note-delay-ms", 1000);
def(config, "motion-detection-radius", 5.0);
def(config, "motion-close-delay-ms", 5000);
def(config, "motion-trigger-cooldown-ms", 2000);
// Optionales MySQL-Backend
def(config, "mysql.enabled", false);
def(config, "mysql.host", "127.0.0.1");
def(config, "mysql.port", 3306);
def(config, "mysql.database", "buttoncontrol");
def(config, "mysql.user", "root");
def(config, "mysql.password", "");
// Controller-Namensanzeige beim Anschauen
def(config, "controller-name-display.enabled", true);
def(config, "controller-name-display.max-look-distance", 8);
def(config, "controller-name-display.format", "§6Controller: §f%s");
// Sounds (NEU)
def(config, "sounds.enabled", true);
def(config, "sounds.door-open", "BLOCK_WOODEN_DOOR_OPEN");
def(config, "sounds.door-close", "BLOCK_WOODEN_DOOR_CLOSE");
def(config, "sounds.iron-door-open", "BLOCK_IRON_DOOR_OPEN");
def(config, "sounds.iron-door-close", "BLOCK_IRON_DOOR_CLOSE");
def(config, "sounds.lamp-on", "BLOCK_LEVER_CLICK");
def(config, "sounds.lamp-off", "BLOCK_LEVER_CLICK");
saveConfig();
}
private void setLangDefaults() {
// Türen (Holz)
def(lang, "tueren-geoeffnet", "§aTüren wurden geöffnet.");
def(lang, "tueren-geschlossen", "§cTüren wurden geschlossen.");
def(lang, "max-tueren-erreicht", "§cMaximale Anzahl an Türen erreicht.");
// Eisentüren (NEU)
def(lang, "eisentueren-geoeffnet", "§aEisentüren wurden geöffnet.");
def(lang, "eisentueren-geschlossen", "§cEisentüren wurden geschlossen.");
def(lang, "eisenfallturen-geoeffnet", "§aEisen-Falltüren wurden geöffnet.");
def(lang, "eisenfallturen-geschlossen", "§cEisen-Falltüren wurden geschlossen.");
// Zauntore
def(lang, "gates-geoeffnet", "§aZauntore wurden geöffnet.");
def(lang, "gates-geschlossen", "§cZauntore wurden geschlossen.");
def(lang, "max-gates-erreicht", "§cMaximale Anzahl an Zauntoren erreicht.");
// Falltüren
def(lang, "fallturen-geoeffnet", "§aFalltüren wurden geöffnet.");
def(lang, "fallturen-geschlossen", "§cFalltüren wurden geschlossen.");
def(lang, "max-fallturen-erreicht", "§cMaximale Anzahl an Falltüren erreicht.");
// Lampen
def(lang, "lampen-eingeschaltet", "§aLampen wurden eingeschaltet.");
def(lang, "lampen-ausgeschaltet", "§cLampen wurden ausgeschaltet.");
def(lang, "max-lampen-erreicht", "§cMaximale Anzahl an Lampen erreicht.");
// Glocken
def(lang, "glocke-gelaeutet", "§aGlocke wurde geläutet.");
def(lang, "max-glocken-erreicht", "§cMaximale Anzahl an Glocken erreicht.");
// Notenblöcke
def(lang, "notenblock-ausgeloest", "§aNotenblock-Klingel wurde ausgelöst.");
def(lang, "instrument-gesetzt", "§aDein Instrument wurde auf %s gesetzt.");
def(lang, "ungueltiges-instrument", "§cUngültiges Instrument! /bc note <Instrument>");
def(lang, "max-notenbloecke-erreicht", "§cMaximale Anzahl an Notenblöcken erreicht.");
// Kolben (vorbereitet)
def(lang, "kolben-ausgefahren", "§6[ButtonControl] §7Kolben wurden ausgefahren.");
def(lang, "kolben-eingefahren", "§6[ButtonControl] §7Kolben wurden eingezogen.");
def(lang, "max-kolben-erreicht", "§6[ButtonControl] §7Maximale Anzahl an Kolben erreicht.");
// Controller
def(lang, "block-verbunden", "§aBlock verbunden.");
def(lang, "block-bereits-verbunden", "§cBlock ist bereits verbunden.");
def(lang, "block-verbindung-entfernt", "§7Verbindung zu abgebautem Block automatisch entfernt.");
def(lang, "keine-bloecke-verbunden", "§cKeine Blöcke sind verbunden.");
def(lang, "bloecke-umgeschaltet", "§eBlöcke wurden umgeschaltet.");
def(lang, "controller-platziert", "§aController platziert.");
def(lang, "controller-entfernt", "§cController entfernt.");
def(lang, "controller-umbenannt", "§aController umbenannt zu: §f%s"); // NEU
// Trust & Berechtigungen
def(lang, "keine-berechtigung", "§cDu hast keine Berechtigung!");
def(lang, "keine-berechtigung-controller", "§cDu darfst diesen Controller nicht benutzen!");
def(lang, "nur-besitzer-abbauen", "§cNur der Besitzer kann diesen Controller verwalten!");
def(lang, "spieler-nicht-gefunden", "§cSpieler nicht gefunden.");
def(lang, "status-geandert", "§6[BC] §7Controller ist nun %s§7.");
def(lang, "trust-hinzugefuegt", "§a%s darf diesen Controller nun benutzen.");
def(lang, "trust-entfernt", "§c%s wurde das Vertrauen entzogen.");
def(lang, "kein-controller-im-blick", "§cBitte sieh einen Controller direkt an!");
// System
// FIX: Korrekte Key-Bezeichnung (war fälschlich "konfiguration-reloaded")
def(lang, "konfiguration-neugeladen", "§aKonfiguration erfolgreich neu geladen!");
saveLang();
}
private void def(FileConfiguration cfg, String key, Object value) {
if (!cfg.contains(key)) cfg.set(key, value);
}
public void reloadConfig() {
config = YamlConfiguration.loadConfiguration(configFile);
lang = YamlConfiguration.loadConfiguration(langFile);
mergeDefaults(config, "config.yml", configFile);
mergeDefaults(lang, "lang.yml", langFile);
setConfigDefaults();
setLangDefaults();
}
public FileConfiguration getConfig() { return config; }
public int getMaxDoors() { return config.getInt("max-doors", 20); }
public int getMaxLamps() { return config.getInt("max-lamps", 50); }
public int getMaxNoteBlocks() { return config.getInt("max-noteblocks", 10); }
public int getMaxGates() { return config.getInt("max-gates", 20); }
public int getMaxTrapdoors() { return config.getInt("max-trapdoors", 20); }
public int getMaxBells() { return config.getInt("max-bells", 5); }
public String getMessage(String key) {
return lang.getString(key, "Nachricht nicht gefunden: " + key);
return lang.getString(key, "§cNachricht fehlt: " + key);
}
public void saveConfig() {
try {
config.save(configFile);
} catch (IOException e) {
plugin.getLogger().severe("Konnte config.yml nicht speichern: " + e.getMessage());
}
try { config.save(configFile); }
catch (IOException e) { plugin.getLogger().severe("config.yml Fehler: " + e.getMessage()); }
}
public void saveLang() {
try {
lang.save(langFile);
} catch (IOException e) {
plugin.getLogger().severe("Konnte lang.yml nicht speichern: " + e.getMessage());
}
try { lang.save(langFile); }
catch (IOException e) { plugin.getLogger().severe("lang.yml Fehler: " + e.getMessage()); }
}
}

View File

@@ -4,115 +4,374 @@ import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class DataManager {
private final ButtonControl plugin;
private FileConfiguration data;
private File dataFile;
private MySQLStorage mySQLStorage;
public DataManager(ButtonControl plugin) {
this.plugin = plugin;
loadData();
initializeStorage();
}
private void initializeStorage() {
mySQLStorage = new MySQLStorage(plugin);
if (!mySQLStorage.initialize()) {
mySQLStorage = null;
}
}
private void loadData() {
dataFile = new File(plugin.getDataFolder(), "data.yml");
if (!dataFile.exists()) {
plugin.saveResource("data.yml", false);
}
if (!dataFile.exists()) plugin.saveResource("data.yml", false);
data = YamlConfiguration.loadConfiguration(dataFile);
}
// --- Spielerbasierte Methoden ---
public List<String> getConnectedBlocks(String playerUUID, String buttonId) {
return data.getStringList("players." + playerUUID + ".buttons." + buttonId);
public void reloadData() {
data = YamlConfiguration.loadConfiguration(dataFile);
if (mySQLStorage != null) {
mySQLStorage.close();
}
initializeStorage();
}
public void shutdown() {
if (mySQLStorage != null) {
mySQLStorage.close();
}
}
// -----------------------------------------------------------------------
// Zugriff & Berechtigungen
// -----------------------------------------------------------------------
public boolean canAccess(String buttonId, UUID playerUUID) {
if (mySQLStorage != null) return mySQLStorage.canAccess(buttonId, playerUUID);
if (isPublic(buttonId)) return true;
if (isOwner(buttonId, playerUUID)) return true;
return data.getStringList("trust." + buttonId).contains(playerUUID.toString());
}
public boolean isOwner(String buttonId, UUID playerUUID) {
if (mySQLStorage != null) return mySQLStorage.isOwner(buttonId, playerUUID);
return data.contains("players." + playerUUID + ".buttons." + buttonId)
|| data.contains("players." + playerUUID + ".placed-controllers");
// Zweite Bedingung: prüft ob irgendein placed-controller dieser UUID die buttonId enthält
}
// -----------------------------------------------------------------------
// Controller-Verwaltung
// -----------------------------------------------------------------------
public String getButtonIdForLocation(String location) {
if (mySQLStorage != null) return mySQLStorage.getButtonIdForLocation(location);
return getButtonIdForPlacedController(location);
}
public void registerController(String location, UUID ownerUUID, String buttonId) {
if (mySQLStorage != null) {
mySQLStorage.registerController(location, ownerUUID, buttonId);
return;
}
data.set("players." + ownerUUID + ".placed-controllers." + location, buttonId);
// Leere buttons-Liste anlegen damit isOwner() sofort greift
if (!data.contains("players." + ownerUUID + ".buttons." + buttonId)) {
data.set("players." + ownerUUID + ".buttons." + buttonId, new ArrayList<>());
}
saveData();
}
public void removeController(String location) {
if (mySQLStorage != null) {
mySQLStorage.removeController(location);
return;
}
// buttonId vor dem Löschen des Location-Eintrags ermitteln
String buttonId = getButtonIdForPlacedController(location);
String ownerUUID = null;
if (data.getConfigurationSection("players") != null) {
for (String uuid : data.getConfigurationSection("players").getKeys(false)) {
String path = "players." + uuid + ".placed-controllers." + location;
if (data.contains(path)) {
data.set(path, null);
ownerUUID = uuid;
}
}
}
// Alle zugehörigen Daten (Name, Status, Trust, Zeitplan, Verbindungen) bereinigen
if (buttonId != null) {
data.set("names." + buttonId, null);
data.set("public-status." + buttonId, null);
data.set("trust." + buttonId, null);
data.set("schedules." + buttonId, null);
if (ownerUUID != null) {
data.set("players." + ownerUUID + ".buttons." + buttonId, null);
}
}
removeMotionSensorSettings(location);
saveData();
}
public String getButtonIdForPlacedController(String location) {
if (mySQLStorage != null) return mySQLStorage.getButtonIdForPlacedController(location);
if (data.getConfigurationSection("players") == null) return null;
for (String uuid : data.getConfigurationSection("players").getKeys(false)) {
String val = data.getString("players." + uuid + ".placed-controllers." + location);
if (val != null) return val;
}
return null;
}
public List<String> getAllPlacedControllers() {
if (mySQLStorage != null) return mySQLStorage.getAllPlacedControllers();
List<String> result = new ArrayList<>();
if (data.getConfigurationSection("players") == null) return result;
for (String uuid : data.getConfigurationSection("players").getKeys(false)) {
var sec = data.getConfigurationSection("players." + uuid + ".placed-controllers");
if (sec != null) result.addAll(sec.getKeys(false));
}
return result;
}
// -----------------------------------------------------------------------
// Verbundene Blöcke
// -----------------------------------------------------------------------
public void setConnectedBlocks(String playerUUID, String buttonId, List<String> blocks) {
if (mySQLStorage != null) {
mySQLStorage.setConnectedBlocks(playerUUID, buttonId, blocks);
return;
}
data.set("players." + playerUUID + ".buttons." + buttonId, blocks);
saveData();
}
public void addPlacedController(String playerUUID, String location, String buttonId) {
data.set("players." + playerUUID + ".placed-controllers." + location, buttonId);
saveData();
}
public String getButtonIdForPlacedController(String playerUUID, String location) {
return data.getString("players." + playerUUID + ".placed-controllers." + location);
}
public void removePlacedController(String playerUUID, String location) {
data.set("players." + playerUUID + ".placed-controllers." + location, null);
saveData();
}
public List<String> getAllPlacedControllers(String playerUUID) {
if (data.getConfigurationSection("players." + playerUUID + ".placed-controllers") == null) {
return new ArrayList<>();
}
Set<String> keys = data.getConfigurationSection("players." + playerUUID + ".placed-controllers").getKeys(false);
return new ArrayList<>(keys);
}
// --- Neue globale Methoden für Tageslichtsensoren etc. ---
/**
* Gibt alle Controller-Orte aller Spieler zurück (global).
* Nützlich für Tageslichtsensoren.
*/
public List<String> getAllPlacedControllers() {
List<String> allControllers = new ArrayList<>();
if (data.getConfigurationSection("players") == null) {
return allControllers;
}
Set<String> players = data.getConfigurationSection("players").getKeys(false);
for (String playerUUID : players) {
allControllers.addAll(getAllPlacedControllers(playerUUID));
}
return allControllers;
}
/**
* Holt die Button-ID für einen platzierten Controller an einem Ort, ohne Spieler-UUID (global).
* Da Controller pro Spieler gespeichert sind, suchen wir alle Spieler ab.
*/
public String getButtonIdForPlacedController(String location) {
if (data.getConfigurationSection("players") == null) return null;
Set<String> players = data.getConfigurationSection("players").getKeys(false);
for (String playerUUID : players) {
String buttonId = getButtonIdForPlacedController(playerUUID, location);
if (buttonId != null) return buttonId;
}
return null;
}
/**
* Holt die verbundenen Blöcke für eine Button-ID (global).
* Da die verbundenen Blöcke pro Spieler gespeichert sind, suchen wir alle Spieler ab.
*/
public List<String> getConnectedBlocks(String buttonId) {
if (data.getConfigurationSection("players") == null) return null;
Set<String> players = data.getConfigurationSection("players").getKeys(false);
for (String playerUUID : players) {
List<String> connected = getConnectedBlocks(playerUUID, buttonId);
if (connected != null && !connected.isEmpty()) {
return connected;
if (mySQLStorage != null) return mySQLStorage.getConnectedBlocks(buttonId);
if (data.getConfigurationSection("players") == null) return new ArrayList<>();
for (String uuid : data.getConfigurationSection("players").getKeys(false)) {
String path = "players." + uuid + ".buttons." + buttonId;
if (data.contains(path)) return data.getStringList(path);
}
return new ArrayList<>();
}
/**
* Entfernt eine Block-Location aus ALLEN Verbindungslisten aller Controller.
* Wird aufgerufen wenn ein verbundener Block abgebaut wird.
*/
public boolean removeFromAllConnectedBlocks(String locStr) {
if (mySQLStorage != null) return mySQLStorage.removeFromAllConnectedBlocks(locStr);
if (data.getConfigurationSection("players") == null) return false;
boolean changed = false;
for (String uuid : data.getConfigurationSection("players").getKeys(false)) {
var buttonsSection = data.getConfigurationSection("players." + uuid + ".buttons");
if (buttonsSection == null) continue;
for (String buttonId : buttonsSection.getKeys(false)) {
String path = "players." + uuid + ".buttons." + buttonId;
List<String> blocks = data.getStringList(path);
if (blocks.remove(locStr)) {
data.set(path, blocks);
changed = true;
}
}
}
return null;
if (changed) saveData();
return changed;
}
public void saveData() {
try {
data.save(dataFile);
} catch (IOException e) {
plugin.getLogger().severe("Konnte data.yml nicht speichern: " + e.getMessage());
// -----------------------------------------------------------------------
// Controller-Name (NEU)
// -----------------------------------------------------------------------
public void setControllerName(String buttonId, String name) {
if (mySQLStorage != null) {
mySQLStorage.setControllerName(buttonId, name);
return;
}
data.set("names." + buttonId, name);
saveData();
}
public String getControllerName(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.getControllerName(buttonId);
return data.getString("names." + buttonId);
}
// -----------------------------------------------------------------------
// Zeitplan (NEU)
// -----------------------------------------------------------------------
public void setScheduleOpenTime(String buttonId, long ticks) {
if (mySQLStorage != null) {
mySQLStorage.setScheduleOpenTime(buttonId, ticks);
return;
}
data.set("schedules." + buttonId + ".open-time", ticks);
saveData();
}
public long getScheduleOpenTime(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.getScheduleOpenTime(buttonId);
return data.getLong("schedules." + buttonId + ".open-time", -1);
}
public void setScheduleCloseTime(String buttonId, long ticks) {
if (mySQLStorage != null) {
mySQLStorage.setScheduleCloseTime(buttonId, ticks);
return;
}
data.set("schedules." + buttonId + ".close-time", ticks);
saveData();
}
public long getScheduleCloseTime(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.getScheduleCloseTime(buttonId);
return data.getLong("schedules." + buttonId + ".close-time", -1);
}
/** Entfernt den kompletten Zeitplan für einen Controller. */
public void clearSchedule(String buttonId) {
if (mySQLStorage != null) {
mySQLStorage.clearSchedule(buttonId);
return;
}
data.set("schedules." + buttonId, null);
saveData();
}
// -----------------------------------------------------------------------
// Trust & Public/Private
// -----------------------------------------------------------------------
public void addTrustedPlayer(String buttonId, UUID targetUUID) {
if (mySQLStorage != null) {
mySQLStorage.addTrustedPlayer(buttonId, targetUUID);
return;
}
List<String> trusted = data.getStringList("trust." + buttonId);
if (!trusted.contains(targetUUID.toString())) {
trusted.add(targetUUID.toString());
data.set("trust." + buttonId, trusted);
saveData();
}
}
public void removeTrustedPlayer(String buttonId, UUID targetUUID) {
if (mySQLStorage != null) {
mySQLStorage.removeTrustedPlayer(buttonId, targetUUID);
return;
}
List<String> trusted = data.getStringList("trust." + buttonId);
trusted.remove(targetUUID.toString());
data.set("trust." + buttonId, trusted);
saveData();
}
public void setPublic(String buttonId, boolean isPublic) {
if (mySQLStorage != null) {
mySQLStorage.setPublic(buttonId, isPublic);
return;
}
data.set("public-status." + buttonId, isPublic);
saveData();
}
public boolean isPublic(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.isPublic(buttonId);
return data.getBoolean("public-status." + buttonId, false);
}
// -----------------------------------------------------------------------
// Instrumente
// -----------------------------------------------------------------------
public void setPlayerInstrument(UUID playerUUID, String instrument) {
if (mySQLStorage != null) {
mySQLStorage.setPlayerInstrument(playerUUID, instrument);
return;
}
data.set("players." + playerUUID + ".instrument", instrument);
saveData();
}
public String getPlayerInstrument(UUID playerUUID) {
if (mySQLStorage != null) return mySQLStorage.getPlayerInstrument(playerUUID);
return data.getString("players." + playerUUID + ".instrument");
}
// -----------------------------------------------------------------------
// Motion-Sensor-Einstellungen
// -----------------------------------------------------------------------
public void setMotionSensorRadius(String location, double radius) {
if (mySQLStorage != null) {
mySQLStorage.setMotionSensorRadius(location, radius);
return;
}
data.set("motion-sensors." + location + ".radius", radius);
saveData();
}
public double getMotionSensorRadius(String location) {
if (mySQLStorage != null) return mySQLStorage.getMotionSensorRadius(location);
return data.getDouble("motion-sensors." + location + ".radius", -1);
}
public void setMotionSensorDelay(String location, long delay) {
if (mySQLStorage != null) {
mySQLStorage.setMotionSensorDelay(location, delay);
return;
}
data.set("motion-sensors." + location + ".delay", delay);
saveData();
}
public long getMotionSensorDelay(String location) {
if (mySQLStorage != null) return mySQLStorage.getMotionSensorDelay(location);
return data.getLong("motion-sensors." + location + ".delay", -1);
}
public void removeMotionSensorSettings(String location) {
if (mySQLStorage != null) {
mySQLStorage.removeMotionSensorSettings(location);
return;
}
data.set("motion-sensors." + location, null);
saveData();
}
// -----------------------------------------------------------------------
// Speichern asynchron
// -----------------------------------------------------------------------
/**
* Serialisiert die Daten synchron (thread-safe), schreibt dann asynchron auf Disk.
* Verhindert I/O-Lags auf dem Main-Thread.
*/
public void saveData() {
if (mySQLStorage != null) return;
final String serialized;
try {
serialized = data.saveToString();
} catch (Exception e) {
plugin.getLogger().severe("Serialisierungsfehler data.yml: " + e.getMessage());
return;
}
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
try (FileWriter fw = new FileWriter(dataFile, false)) {
fw.write(serialized);
} catch (IOException e) {
plugin.getLogger().severe("Konnte data.yml nicht speichern: " + e.getMessage());
}
});
}
}

View File

@@ -0,0 +1,14 @@
package viper;
import org.bukkit.plugin.java.JavaPlugin;
// Import aus dem korrekten verschobenen Package:
import org.bstats.bukkit.Metrics;
public class MetricsHandler {
private static final int BSTATS_PLUGIN_ID = 26862;
public static void startMetrics(JavaPlugin plugin) {
new Metrics(plugin, BSTATS_PLUGIN_ID);
}
}

View File

@@ -0,0 +1,153 @@
package viper;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Arrays;
public class MotionSensorGUI implements Listener {
private final ButtonControl plugin;
private final DataManager dataManager;
private final ConfigManager configManager;
private final String blockLocation;
private final String buttonId;
private final Player player;
private final Inventory inv;
public MotionSensorGUI(ButtonControl plugin, Player player, String blockLocation, String buttonId) {
this.plugin = plugin;
this.dataManager = plugin.getDataManager();
this.configManager = plugin.getConfigManager();
this.blockLocation = blockLocation;
this.buttonId = buttonId;
this.player = player;
this.inv = Bukkit.createInventory(player, 27, "Bewegungsmelder Einstellungen");
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
public void open() {
// Aktuelle Werte aus DataManager holen oder Standardwerte aus Config
double radius = dataManager.getMotionSensorRadius(blockLocation);
if (radius == -1) radius = configManager.getConfig().getDouble("motion-detection-radius", 5.0);
long delay = dataManager.getMotionSensorDelay(blockLocation);
if (delay == -1) delay = configManager.getConfig().getLong("motion-close-delay-ms", 5000L);
// Füllitems für leere Slots (graue Glasscheiben)
ItemStack filler = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta fillerMeta = filler.getItemMeta();
fillerMeta.setDisplayName(ChatColor.RESET + "");
filler.setItemMeta(fillerMeta);
for (int i = 0; i < 27; i++) {
inv.setItem(i, filler);
}
// Items für die GUI
ItemStack radiusItem = new ItemStack(Material.COMPASS);
ItemMeta radiusMeta = radiusItem.getItemMeta();
radiusMeta.setDisplayName(ChatColor.GREEN + "Erkennungsradius: " + radius + " Blöcke");
radiusMeta.setLore(Arrays.asList(
ChatColor.GRAY + "Linksklick: +0.5 Blöcke",
ChatColor.GRAY + "Rechtsklick: -0.5 Blöcke"
));
radiusItem.setItemMeta(radiusMeta);
ItemStack delayItem = new ItemStack(Material.CLOCK);
ItemMeta delayMeta = delayItem.getItemMeta();
delayMeta.setDisplayName(ChatColor.GREEN + "Schließverzögerung: " + (delay / 1000.0) + " Sekunden");
delayMeta.setLore(Arrays.asList(
ChatColor.GRAY + "Linksklick: +1 Sekunde",
ChatColor.GRAY + "Rechtsklick: -1 Sekunde"
));
delayItem.setItemMeta(delayMeta);
ItemStack saveItem = new ItemStack(Material.EMERALD);
ItemMeta saveMeta = saveItem.getItemMeta();
saveMeta.setDisplayName(ChatColor.GREEN + "Speichern und Schließen");
saveItem.setItemMeta(saveMeta);
// Items in die GUI setzen
inv.setItem(11, radiusItem);
inv.setItem(15, delayItem);
inv.setItem(22, saveItem);
player.openInventory(inv);
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!event.getInventory().equals(inv) || !event.getWhoClicked().equals(player)) return;
if (event.getCurrentItem() == null) return;
event.setCancelled(true); // Alle Klicks standardmäßig abbrechen
int slot = event.getRawSlot();
ItemStack clicked = event.getCurrentItem();
// Nur Klicks auf Slots 11, 15 und 22 verarbeiten
if (slot != 11 && slot != 15 && slot != 22) return;
double radius = dataManager.getMotionSensorRadius(blockLocation);
if (radius == -1) radius = configManager.getConfig().getDouble("motion-detection-radius", 5.0);
long delay = dataManager.getMotionSensorDelay(blockLocation);
if (delay == -1) delay = configManager.getConfig().getLong("motion-close-delay-ms", 5000L);
if (clicked.getType() == Material.COMPASS && slot == 11) {
if (event.isLeftClick()) {
radius = Math.min(radius + 0.5, 20.0); // Max. Radius: 20 Blöcke
} else if (event.isRightClick()) {
radius = Math.max(radius - 0.5, 0.5); // Min. Radius: 0.5 Blöcke
}
dataManager.setMotionSensorRadius(blockLocation, radius);
ItemMeta meta = clicked.getItemMeta();
meta.setDisplayName(ChatColor.GREEN + "Erkennungsradius: " + radius + " Blöcke");
meta.setLore(Arrays.asList(
ChatColor.GRAY + "Linksklick: +0.5 Blöcke",
ChatColor.GRAY + "Rechtsklick: -0.5 Blöcke"
));
clicked.setItemMeta(meta);
inv.setItem(11, clicked);
} else if (clicked.getType() == Material.CLOCK && slot == 15) {
if (event.isLeftClick()) {
delay = Math.min(delay + 1000, 30000); // Max. Verzögerung: 30 Sekunden
} else if (event.isRightClick()) {
delay = Math.max(delay - 1000, 1000); // Min. Verzögerung: 1 Sekunde
}
dataManager.setMotionSensorDelay(blockLocation, delay);
ItemMeta meta = clicked.getItemMeta();
meta.setDisplayName(ChatColor.GREEN + "Schließverzögerung: " + (delay / 1000.0) + " Sekunden");
meta.setLore(Arrays.asList(
ChatColor.GRAY + "Linksklick: +1 Sekunde",
ChatColor.GRAY + "Rechtsklick: -1 Sekunde"
));
clicked.setItemMeta(meta);
inv.setItem(15, clicked);
} else if (clicked.getType() == Material.EMERALD && slot == 22) {
player.closeInventory();
}
}
@EventHandler
public void onInventoryDrag(InventoryDragEvent event) {
if (!event.getInventory().equals(inv) || !event.getWhoClicked().equals(player)) return;
event.setCancelled(true); // Verhindert Drag-and-Drop
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
if (event.getPlayer().equals(player) && event.getInventory().equals(inv)) {
InventoryClickEvent.getHandlerList().unregister(this);
InventoryDragEvent.getHandlerList().unregister(this);
InventoryCloseEvent.getHandlerList().unregister(this);
}
}
}

View File

@@ -0,0 +1,506 @@
package viper;
import org.bukkit.configuration.file.FileConfiguration;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class MySQLStorage {
private final ButtonControl plugin;
private final String host;
private final int port;
private final String database;
private final String user;
private final String password;
private final boolean enabled;
private Connection connection;
public MySQLStorage(ButtonControl plugin) {
this.plugin = plugin;
FileConfiguration cfg = plugin.getConfigManager().getConfig();
this.enabled = cfg.getBoolean("mysql.enabled", false);
this.host = cfg.getString("mysql.host", "127.0.0.1");
this.port = cfg.getInt("mysql.port", 3306);
this.database = cfg.getString("mysql.database", "buttoncontrol");
this.user = cfg.getString("mysql.user", "root");
this.password = cfg.getString("mysql.password", "");
}
public boolean initialize() {
if (!enabled) return false;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
getConnection();
createTables();
plugin.getLogger().info("MySQL aktiviert.");
return true;
} catch (Exception e) {
plugin.getLogger().warning("MySQL konnte nicht initialisiert werden, verwende data.yml: " + e.getMessage());
return false;
}
}
public void close() {
if (connection != null) {
try {
connection.close();
} catch (SQLException ignored) {
}
connection = null;
}
}
private Connection getConnection() throws SQLException {
if (connection != null && connection.isValid(2)) {
return connection;
}
String url = "jdbc:mysql://" + host + ":" + port + "/" + database
+ "?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf8&serverTimezone=UTC";
connection = DriverManager.getConnection(url, user, password);
return connection;
}
private void createTables() throws SQLException {
try (Statement st = getConnection().createStatement()) {
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_controllers ("
+ "location VARCHAR(128) PRIMARY KEY,"
+ "owner_uuid VARCHAR(36) NOT NULL,"
+ "button_id VARCHAR(64) NOT NULL"
+ ")");
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_button_connections ("
+ "owner_uuid VARCHAR(36) NOT NULL,"
+ "button_id VARCHAR(64) NOT NULL,"
+ "block_location VARCHAR(128) NOT NULL,"
+ "PRIMARY KEY (button_id, block_location)"
+ ")");
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_controller_names ("
+ "button_id VARCHAR(64) PRIMARY KEY,"
+ "name VARCHAR(64)"
+ ")");
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_schedules ("
+ "button_id VARCHAR(64) PRIMARY KEY,"
+ "open_time BIGINT,"
+ "close_time BIGINT"
+ ")");
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_trust ("
+ "button_id VARCHAR(64) NOT NULL,"
+ "target_uuid VARCHAR(36) NOT NULL,"
+ "PRIMARY KEY (button_id, target_uuid)"
+ ")");
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_public_status ("
+ "button_id VARCHAR(64) PRIMARY KEY,"
+ "is_public BOOLEAN NOT NULL"
+ ")");
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_player_settings ("
+ "player_uuid VARCHAR(36) PRIMARY KEY,"
+ "instrument VARCHAR(32)"
+ ")");
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_motion_sensors ("
+ "location VARCHAR(128) PRIMARY KEY,"
+ "radius DOUBLE,"
+ "delay_ms BIGINT"
+ ")");
}
}
public boolean canAccess(String buttonId, UUID playerUUID) {
return isPublic(buttonId) || isOwner(buttonId, playerUUID) || isTrusted(buttonId, playerUUID);
}
public boolean isOwner(String buttonId, UUID playerUUID) {
String uuid = playerUUID.toString();
String q1 = "SELECT 1 FROM bc_controllers WHERE button_id = ? AND owner_uuid = ? LIMIT 1";
String q2 = "SELECT 1 FROM bc_button_connections WHERE button_id = ? AND owner_uuid = ? LIMIT 1";
try (PreparedStatement ps1 = getConnection().prepareStatement(q1);
PreparedStatement ps2 = getConnection().prepareStatement(q2)) {
ps1.setString(1, buttonId);
ps1.setString(2, uuid);
try (ResultSet rs = ps1.executeQuery()) {
if (rs.next()) return true;
}
ps2.setString(1, buttonId);
ps2.setString(2, uuid);
try (ResultSet rs = ps2.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL isOwner Fehler: " + e.getMessage());
return false;
}
}
public String getButtonIdForLocation(String location) {
String q = "SELECT button_id FROM bc_controllers WHERE location = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, location);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString(1) : null;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getButtonIdForLocation Fehler: " + e.getMessage());
return null;
}
}
public void registerController(String location, UUID ownerUUID, String buttonId) {
String q = "INSERT INTO bc_controllers (location, owner_uuid, button_id) VALUES (?, ?, ?)"
+ " ON DUPLICATE KEY UPDATE owner_uuid = VALUES(owner_uuid), button_id = VALUES(button_id)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, location);
ps.setString(2, ownerUUID.toString());
ps.setString(3, buttonId);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL registerController Fehler: " + e.getMessage());
}
}
public void removeController(String location) {
// buttonId vor dem Löschen ermitteln
String buttonId = getButtonIdForLocation(location);
String q = "DELETE FROM bc_controllers WHERE location = ?";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, location);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL removeController Fehler: " + e.getMessage());
}
// Alle zugehörigen Tabellen bereinigen
if (buttonId != null) {
deleteButtonData(buttonId);
}
removeMotionSensorSettings(location);
}
private void deleteButtonData(String buttonId) {
String[] queries = {
"DELETE FROM bc_controller_names WHERE button_id = ?",
"DELETE FROM bc_public_status WHERE button_id = ?",
"DELETE FROM bc_trust WHERE button_id = ?",
"DELETE FROM bc_schedules WHERE button_id = ?",
"DELETE FROM bc_button_connections WHERE button_id = ?"
};
for (String q : queries) {
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL deleteButtonData Fehler: " + e.getMessage());
}
}
}
public String getButtonIdForPlacedController(String location) {
return getButtonIdForLocation(location);
}
public List<String> getAllPlacedControllers() {
List<String> result = new ArrayList<>();
String q = "SELECT location FROM bc_controllers";
try (PreparedStatement ps = getConnection().prepareStatement(q);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) result.add(rs.getString(1));
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getAllPlacedControllers Fehler: " + e.getMessage());
}
return result;
}
public void setConnectedBlocks(String playerUUID, String buttonId, List<String> blocks) {
String del = "DELETE FROM bc_button_connections WHERE owner_uuid = ? AND button_id = ?";
String ins = "INSERT INTO bc_button_connections (owner_uuid, button_id, block_location) VALUES (?, ?, ?)";
try (PreparedStatement psDel = getConnection().prepareStatement(del);
PreparedStatement psIns = getConnection().prepareStatement(ins)) {
psDel.setString(1, playerUUID);
psDel.setString(2, buttonId);
psDel.executeUpdate();
for (String block : blocks) {
psIns.setString(1, playerUUID);
psIns.setString(2, buttonId);
psIns.setString(3, block);
psIns.addBatch();
}
psIns.executeBatch();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setConnectedBlocks Fehler: " + e.getMessage());
}
}
public List<String> getConnectedBlocks(String buttonId) {
List<String> result = new ArrayList<>();
String q = "SELECT block_location FROM bc_button_connections WHERE button_id = ?";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) result.add(rs.getString(1));
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getConnectedBlocks Fehler: " + e.getMessage());
}
return result;
}
public boolean removeFromAllConnectedBlocks(String locStr) {
String q = "DELETE FROM bc_button_connections WHERE block_location = ?";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, locStr);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
plugin.getLogger().warning("MySQL removeFromAllConnectedBlocks Fehler: " + e.getMessage());
return false;
}
}
public void setControllerName(String buttonId, String name) {
String q = "INSERT INTO bc_controller_names (button_id, name) VALUES (?, ?)"
+ " ON DUPLICATE KEY UPDATE name = VALUES(name)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setString(2, name);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setControllerName Fehler: " + e.getMessage());
}
}
public String getControllerName(String buttonId) {
String q = "SELECT name FROM bc_controller_names WHERE button_id = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString(1) : null;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getControllerName Fehler: " + e.getMessage());
return null;
}
}
public void setScheduleOpenTime(String buttonId, long ticks) {
String q = "INSERT INTO bc_schedules (button_id, open_time, close_time) VALUES (?, ?, COALESCE((SELECT close_time FROM bc_schedules WHERE button_id = ?), -1))"
+ " ON DUPLICATE KEY UPDATE open_time = VALUES(open_time)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setLong(2, ticks);
ps.setString(3, buttonId);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setScheduleOpenTime Fehler: " + e.getMessage());
}
}
public long getScheduleOpenTime(String buttonId) {
String q = "SELECT open_time FROM bc_schedules WHERE button_id = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getLong(1) : -1;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getScheduleOpenTime Fehler: " + e.getMessage());
return -1;
}
}
public void setScheduleCloseTime(String buttonId, long ticks) {
String q = "INSERT INTO bc_schedules (button_id, open_time, close_time) VALUES (?, COALESCE((SELECT open_time FROM bc_schedules WHERE button_id = ?), -1), ?)"
+ " ON DUPLICATE KEY UPDATE close_time = VALUES(close_time)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setString(2, buttonId);
ps.setLong(3, ticks);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setScheduleCloseTime Fehler: " + e.getMessage());
}
}
public long getScheduleCloseTime(String buttonId) {
String q = "SELECT close_time FROM bc_schedules WHERE button_id = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getLong(1) : -1;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getScheduleCloseTime Fehler: " + e.getMessage());
return -1;
}
}
public void clearSchedule(String buttonId) {
String q = "DELETE FROM bc_schedules WHERE button_id = ?";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL clearSchedule Fehler: " + e.getMessage());
}
}
public void addTrustedPlayer(String buttonId, UUID targetUUID) {
String q = "INSERT IGNORE INTO bc_trust (button_id, target_uuid) VALUES (?, ?)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setString(2, targetUUID.toString());
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL addTrustedPlayer Fehler: " + e.getMessage());
}
}
public void removeTrustedPlayer(String buttonId, UUID targetUUID) {
String q = "DELETE FROM bc_trust WHERE button_id = ? AND target_uuid = ?";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setString(2, targetUUID.toString());
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL removeTrustedPlayer Fehler: " + e.getMessage());
}
}
public void setPublic(String buttonId, boolean isPublic) {
String q = "INSERT INTO bc_public_status (button_id, is_public) VALUES (?, ?)"
+ " ON DUPLICATE KEY UPDATE is_public = VALUES(is_public)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setBoolean(2, isPublic);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setPublic Fehler: " + e.getMessage());
}
}
public boolean isPublic(String buttonId) {
String q = "SELECT is_public FROM bc_public_status WHERE button_id = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() && rs.getBoolean(1);
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL isPublic Fehler: " + e.getMessage());
return false;
}
}
public void setPlayerInstrument(UUID playerUUID, String instrument) {
String q = "INSERT INTO bc_player_settings (player_uuid, instrument) VALUES (?, ?)"
+ " ON DUPLICATE KEY UPDATE instrument = VALUES(instrument)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, playerUUID.toString());
ps.setString(2, instrument);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setPlayerInstrument Fehler: " + e.getMessage());
}
}
public String getPlayerInstrument(UUID playerUUID) {
String q = "SELECT instrument FROM bc_player_settings WHERE player_uuid = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, playerUUID.toString());
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString(1) : null;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getPlayerInstrument Fehler: " + e.getMessage());
return null;
}
}
public void setMotionSensorRadius(String location, double radius) {
String q = "INSERT INTO bc_motion_sensors (location, radius, delay_ms) VALUES (?, ?, COALESCE((SELECT delay_ms FROM bc_motion_sensors WHERE location = ?), -1))"
+ " ON DUPLICATE KEY UPDATE radius = VALUES(radius)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, location);
ps.setDouble(2, radius);
ps.setString(3, location);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setMotionSensorRadius Fehler: " + e.getMessage());
}
}
public double getMotionSensorRadius(String location) {
String q = "SELECT radius FROM bc_motion_sensors WHERE location = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, location);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getDouble(1) : -1;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getMotionSensorRadius Fehler: " + e.getMessage());
return -1;
}
}
public void setMotionSensorDelay(String location, long delay) {
String q = "INSERT INTO bc_motion_sensors (location, radius, delay_ms) VALUES (?, COALESCE((SELECT radius FROM bc_motion_sensors WHERE location = ?), -1), ?)"
+ " ON DUPLICATE KEY UPDATE delay_ms = VALUES(delay_ms)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, location);
ps.setString(2, location);
ps.setLong(3, delay);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setMotionSensorDelay Fehler: " + e.getMessage());
}
}
public long getMotionSensorDelay(String location) {
String q = "SELECT delay_ms FROM bc_motion_sensors WHERE location = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, location);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getLong(1) : -1;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getMotionSensorDelay Fehler: " + e.getMessage());
return -1;
}
}
public void removeMotionSensorSettings(String location) {
String q = "DELETE FROM bc_motion_sensors WHERE location = ?";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, location);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL removeMotionSensorSettings Fehler: " + e.getMessage());
}
}
private boolean isTrusted(String buttonId, UUID playerUUID) {
String q = "SELECT 1 FROM bc_trust WHERE button_id = ? AND target_uuid = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setString(2, playerUUID.toString());
try (ResultSet rs = ps.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL isTrusted Fehler: " + e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,216 @@
package viper;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* GUI zur Konfiguration der zeitgesteuerten Automatisierung eines Controllers.
*
* Layout (9×3 = 27 Slots):
* Slot 10 Öffnungszeit (LIME_DYE / Sonne) ← Links/Rechts: ±1h | Shift: ±15min
* Slot 13 Aktivierung an/aus (LEVER)
* Slot 16 Schließzeit (RED_DYE / Mond) ← Links/Rechts: ±1h | Shift: ±15min
* Slot 22 Speichern & Schließen (EMERALD)
*
* Zeit wird als Ingame-Ticks gespeichert (023999).
* ticksToTime() / timeToTicks() in ButtonControl konvertieren nach "HH:MM".
*/
public class ScheduleGUI implements Listener {
private final ButtonControl plugin;
private final DataManager dataManager;
private final Player player;
private final String buttonId;
private final Inventory inv;
// Aktuelle Werte während die GUI offen ist
private long openTime;
private long closeTime;
private boolean enabled;
public ScheduleGUI(ButtonControl plugin, Player player, String buttonId) {
this.plugin = plugin;
this.dataManager = plugin.getDataManager();
this.player = player;
this.buttonId = buttonId;
this.inv = Bukkit.createInventory(null, 27, "§6Zeitplan-Einstellungen");
// Gespeicherte Werte laden (oder Standardwerte)
long savedOpen = dataManager.getScheduleOpenTime(buttonId);
long savedClose = dataManager.getScheduleCloseTime(buttonId);
this.openTime = savedOpen >= 0 ? savedOpen : plugin.timeToTicks(7, 0); // 07:00
this.closeTime = savedClose >= 0 ? savedClose : plugin.timeToTicks(19, 0); // 19:00
this.enabled = savedOpen >= 0;
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
public void open() {
renderItems();
player.openInventory(inv);
}
// -----------------------------------------------------------------------
// GUI aufbauen
// -----------------------------------------------------------------------
private void renderItems() {
// Füllung
ItemStack filler = makeItem(Material.GRAY_STAINED_GLASS_PANE, ChatColor.RESET + "");
for (int i = 0; i < 27; i++) inv.setItem(i, filler);
// Slot 10 Öffnungszeit
inv.setItem(10, makeTimeItem(
Material.LIME_DYE,
"§a§lÖffnungszeit",
openTime,
"§7Linksklick: §f+1 Stunde",
"§7Rechtsklick: §f1 Stunde",
"§7Shift+Links: §f+15 Minuten",
"§7Shift+Rechts: §f15 Minuten"
));
// Slot 13 Aktivierung an/aus
Material leverMat = enabled ? Material.LEVER : Material.DEAD_BUSH;
String leverName = enabled ? "§a§lZeitplan aktiv" : "§c§lZeitplan deaktiviert";
String leverDesc = enabled ? "§7Klick zum §cDeaktivieren" : "§7Klick zum §aAktivieren";
inv.setItem(13, makeItem(leverMat, leverName, leverDesc, "§8(Zeitplan wird gespeichert)"));
// Slot 16 Schließzeit
inv.setItem(16, makeTimeItem(
Material.RED_DYE,
"§c§lSchließzeit",
closeTime,
"§7Linksklick: §f+1 Stunde",
"§7Rechtsklick: §f1 Stunde",
"§7Shift+Links: §f+15 Minuten",
"§7Shift+Rechts: §f15 Minuten"
));
// Slot 22 Speichern
inv.setItem(22, makeItem(Material.EMERALD,
"§a§lSpeichern & Schließen",
"§7Speichert den aktuellen Zeitplan."));
}
private ItemStack makeTimeItem(Material mat, String name, long ticks, String... loreLines) {
String timeStr = plugin.ticksToTime(ticks);
List<String> lore = new ArrayList<>();
lore.add("§e§l" + timeStr + " Uhr §7(Ingame)");
lore.add("");
lore.addAll(Arrays.asList(loreLines));
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(name);
meta.setLore(lore);
item.setItemMeta(meta);
}
return item;
}
private ItemStack makeItem(Material mat, String name, String... lore) {
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(name);
meta.setLore(Arrays.asList(lore));
item.setItemMeta(meta);
}
return item;
}
// -----------------------------------------------------------------------
// Event-Handler
// -----------------------------------------------------------------------
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!event.getInventory().equals(inv)) return;
if (!event.getWhoClicked().equals(player)) return;
event.setCancelled(true);
ItemStack clicked = event.getCurrentItem();
if (clicked == null || clicked.getType() == Material.GRAY_STAINED_GLASS_PANE) return;
int slot = event.getRawSlot();
// Nur Klicks in unserer GUI (026) verarbeiten
if (slot < 0 || slot > 26) return;
// Schrittgröße: Shift = 15 Min (250 Ticks), sonst 1 Std (1000 Ticks)
long step = event.isShiftClick() ? 250L : 1000L;
if (slot == 10) {
// Öffnungszeit anpassen
if (event.isLeftClick()) openTime = (openTime + step + 24000) % 24000;
if (event.isRightClick()) openTime = (openTime - step + 24000) % 24000;
inv.setItem(10, makeTimeItem(Material.LIME_DYE, "§a§lÖffnungszeit", openTime,
"§7Linksklick: §f+1 Stunde", "§7Rechtsklick: §f1 Stunde",
"§7Shift+Links: §f+15 Minuten", "§7Shift+Rechts: §f15 Minuten"));
} else if (slot == 13) {
// Aktivierung umschalten
enabled = !enabled;
renderItems();
} else if (slot == 16) {
// Schließzeit anpassen
if (event.isLeftClick()) closeTime = (closeTime + step + 24000) % 24000;
if (event.isRightClick()) closeTime = (closeTime - step + 24000) % 24000;
inv.setItem(16, makeTimeItem(Material.RED_DYE, "§c§lSchließzeit", closeTime,
"§7Linksklick: §f+1 Stunde", "§7Rechtsklick: §f1 Stunde",
"§7Shift+Links: §f+15 Minuten", "§7Shift+Rechts: §f15 Minuten"));
} else if (slot == 22) {
// Speichern
save();
player.closeInventory();
}
}
@EventHandler
public void onInventoryDrag(InventoryDragEvent event) {
if (event.getInventory().equals(inv) && event.getWhoClicked().equals(player))
event.setCancelled(true);
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
if (!event.getPlayer().equals(player) || !event.getInventory().equals(inv)) return;
InventoryClickEvent.getHandlerList().unregister(this);
InventoryDragEvent.getHandlerList().unregister(this);
InventoryCloseEvent.getHandlerList().unregister(this);
}
// -----------------------------------------------------------------------
// Speichern
// -----------------------------------------------------------------------
private void save() {
if (enabled) {
dataManager.setScheduleOpenTime(buttonId, openTime);
dataManager.setScheduleCloseTime(buttonId, closeTime);
player.sendMessage("§a[BC] §7Zeitplan gespeichert: §aÖffnet §7um §e"
+ plugin.ticksToTime(openTime)
+ " §7· §cSchließt §7um §e"
+ plugin.ticksToTime(closeTime));
} else {
dataManager.clearSchedule(buttonId);
player.sendMessage("§7[BC] Zeitplan deaktiviert.");
}
}
}

View File

@@ -0,0 +1,42 @@
package viper;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
import java.util.function.Consumer;
import org.json.JSONObject; // -> Dependency in pom.xml nötig
public class UpdateChecker {
private final JavaPlugin plugin;
private final int resourceId;
public UpdateChecker(JavaPlugin plugin, int resourceId) {
this.plugin = plugin;
this.resourceId = resourceId;
}
public void getVersion(final Consumer<String> consumer) {
Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> {
try {
HttpURLConnection connection = (HttpURLConnection)
new URL("https://api.spiget.org/v2/resources/" + resourceId + "/versions/latest").openConnection();
connection.setRequestMethod("GET");
connection.addRequestProperty("User-Agent", "Mozilla/5.0");
try (Scanner scanner = new Scanner(connection.getInputStream())) {
String response = scanner.useDelimiter("\\A").next();
JSONObject json = new JSONObject(response);
String version = json.optString("name", "").trim();
consumer.accept(version);
}
} catch (Exception e) {
plugin.getLogger().warning("Konnte nicht nach Updates suchen: " + e.getMessage());
consumer.accept("");
}
});
}
}

View File

@@ -1,2 +1,53 @@
# ButtonControl Konfiguration
# ── Maximale Anzahl verbundener Blöcke pro Controller ──────────────────────
max-doors: 20
max-lamps: 50
max-noteblocks: 10
max-gates: 20
max-trapdoors: 20
max-bells: 5
# ── Notenblöcke ─────────────────────────────────────────────────────────────
default-note: "PIANO"
double-note-enabled: true
double-note-delay-ms: 1000
# ── Bewegungsmelder ──────────────────────────────────────────────────────────
# Gilt als Standardwert wenn für einen Sensor nichts individuell gesetzt wurde
motion-detection-radius: 5.0
motion-close-delay-ms: 5000
# Cooldown: wie lange nach dem Schließen der Sensor nicht erneut auslöst
motion-trigger-cooldown-ms: 2000
# ── Optionales MySQL-Backend ────────────────────────────────────────────────
mysql:
# false = data.yml verwenden, true = MySQL verwenden
enabled: false
host: "127.0.0.1"
port: 3306
database: "controll"
user: "controll"
password: "controll"
# ── Controller-Name beim Anschauen ──────────────────────────────────────────
controller-name-display:
enabled: true
# Distanz in Blöcken für die Blickerkennung
max-look-distance: 8
# %s = Name aus /bc rename
format: "§6Controller: §f%s"
# ── Sounds beim Öffnen/Schließen (NEU) ──────────────────────────────────────
# Auf false setzen um alle Controller-Sounds zu deaktivieren
sounds:
enabled: true
# Holztüren, Zauntore, Falltüren
door-open: BLOCK_WOODEN_DOOR_OPEN
door-close: BLOCK_WOODEN_DOOR_CLOSE
# Eisentüren und Eisenfalltüren
iron-door-open: BLOCK_IRON_DOOR_OPEN
iron-door-close: BLOCK_IRON_DOOR_CLOSE
# Redstone-Lampen
lamp-on: BLOCK_LEVER_CLICK
lamp-off: BLOCK_LEVER_CLICK

View File

@@ -1,12 +1,64 @@
doors-open: "§aTüren wurden geöffnet."
doors-closed: "§cTüren wurden geschlossen."
lamps-on: "§aLampen wurden eingeschaltet."
lamps-off: cLampen wurden ausgeschaltet."
blocks-toggled: eBlöcke wurden umgeschaltet."
no-blocks-connected: "§cKeine Blöcke sind verbunden."
max-doors-reached: "§cMaximale Anzahl Türen erreicht."
max-lamps-reached: "§cMaximale Anzahl Lampen erreicht."
block-connected: "§aBlock verbunden."
block-already-connected: "§cBlock ist bereits verbunden."
controller-placed: "§aController platziert."
controller-removed: "§cController entfernt."
# lang.yml ButtonControl Nachrichten
# ── Holztüren ────────────────────────────────────────────────────────────────
tueren-geoeffnet: aTüren wurden geöffnet."
tueren-geschlossen: cTüren wurden geschlossen."
max-tueren-erreicht: "§cMaximale Anzahl an Türen erreicht."
# ── Eisentüren (NEU) ─────────────────────────────────────────────────────────
eisentueren-geoeffnet: "§aEisentüren wurden geöffnet."
eisentueren-geschlossen: "§cEisentüren wurden geschlossen."
eisenfallturen-geoeffnet: "§aEisen-Falltüren wurden geöffnet."
eisenfallturen-geschlossen: "§cEisen-Falltüren wurden geschlossen."
# ── Zauntore ─────────────────────────────────────────────────────────────────
gates-geoeffnet: "§aZauntore wurden geöffnet."
gates-geschlossen: "§cZauntore wurden geschlossen."
max-gates-erreicht: "§cMaximale Anzahl an Zauntoren erreicht."
# ── Falltüren ────────────────────────────────────────────────────────────────
fallturen-geoeffnet: "§aFalltüren wurden geöffnet."
fallturen-geschlossen: "§cFalltüren wurden geschlossen."
max-fallturen-erreicht: "§cMaximale Anzahl an Falltüren erreicht."
# ── Lampen & Glocken ─────────────────────────────────────────────────────────
lampen-eingeschaltet: "§aLampen wurden eingeschaltet."
lampen-ausgeschaltet: "§cLampen wurden ausgeschaltet."
max-lampen-erreicht: "§cMaximale Anzahl an Lampen erreicht."
glocke-gelaeutet: "§aGlocke wurde geläutet."
max-glocken-erreicht: "§cMaximale Anzahl an Glocken erreicht."
# ── Notenblöcke ──────────────────────────────────────────────────────────────
notenblock-ausgeloest: "§aNotenblock-Klingel wurde ausgelöst."
instrument-gesetzt: "§aDein Instrument wurde auf %s gesetzt."
ungueltiges-instrument: "§cUngültiges Instrument! Verwende: /bc note <Instrument>"
max-notenbloecke-erreicht: "§cMaximale Anzahl an Notenblöcken erreicht."
# ── Kolben (vorbereitet für zukünftige Erweiterung) ─────────────────────────
kolben-ausgefahren: "§6[ButtonControl] §7Kolben wurden ausgefahren."
kolben-eingefahren: "§6[ButtonControl] §7Kolben wurden eingezogen."
max-kolben-erreicht: "§6[ButtonControl] §7Maximale Anzahl an Kolben erreicht."
# ── Controller & Verbindung ──────────────────────────────────────────────────
block-verbunden: "§aBlock verbunden."
block-bereits-verbunden: "§cBlock ist bereits verbunden."
block-verbindung-entfernt: "§7Verbindung zu abgebautem Block automatisch entfernt."
keine-bloecke-verbunden: "§cKeine Blöcke sind verbunden."
bloecke-umgeschaltet: "§eBlöcke wurden umgeschaltet."
controller-platziert: "§aController platziert."
controller-entfernt: "§cController entfernt."
controller-umbenannt: "§aController umbenannt zu: §f%s"
# ── Trust- & Sicherheitssystem ───────────────────────────────────────────────
keine-berechtigung: "§cDu hast keine Berechtigung für diesen Befehl!"
keine-berechtigung-controller: "§cDu darfst diesen Controller nicht benutzen!"
nur-besitzer-abbauen: "§cNur der Besitzer kann diesen Controller verwalten!"
spieler-nicht-gefunden: "§cDieser Spieler wurde nicht gefunden."
# %s = "§aÖffentlich" oder "§cPrivat"
status-geandert: "§6[BC] §7Controller ist nun %s§7."
trust-hinzugefuegt: "§a%s darf diesen Controller nun benutzen."
trust-entfernt: "§c%s wurde das Vertrauen entzogen."
kein-controller-im-blick: "§cBitte sieh einen Controller direkt an!"
# ── System ───────────────────────────────────────────────────────────────────
konfiguration-neugeladen: "§aKonfiguration und Daten erfolgreich neu geladen!"

View File

@@ -1,11 +1,32 @@
name: ButtonControl
version: 1.0
version: 1.7
main: viper.ButtonControl
api-version: 1.21
author: viper
description: Ein Plugin, um Türen und Redstone-Lampen mit einem Button zu steuern.
author: M_Viper
description: >
Steuert Türen, Eisentüren, Lampen, Notenblöcke und Glocken mit Buttons,
Tageslichtsensoren, Bewegungsmeldern, Teppich-Sensoren und Schildern.
Unterstützt Zeitpläne, Trust-System und Admin-Bypass.
commands:
bc:
description: Zeigt Informationen zum Plugin an
usage: /<command> info
description: Hauptbefehl für ButtonControl
usage: /bc <info|reload|note|list|rename|schedule|trust|untrust|public|private>
aliases: [buttoncontrol]
permissions:
buttoncontrol.admin:
description: Admin-Bypass Zugriff auf und Verwaltung aller Controller
default: op
buttoncontrol.reload:
description: Konfiguration neu laden
default: op
buttoncontrol.note:
description: Notenblock-Instrument ändern
default: true
buttoncontrol.update:
description: Update-Benachrichtigungen empfangen
default: op
buttoncontrol.trust:
description: Trust-System und Public/Private-Status verwalten
default: true