46 Commits
1.0 ... 1.9

Author SHA1 Message Date
cf112e6877 Update from Git Manager GUI 2026-03-27 15:03:54 +01:00
a9d2d7a911 Upload via Git Manager GUI - pom.xml 2026-03-27 14:03:52 +00:00
e351990d74 Upload via Git Manager GUI - dependency-reduced-pom.xml 2026-03-27 14:03:52 +00:00
fbeead1900 Upload via Git Manager GUI - dependency-reduced-pom.xml 2026-03-25 22:34:28 +00:00
1f9a2c2198 Update from Git Manager GUI 2026-03-25 23:34:26 +01:00
9ef3e7e14b Upload via Git Manager GUI - pom.xml 2026-03-25 22:34:26 +00:00
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 4743 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. > **Version:** 1.8 · **Autor:** M_Viper · **Spigot:** [spigotmc.org/resources/127702](https://www.spigotmc.org/resources/127702/)
Ideal für smarte Beleuchtung und Türsysteme in Survival-, Citybuild- oder Roleplay-Servern.
--- ---
## 📦 Features ## 📋 Inhaltsverzeichnis
- 🎛️ **Steuer-Button** steuert beliebige Türen oder Lampen 1. [Was ist ButtonControl?](#was-ist-buttoncontrol)
- 🌞 **Tageslichtsensor-Erkennung** automatische Lampensteuerung bei Tag/Nacht 2. [Controller-Typen im Überblick](#controller-typen-im-überblick)
- 🔌 **Verbindungssystem** verknüpfe mehrere Blöcke mit einem Steuergerät 3. [Steuerbare Blöcke](#steuerbare-blöcke)
- 🧱 **Custom Rezepte** eigene Crafting-Rezepte für Steuer-Button & Tageslichtsensor 4. [Schritt-für-Schritt: Erste Schritte](#schritt-für-schritt-erste-schritte)
- 🔍 **Kommando `/bc info`** zeigt Plugin-Infos direkt im Spiel an 5. [Rezepte Controller herstellen](#rezepte--controller-herstellen)
- ✅ Vollständig kompatibel mit **Minecraft 1.21.5 1.21.8** 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 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.
2. Lege sie in den `plugins/`-Ordner deines Servers
3. Starte oder reloade deinen Server **Mögliche Controller-Typen:**
4. Fertig! 🎉 - 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 | | Symbol | Controller | Auslöser | Besonderheit |
|----------------|------------------------------------------| |--------|-----------|---------|--------------|
| `/bc info` | Zeigt Informationen über das Plugin an | | 🔘 | **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** Folgende Blöcke können mit einem Controller verbunden werden:
- ✅ Java 17+
- ✅ Paper / Spigot / Purpur | 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 ### 4. Controller benutzen
> 🌞 = Daylight Detector Klicke mit **Rechtsklick** auf den platzierten Controller. Alle verbundenen Blöcke werden gleichzeitig umgeschaltet.
--- ---
## 👤 Autor ## Rezepte Controller herstellen
**M_Viper** Alle Rezepte folgen demselben Muster: **3× dasselbe Material in der mittleren Spalte** der Werkbank.
Plugin-Entwicklung & Idee
```
[ ] [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**. ### Platzieren
Du darfst es frei nutzen, verändern und teilen Credits willkommen. 🤝 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>

95
pom.xml Normal file
View File

@@ -0,0 +1,95 @@
<?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.9</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 (optional) -->
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.2</version>
<optional>true</optional>
</dependency>
<!-- org.json für UpdateChecker JSON-Parsing -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
<!-- MySQL Connector/J für optionales Datenbank-Backend -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,21 @@
package viper; package viper;
import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Block; 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.block.data.Lightable;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.Action; import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
@@ -29,187 +34,481 @@ public class ButtonListener implements Listener {
this.dataManager = dataManager; this.dataManager = dataManager;
} }
// -----------------------------------------------------------------------
// Interact Benutzung + Verbinden
// -----------------------------------------------------------------------
@EventHandler @EventHandler
public void onPlayerInteract(PlayerInteractEvent event) { 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;
Player player = event.getPlayer();
UUID playerUUID = player.getUniqueId();
ItemStack item = event.getItem(); ItemStack item = event.getItem();
Block block = event.getClickedBlock(); Block block = event.getClickedBlock();
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && block != null && // ── 1. Bereits platzierter Controller ──────────────────────────────
(block.getType() == Material.STONE_BUTTON || block.getType() == Material.DAYLIGHT_DETECTOR)) { if (event.getAction() == Action.RIGHT_CLICK_BLOCK && block != null) {
String blockLocation = plugin.toLoc(block);
String blockLocation = block.getWorld().getName() + "," + block.getX() + "," + block.getY() + "," + block.getZ(); String buttonId = dataManager.getButtonIdForLocation(blockLocation);
String buttonId = dataManager.getButtonIdForPlacedController(playerUUID, blockLocation);
if (buttonId != null) { if (buttonId != null) {
// Admin-Bypass
if (!dataManager.canAccess(buttonId, playerUUID)
&& !player.hasPermission("buttoncontrol.admin")) {
player.sendMessage(configManager.getMessage("keine-berechtigung-controller"));
event.setCancelled(true); event.setCancelled(true);
List<String> connectedBlocks = dataManager.getConnectedBlocks(playerUUID, buttonId); return;
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) {
// 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);
boolean hasConnectedBlocks = connectedBlocks != null && !connectedBlocks.isEmpty();
boolean secretTriggered = plugin.triggerSecretWall(buttonId);
if (hasConnectedBlocks) {
toggleConnectedBlocks(player, playerUUID, connectedBlocks);
}
if (!hasConnectedBlocks && !secretTriggered) {
player.sendMessage(configManager.getMessage("keine-bloecke-verbunden"));
}
}
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 (isInteractableTarget(block.getType())) {
event.setCancelled(true);
// Doppeltür: immer untersten Block speichern
Block targetBlock = getBottomDoorBlock(block);
ItemMeta meta = item.getItemMeta();
String buttonId = extractButtonId(meta);
if (buttonId == null) {
buttonId = UUID.randomUUID().toString();
updateButtonLore(item, buttonId);
}
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"));
}
}
}
// -----------------------------------------------------------------------
// Block-Break: Controller abbauen
// -----------------------------------------------------------------------
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
Block block = event.getBlock();
String blockLocation = plugin.toLoc(block);
String buttonId = dataManager.getButtonIdForLocation(blockLocation);
if (buttonId != null) {
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"));
}
}
// -----------------------------------------------------------------------
// 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"));
}
}
// -----------------------------------------------------------------------
// 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 anyGrateOpened = false, anyGrateClosed = false;
boolean anyCreakingHeartOn = false, anyCreakingHeartOff = false;
boolean anyNoteBlockPlayed = false;
boolean anyBellPlayed = false;
boolean anyDispenserTriggered = false;
boolean anyDropperTriggered = 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; }
}
}
// ── Lampen (Redstone + Kupferlampen) ─────────────────────────
else if (isLamp(mat)) {
if (targetBlock.getBlockData() instanceof Lightable) {
Lightable lamp = (Lightable) targetBlock.getBlockData(); Lightable lamp = (Lightable) targetBlock.getBlockData();
boolean wasLit = lamp.isLit(); boolean wasLit = lamp.isLit();
lamp.setLit(!wasLit); lamp.setLit(!wasLit);
targetBlock.setBlockData(lamp); targetBlock.setBlockData(lamp);
if (soundsEnabled) {
if (!wasLit) { playConfigSound(location,
anyLampOn = true; wasLit ? "sounds.lamp-off" : "sounds.lamp-on",
} else { "BLOCK_LEVER_CLICK");
anyLampOff = true; }
if (!wasLit) anyLampOn = true; else anyLampOff = true;
}
}
// ── Gitter (alle *_GRATE + Eisenstangen) ─────────────────────
else if (plugin.isGrate(mat) || (mat == Material.AIR && plugin.isManagedOpenGrateLocation(locStr))) {
Boolean nowOpen = plugin.toggleGrate(targetBlock);
if (nowOpen != null) {
if (nowOpen) anyGrateOpened = true;
else anyGrateClosed = 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;
}
// ── Spender / Werfer ──────────────────────────────────────────
else if (mat == Material.DISPENSER) {
if (triggerContainer(targetBlock, "dispense")) {
targetBlock.getWorld().playSound(location, Sound.BLOCK_DISPENSER_DISPENSE, 1.0f, 1.0f);
anyDispenserTriggered = true;
}
}
else if (mat == Material.DROPPER) {
if (triggerContainer(targetBlock, "drop")) {
targetBlock.getWorld().playSound(location, Sound.BLOCK_DISPENSER_DISPENSE, 1.0f, 1.0f);
anyDropperTriggered = true;
}
}
// ── Creaking Heart ────────────────────────────────────────────
else if (isCreakingHeart(mat)) {
Boolean nowActive = plugin.togglePersistentCreakingHeart(targetBlock);
if (nowActive != null) {
if (nowActive) anyCreakingHeartOn = true;
else anyCreakingHeartOff = true;
} }
} }
} }
if (anyDoorOpened) { // Feedback-Nachrichten
event.getPlayer().sendMessage(configManager.getMessage("doors-open")); if (anyDoorOpened) player.sendMessage(configManager.getMessage("tueren-geoeffnet"));
} if (anyDoorClosed) player.sendMessage(configManager.getMessage("tueren-geschlossen"));
if (anyDoorClosed) { if (anyIronDoorOpened) player.sendMessage(configManager.getMessage("eisentueren-geoeffnet"));
event.getPlayer().sendMessage(configManager.getMessage("doors-closed")); if (anyIronDoorClosed) player.sendMessage(configManager.getMessage("eisentueren-geschlossen"));
} if (anyIronTrapOpened) player.sendMessage(configManager.getMessage("eisenfallturen-geoeffnet"));
if (anyLampOn) { if (anyIronTrapClosed) player.sendMessage(configManager.getMessage("eisenfallturen-geschlossen"));
event.getPlayer().sendMessage(configManager.getMessage("lamps-on")); if (anyGateOpened) player.sendMessage(configManager.getMessage("gates-geoeffnet"));
} if (anyGateClosed) player.sendMessage(configManager.getMessage("gates-geschlossen"));
if (anyLampOff) { if (anyTrapOpened) player.sendMessage(configManager.getMessage("fallturen-geoeffnet"));
event.getPlayer().sendMessage(configManager.getMessage("lamps-off")); 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 (anyGrateOpened) player.sendMessage(configManager.getMessage("gitter-geoeffnet"));
if (anyGrateClosed) player.sendMessage(configManager.getMessage("gitter-geschlossen"));
if (anyCreakingHeartOn) player.sendMessage(configManager.getMessage("creaking-heart-aktiviert"));
if (anyCreakingHeartOff) player.sendMessage(configManager.getMessage("creaking-heart-deaktiviert"));
if (anyNoteBlockPlayed) player.sendMessage(configManager.getMessage("notenblock-ausgeloest"));
if (anyBellPlayed) player.sendMessage(configManager.getMessage("glocke-gelaeutet"));
if (anyDispenserTriggered) player.sendMessage(configManager.getMessage("spender-ausgeloest"));
if (anyDropperTriggered) player.sendMessage(configManager.getMessage("werfer-ausgeloest"));
} }
} else { /**
event.getPlayer().sendMessage(configManager.getMessage("no-blocks-connected")); * 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.
} */
return; private void playConfigSound(Location loc, String configKey, String fallback) {
} String soundName = configManager.getConfig().getString(configKey, fallback);
try {
if (item == null || (!item.getType().equals(Material.STONE_BUTTON) && !item.getType().equals(Material.DAYLIGHT_DETECTOR))) { Sound sound = Sound.valueOf(soundName.toUpperCase());
return; loc.getWorld().playSound(loc, sound, 1.0f, 1.0f);
} } catch (IllegalArgumentException e) {
try {
if (!item.hasItemMeta() || !item.getItemMeta().getDisplayName().contains("§6Steuer-")) { Sound sound = Sound.valueOf(fallback.toUpperCase());
return; loc.getWorld().playSound(loc, sound, 1.0f, 1.0f);
} } catch (IllegalArgumentException ignored) { }
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || block == null) {
return;
}
if (isDoor(block.getType()) || block.getType() == Material.REDSTONE_LAMP) {
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<>();
}
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);
updateButtonLore(item, buttonId);
event.getPlayer().sendMessage(configManager.getMessage("block-connected"));
} else {
event.getPlayer().sendMessage(configManager.getMessage("block-already-connected"));
}
} }
} }
@EventHandler private boolean triggerContainer(Block block, String methodName) {
public void onBlockPlace(BlockPlaceEvent event) { try {
String playerUUID = event.getPlayer().getUniqueId().toString(); Object state = block.getState();
java.lang.reflect.Method method = state.getClass().getMethod(methodName);
ItemStack item = event.getItemInHand(); Object result = method.invoke(state);
Block block = event.getBlockPlaced(); return !(result instanceof Boolean) || (Boolean) result;
} catch (ReflectiveOperationException ignored) {
if (item == null || (!item.getType().equals(Material.STONE_BUTTON) && !item.getType().equals(Material.DAYLIGHT_DETECTOR))) { return false;
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"));
}
@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);
if (buttonId != null) {
dataManager.removePlacedController(playerUUID, blockLocation);
dataManager.setConnectedBlocks(playerUUID, buttonId, null);
event.getPlayer().sendMessage(configManager.getMessage("controller-removed"));
} }
} }
private boolean isDoor(Material material) { // -----------------------------------------------------------------------
return material.toString().endsWith("_DOOR"); // 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 (isLamp(type)) {
if (connected.stream().filter(l -> isLamp(getMaterialAt(l))).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;
}
} else if (type == Material.DISPENSER) {
if (connected.stream().filter(l -> getMaterialAt(l) == Material.DISPENSER).count()
>= configManager.getMaxDispensers()) {
player.sendMessage(configManager.getMessage("max-spender-erreicht")); return false;
}
} else if (type == Material.DROPPER) {
if (connected.stream().filter(l -> getMaterialAt(l) == Material.DROPPER).count()
>= configManager.getMaxDroppers()) {
player.sendMessage(configManager.getMessage("max-werfer-erreicht")); return false;
}
}
return true;
} }
private Material getMaterialFromLocation(String locString) { // -----------------------------------------------------------------------
String[] parts = locString.split(","); // Hilfsmethoden
if (parts.length != 4) return null; // -----------------------------------------------------------------------
Location loc = new Location(plugin.getServer().getWorld(parts[0]),
Integer.parseInt(parts[1]), /** Gibt bei zweiteiligen Türen immer den untersten Block zurück. */
Integer.parseInt(parts[2]), private Block getBottomDoorBlock(Block block) {
Integer.parseInt(parts[3])); Material mat = block.getType();
return loc.getBlock().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) { private void updateButtonLore(ItemStack item, String buttonId) {
ItemMeta meta = item.getItemMeta(); ItemMeta meta = item.getItemMeta();
if (meta != null) { if (meta != null) {
List<String> lore = meta.hasLore() ? meta.getLore() : new ArrayList<>(); List<String> lore = new ArrayList<>();
if (!lore.contains(buttonId)) { lore.add("§8ID: " + buttonId);
lore.add(buttonId); lore.add("§7Ein universeller Controller für");
meta.setLore(lore); meta.setLore(lore);
item.setItemMeta(meta); 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 isLamp(Material m) {
return m == Material.REDSTONE_LAMP
|| "COPPER_BULB".equals(m.name())
|| m.name().endsWith("_COPPER_BULB");
}
private boolean isCreakingHeart(Material m) { return "CREAKING_HEART".equals(m.name()); }
private boolean isInteractableTarget(Material m) {
return isDoor(m) || isGate(m) || isTrapdoor(m)
|| m == Material.IRON_DOOR || m == Material.IRON_TRAPDOOR
|| isLamp(m) || plugin.isGrate(m)
|| m == Material.NOTE_BLOCK || m == Material.BELL
|| m == Material.DISPENSER || m == Material.DROPPER
|| isCreakingHeart(m);
}
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,73 @@
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", "undo", "secret"
);
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;
case "secret":
StringUtil.copyPartialMatches(args[1], Arrays.asList("select", "info", "add", "remove", "clear", "delay", "animation"), completions);
break;
}
} else if (args.length == 3 && args[0].equalsIgnoreCase("secret") && args[1].equalsIgnoreCase("delay")) {
completions.add("3");
completions.add("5");
completions.add("10");
completions.add("30");
completions.add("60");
} else if (args.length == 3 && args[0].equalsIgnoreCase("secret") && args[1].equalsIgnoreCase("animation")) {
completions.add("instant");
completions.add("wave");
completions.add("reverse");
completions.add("center");
}
if (args.length == 2 && args[0].equalsIgnoreCase("secret")) {
if (completions.isEmpty()) {
StringUtil.copyPartialMatches(args[1], Arrays.asList("select", "info", "add", "remove", "clear", "delay", "animation"), completions);
}
}
Collections.sort(completions);
return completions;
}
}

View File

@@ -3,8 +3,8 @@ package viper;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File; import java.io.*;
import java.io.IOException; import java.nio.charset.StandardCharsets;
public class ConfigManager { public class ConfigManager {
private final ButtonControl plugin; private final ButtonControl plugin;
@@ -21,45 +21,192 @@ public class ConfigManager {
private void loadConfig() { private void loadConfig() {
configFile = new File(plugin.getDataFolder(), "config.yml"); configFile = new File(plugin.getDataFolder(), "config.yml");
if (!configFile.exists()) { if (!configFile.exists()) plugin.saveResource("config.yml", false);
plugin.saveResource("config.yml", false);
}
config = YamlConfiguration.loadConfiguration(configFile); config = YamlConfiguration.loadConfiguration(configFile);
mergeDefaults(config, "config.yml", configFile);
setConfigDefaults();
} }
private void loadLang() { private void loadLang() {
langFile = new File(plugin.getDataFolder(), "lang.yml"); langFile = new File(plugin.getDataFolder(), "lang.yml");
if (!langFile.exists()) { if (!langFile.exists()) plugin.saveResource("lang.yml", false);
plugin.saveResource("lang.yml", false);
}
lang = YamlConfiguration.loadConfiguration(langFile); 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() { private void setConfigDefaults() {
return config.getInt("max-lamps", 50); 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, "max-dispensers", 20);
def(config, "max-droppers", 20);
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);
def(config, "timed-container-interval-ticks", 40);
// 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.");
// Creaking Heart
def(lang, "creaking-heart-aktiviert", "§aKnarrherz wurde aktiviert.");
def(lang, "creaking-heart-deaktiviert", "§cKnarrherz wurde deaktiviert.");
// Gitter
def(lang, "gitter-geoeffnet", "§aGitter wurden geöffnet.");
def(lang, "gitter-geschlossen", "§cGitter wurden geschlossen.");
// Glocken
def(lang, "glocke-gelaeutet", "§aGlocke wurde geläutet.");
def(lang, "max-glocken-erreicht", "§cMaximale Anzahl an Glocken erreicht.");
// Spender / Werfer
def(lang, "spender-ausgeloest", "§aSpender wurden ausgelöst.");
def(lang, "werfer-ausgeloest", "§aWerfer wurden ausgelöst.");
def(lang, "max-spender-erreicht", "§cMaximale Anzahl an Spendern erreicht.");
def(lang, "max-werfer-erreicht", "§cMaximale Anzahl an Werfern 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 int getMaxDispensers() { return config.getInt("max-dispensers", 20); }
public int getMaxDroppers() { return config.getInt("max-droppers", 20); }
public String getMessage(String key) { public String getMessage(String key) {
return lang.getString(key, "Nachricht nicht gefunden: " + key); return lang.getString(key, "§cNachricht fehlt: " + key);
} }
public void saveConfig() { public void saveConfig() {
try { try { config.save(configFile); }
config.save(configFile); catch (IOException e) { plugin.getLogger().severe("config.yml Fehler: " + e.getMessage()); }
} catch (IOException e) {
plugin.getLogger().severe("Konnte config.yml nicht speichern: " + e.getMessage());
}
} }
public void saveLang() { public void saveLang() {
try { try { lang.save(langFile); }
lang.save(langFile); catch (IOException e) { plugin.getLogger().severe("lang.yml Fehler: " + e.getMessage()); }
} catch (IOException e) {
plugin.getLogger().severe("Konnte lang.yml nicht speichern: " + e.getMessage());
}
} }
} }

View File

@@ -4,115 +4,462 @@ import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File; import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.UUID;
public class DataManager { public class DataManager {
private final ButtonControl plugin; private final ButtonControl plugin;
private FileConfiguration data; private FileConfiguration data;
private File dataFile; private File dataFile;
private MySQLStorage mySQLStorage;
public DataManager(ButtonControl plugin) { public DataManager(ButtonControl plugin) {
this.plugin = plugin; this.plugin = plugin;
loadData(); loadData();
initializeStorage();
}
private void initializeStorage() {
mySQLStorage = new MySQLStorage(plugin);
if (!mySQLStorage.initialize()) {
mySQLStorage = null;
}
} }
private void loadData() { private void loadData() {
dataFile = new File(plugin.getDataFolder(), "data.yml"); dataFile = new File(plugin.getDataFolder(), "data.yml");
if (!dataFile.exists()) { if (!dataFile.exists()) plugin.saveResource("data.yml", false);
plugin.saveResource("data.yml", false);
}
data = YamlConfiguration.loadConfiguration(dataFile); data = YamlConfiguration.loadConfiguration(dataFile);
} }
// --- Spielerbasierte Methoden --- public void reloadData() {
data = YamlConfiguration.loadConfiguration(dataFile);
public List<String> getConnectedBlocks(String playerUUID, String buttonId) { if (mySQLStorage != null) {
return data.getStringList("players." + playerUUID + ".buttons." + buttonId); 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);
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);
}
}
}
// Alle zugehörigen Daten (Name, Status, Trust, Zeitplan, Verbindungen, Secret) 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);
// Secret-Wall-Daten ebenfalls entfernen
data.set("secret-walls." + buttonId, null);
// Sicherheitshalber bei ALLEN Spielern den Button-Eintrag löschen
if (data.getConfigurationSection("players") != null) {
for (String uuid : data.getConfigurationSection("players").getKeys(false)) {
data.set("players." + uuid + ".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) { 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); data.set("players." + playerUUID + ".buttons." + buttonId, blocks);
saveData(); saveData();
} }
public void addPlacedController(String playerUUID, String location, String buttonId) { public List<String> getConnectedBlocks(String buttonId) {
data.set("players." + playerUUID + ".placed-controllers." + location, buttonId); if (mySQLStorage != null) return mySQLStorage.getConnectedBlocks(buttonId);
saveData(); 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);
} }
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<>(); 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). * Entfernt eine Block-Location aus ALLEN Verbindungslisten aller Controller.
* Nützlich für Tageslichtsensoren. * Wird aufgerufen wenn ein verbundener Block abgebaut wird.
*/ */
public List<String> getAllPlacedControllers() { public boolean removeFromAllConnectedBlocks(String locStr) {
List<String> allControllers = new ArrayList<>(); if (mySQLStorage != null) return mySQLStorage.removeFromAllConnectedBlocks(locStr);
if (data.getConfigurationSection("players") == null) { if (data.getConfigurationSection("players") == null) return false;
return allControllers; 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;
} }
Set<String> players = data.getConfigurationSection("players").getKeys(false);
for (String playerUUID : players) {
allControllers.addAll(getAllPlacedControllers(playerUUID));
} }
return allControllers;
} }
if (changed) saveData();
return changed;
}
// -----------------------------------------------------------------------
// 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);
}
public void setScheduleShotDelayTicks(String buttonId, int ticks) {
if (mySQLStorage != null) {
mySQLStorage.setScheduleShotDelayTicks(buttonId, ticks);
return;
}
data.set("schedules." + buttonId + ".shot-delay-ticks", ticks);
saveData();
}
public int getScheduleShotDelayTicks(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.getScheduleShotDelayTicks(buttonId);
return data.getInt("schedules." + buttonId + ".shot-delay-ticks", -1);
}
public void setScheduleTriggerMode(String buttonId, String mode) {
if (mySQLStorage != null) {
mySQLStorage.setScheduleTriggerMode(buttonId, mode);
return;
}
data.set("schedules." + buttonId + ".trigger-mode", mode);
saveData();
}
public String getScheduleTriggerMode(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.getScheduleTriggerMode(buttonId);
return data.getString("schedules." + buttonId + ".trigger-mode");
}
/** 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();
}
// -----------------------------------------------------------------------
// Secret-Wall (Geheimwand)
// -----------------------------------------------------------------------
public void setSecretBlocks(String buttonId, List<String> blocks) {
if (mySQLStorage != null) {
mySQLStorage.setSecretBlocks(buttonId, blocks);
return;
}
data.set("secret-walls." + buttonId + ".blocks", blocks);
saveData();
}
public List<String> getSecretBlocks(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.getSecretBlocks(buttonId);
return data.getStringList("secret-walls." + buttonId + ".blocks");
}
public void setSecretRestoreDelayMs(String buttonId, long delayMs) {
if (mySQLStorage != null) {
mySQLStorage.setSecretRestoreDelayMs(buttonId, delayMs);
return;
}
data.set("secret-walls." + buttonId + ".delay-ms", delayMs);
saveData();
}
public long getSecretRestoreDelayMs(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.getSecretRestoreDelayMs(buttonId);
return data.getLong("secret-walls." + buttonId + ".delay-ms", 5000L);
}
public void setSecretAnimation(String buttonId, String animation) {
if (mySQLStorage != null) {
mySQLStorage.setSecretAnimation(buttonId, animation);
return;
}
data.set("secret-walls." + buttonId + ".animation", animation);
saveData();
}
public String getSecretAnimation(String buttonId) {
if (mySQLStorage != null) return mySQLStorage.getSecretAnimation(buttonId);
return data.getString("secret-walls." + buttonId + ".animation", "wave");
}
public void clearSecret(String buttonId) {
if (mySQLStorage != null) {
mySQLStorage.clearSecret(buttonId);
return;
}
data.set("secret-walls." + buttonId, null);
saveData();
}
// -----------------------------------------------------------------------
// Speichern asynchron
// -----------------------------------------------------------------------
/** /**
* Holt die Button-ID für einen platzierten Controller an einem Ort, ohne Spieler-UUID (global). * Serialisiert die Daten synchron (thread-safe), schreibt dann asynchron auf Disk.
* Da Controller pro Spieler gespeichert sind, suchen wir alle Spieler ab. * Verhindert I/O-Lags auf dem Main-Thread.
*/ */
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;
}
}
return null;
}
public void saveData() { public void saveData() {
if (mySQLStorage != null) return;
final String serialized;
try { try {
data.save(dataFile); 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) { } catch (IOException e) {
plugin.getLogger().severe("Konnte data.yml nicht speichern: " + e.getMessage()); plugin.getLogger().severe("Konnte data.yml nicht speichern: " + e.getMessage());
} }
});
} }
} }

View File

@@ -0,0 +1,23 @@
package viper;
import org.bukkit.plugin.java.JavaPlugin;
// Import aus dem korrekten verschobenen Package:
public class MetricsHandler {
private static final int BSTATS_PLUGIN_ID = 26862;
public static void startMetrics(JavaPlugin plugin) {
try {
Class<?> metricsClass = Class.forName("org.bstats.bukkit.Metrics");
metricsClass.getConstructor(JavaPlugin.class, int.class)
.newInstance(plugin, BSTATS_PLUGIN_ID);
} catch (ClassNotFoundException e) {
plugin.getLogger().info("bStats nicht gefunden Telemetrie deaktiviert.");
} catch (Exception e) {
plugin.getLogger().warning("bStats konnte nicht initialisiert werden: " + e.getMessage());
}
}
}

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,680 @@
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 Verbindung fehlgeschlagen - verwende data.yml für Datenspeicherung.");
plugin.getLogger().fine("Fehlerdetails: " + 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,"
+ "shot_delay_ticks INT,"
+ "trigger_mode VARCHAR(16)"
+ ")");
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"
+ ")");
st.executeUpdate("CREATE TABLE IF NOT EXISTS bc_secret_walls ("
+ "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_secret_settings ("
+ "button_id VARCHAR(64) PRIMARY KEY,"
+ "delay_ms BIGINT NOT NULL,"
+ "animation VARCHAR(16) NOT NULL DEFAULT 'wave'"
+ ")");
st.executeUpdate("ALTER TABLE bc_schedules "
+ "ADD COLUMN IF NOT EXISTS shot_delay_ticks INT");
st.executeUpdate("ALTER TABLE bc_schedules "
+ "ADD COLUMN IF NOT EXISTS trigger_mode VARCHAR(16)");
st.executeUpdate("ALTER TABLE bc_secret_settings "
+ "ADD COLUMN IF NOT EXISTS animation VARCHAR(16) NOT NULL DEFAULT 'wave'");
}
}
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 = ?",
"DELETE FROM bc_secret_walls WHERE button_id = ?",
"DELETE FROM bc_secret_settings 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, shot_delay_ticks, trigger_mode) VALUES (?, ?, -1, -1, 'simultaneous')"
+ " ON DUPLICATE KEY UPDATE open_time = VALUES(open_time)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setLong(2, ticks);
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, shot_delay_ticks, trigger_mode) VALUES (?, -1, ?, -1, 'simultaneous')"
+ " ON DUPLICATE KEY UPDATE close_time = VALUES(close_time)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setLong(2, 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 setScheduleShotDelayTicks(String buttonId, int ticks) {
String q = "INSERT INTO bc_schedules (button_id, open_time, close_time, shot_delay_ticks, trigger_mode) VALUES (?, -1, -1, ?, 'simultaneous')"
+ " ON DUPLICATE KEY UPDATE shot_delay_ticks = VALUES(shot_delay_ticks)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setInt(2, ticks);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setScheduleShotDelayTicks Fehler: " + e.getMessage());
}
}
public int getScheduleShotDelayTicks(String buttonId) {
String q = "SELECT shot_delay_ticks FROM bc_schedules WHERE button_id = ? LIMIT 1";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return -1;
int delay = rs.getInt(1);
return rs.wasNull() ? -1 : delay;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getScheduleShotDelayTicks Fehler: " + e.getMessage());
return -1;
}
}
public void setScheduleTriggerMode(String buttonId, String mode) {
String q = "INSERT INTO bc_schedules (button_id, open_time, close_time, shot_delay_ticks, trigger_mode) VALUES (?, -1, -1, -1, ?)"
+ " ON DUPLICATE KEY UPDATE trigger_mode = VALUES(trigger_mode)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setString(2, mode);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setScheduleTriggerMode Fehler: " + e.getMessage());
}
}
public String getScheduleTriggerMode(String buttonId) {
String q = "SELECT trigger_mode 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.getString(1) : null;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getScheduleTriggerMode Fehler: " + e.getMessage());
return null;
}
}
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());
}
}
public void setSecretBlocks(String buttonId, List<String> blocks) {
String del = "DELETE FROM bc_secret_walls WHERE button_id = ?";
String ins = "INSERT INTO bc_secret_walls (button_id, block_location) VALUES (?, ?)";
try (PreparedStatement psDel = getConnection().prepareStatement(del);
PreparedStatement psIns = getConnection().prepareStatement(ins)) {
psDel.setString(1, buttonId);
psDel.executeUpdate();
for (String block : blocks) {
psIns.setString(1, buttonId);
psIns.setString(2, block);
psIns.addBatch();
}
psIns.executeBatch();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setSecretBlocks Fehler: " + e.getMessage());
}
}
public List<String> getSecretBlocks(String buttonId) {
List<String> result = new ArrayList<>();
String q = "SELECT block_location FROM bc_secret_walls 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 getSecretBlocks Fehler: " + e.getMessage());
}
return result;
}
public void setSecretRestoreDelayMs(String buttonId, long delayMs) {
String q = "INSERT INTO bc_secret_settings (button_id, delay_ms) VALUES (?, ?)"
+ " ON DUPLICATE KEY UPDATE delay_ms = VALUES(delay_ms)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setLong(2, delayMs);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setSecretRestoreDelayMs Fehler: " + e.getMessage());
}
}
public long getSecretRestoreDelayMs(String buttonId) {
String q = "SELECT delay_ms FROM bc_secret_settings 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) : 5000L;
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getSecretRestoreDelayMs Fehler: " + e.getMessage());
return 5000L;
}
}
public void setSecretAnimation(String buttonId, String animation) {
String q = "INSERT INTO bc_secret_settings (button_id, delay_ms, animation) VALUES (?, COALESCE((SELECT delay_ms FROM bc_secret_settings WHERE button_id = ?), 5000), ?)"
+ " ON DUPLICATE KEY UPDATE animation = VALUES(animation)";
try (PreparedStatement ps = getConnection().prepareStatement(q)) {
ps.setString(1, buttonId);
ps.setString(2, buttonId);
ps.setString(3, animation);
ps.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL setSecretAnimation Fehler: " + e.getMessage());
}
}
public String getSecretAnimation(String buttonId) {
String q = "SELECT animation FROM bc_secret_settings 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) : "wave";
}
} catch (SQLException e) {
plugin.getLogger().warning("MySQL getSecretAnimation Fehler: " + e.getMessage());
return "wave";
}
}
public void clearSecret(String buttonId) {
String q1 = "DELETE FROM bc_secret_walls WHERE button_id = ?";
String q2 = "DELETE FROM bc_secret_settings WHERE button_id = ?";
try (PreparedStatement ps1 = getConnection().prepareStatement(q1);
PreparedStatement ps2 = getConnection().prepareStatement(q2)) {
ps1.setString(1, buttonId);
ps1.executeUpdate();
ps2.setString(1, buttonId);
ps2.executeUpdate();
} catch (SQLException e) {
plugin.getLogger().warning("MySQL clearSecret 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,315 @@
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;
import java.util.Locale;
/**
* GUI zur Konfiguration der zeitgesteuerten Automatisierung eines Controllers.
*
* Layout (9×3 = 27 Slots):
* Slot 4 Abschuss-Verzögerung Werfer/Spender (REPEATER) ← Links/Rechts: ±1 Tick | Shift: ±5
* Slot 6 Schuss-Modus (COMPARATOR) ← Klick: gleichzeitig / nacheinander
* 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 int shotDelayTicks;
private String triggerMode;
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);
int savedShotDelay = dataManager.getScheduleShotDelayTicks(buttonId);
String savedTriggerMode = dataManager.getScheduleTriggerMode(buttonId);
this.openTime = savedOpen >= 0 ? savedOpen : plugin.timeToTicks(7, 0); // 07:00
this.closeTime = savedClose >= 0 ? savedClose : plugin.timeToTicks(19, 0); // 19:00
this.shotDelayTicks = savedShotDelay >= 0
? savedShotDelay
: Math.max(1, plugin.getConfigManager().getConfig().getInt("timed-container-shot-delay-ticks", 2));
this.triggerMode = normalizeTriggerMode(savedTriggerMode != null
? savedTriggerMode
: plugin.getConfigManager().getConfig().getString("timed-container-trigger-mode", "simultaneous"));
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 4 Abschuss-Verzögerung
inv.setItem(4, makeDelayItem());
// Slot 6 Modus
inv.setItem(6, makeModeItem());
// 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 makeDelayItem() {
List<String> lore = new ArrayList<>();
lore.add("§e§l" + shotDelayTicks + " Ticks §7(" + formatShotDelaySeconds() + "s§7)");
if (isSequentialMode()) {
lore.add("§7Aktuell: §f" + shotDelayTicks + " Ticks zwischen einzelnen Geräten");
} else if (shotDelayTicks <= 1) {
lore.add("§7Aktuell: §falle verbundenen Werfer schießen jeden Tick");
} else {
lore.add("§7Aktuell: §f" + shotDelayTicks + " Ticks zwischen gemeinsamen Schüssen");
}
lore.add("");
lore.add("§7Linksklick: §f+1 Tick");
lore.add("§7Rechtsklick: §f1 Tick");
lore.add("§7Shift+Links: §f+5 Ticks");
lore.add("§7Shift+Rechts: §f5 Ticks");
lore.add("§8(1 Tick = schnellstmöglich)");
ItemStack item = new ItemStack(Material.REPEATER);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§b§lAbschuss-Verzögerung");
meta.setLore(lore);
item.setItemMeta(meta);
}
return item;
}
private ItemStack makeModeItem() {
List<String> lore = new ArrayList<>();
lore.add(isSequentialMode()
? "§e§lNacheinander"
: "§e§lGleichzeitig");
lore.add("");
lore.add("§7Klick: §fModus wechseln");
lore.add("§8Gleichzeitig = alle Werfer zusammen");
lore.add("§8Nacheinander = Geräte rotieren der Reihe nach");
ItemStack item = new ItemStack(Material.COMPARATOR);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName("§d§lSchuss-Modus");
meta.setLore(lore);
item.setItemMeta(meta);
}
return item;
}
private String formatShotDelaySeconds() {
return String.format(Locale.US, "%.2f", shotDelayTicks / 20.0);
}
private boolean isSequentialMode() {
return "sequential".equals(triggerMode);
}
private String normalizeTriggerMode(String mode) {
return "sequential".equalsIgnoreCase(mode) ? "sequential" : "simultaneous";
}
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 == 4) {
int delayStep = event.isShiftClick() ? 5 : 1;
if (event.isLeftClick()) shotDelayTicks += delayStep;
if (event.isRightClick()) shotDelayTicks -= delayStep;
if (shotDelayTicks < 1) shotDelayTicks = 1;
if (shotDelayTicks > 200) shotDelayTicks = 200;
inv.setItem(4, makeDelayItem());
} else if (slot == 6) {
triggerMode = isSequentialMode() ? "simultaneous" : "sequential";
inv.setItem(4, makeDelayItem());
inv.setItem(6, makeModeItem());
} else 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);
dataManager.setScheduleShotDelayTicks(buttonId, shotDelayTicks);
dataManager.setScheduleTriggerMode(buttonId, triggerMode);
player.sendMessage("§a[BC] §7Zeitplan gespeichert: §aÖffnet §7um §e"
+ plugin.ticksToTime(openTime)
+ " §7· §cSchließt §7um §e"
+ plugin.ticksToTime(closeTime)
+ " §7· §bDelay §e"
+ shotDelayTicks
+ "§7 Ticks §8("
+ formatShotDelaySeconds()
+ "s§8) §7· §dModus §e"
+ (isSequentialMode() ? "nacheinander" : "gleichzeitig"));
} 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,63 @@
# ButtonControl Konfiguration
# ── Maximale Anzahl verbundener Blöcke pro Controller ──────────────────────
max-doors: 20 max-doors: 20
max-lamps: 50 max-lamps: 50
max-noteblocks: 10
max-gates: 20
max-trapdoors: 20
max-bells: 5
max-dispensers: 20
max-droppers: 20
# ── 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
# Legacy-Fallback für ältere Zeitpläne ohne eigenen GUI-Delay-Wert
# (20 Ticks = 1 Sekunde)
timed-container-interval-ticks: 40
# Standardwert für die Zeit zwischen einzelnen Abschüssen im Zeitplan
# Wird verwendet, bis ein Controller in der GUI einen eigenen Wert speichert
timed-container-shot-delay-ticks: 2
# Standardmodus für Zeitplan-Werfer/Spender: simultaneous oder sequential
timed-container-trigger-mode: simultaneous
# ── 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,72 @@
doors-open: "§aTüren wurden geöffnet." # lang.yml ButtonControl Nachrichten
doors-closed: "§cTüren wurden geschlossen."
lamps-on: "§aLampen wurden eingeschaltet." # ── Holztüren ────────────────────────────────────────────────────────────────
lamps-off: cLampen wurden ausgeschaltet." tueren-geoeffnet: aTüren wurden geöffnet."
blocks-toggled: eBlöcke wurden umgeschaltet." tueren-geschlossen: cTüren wurden geschlossen."
no-blocks-connected: "§cKeine Blöcke sind verbunden." max-tueren-erreicht: "§cMaximale Anzahl an Türen erreicht."
max-doors-reached: "§cMaximale Anzahl Türen erreicht."
max-lamps-reached: "§cMaximale Anzahl Lampen erreicht." # ── Eisentüren (NEU) ─────────────────────────────────────────────────────────
block-connected: "§aBlock verbunden." eisentueren-geoeffnet: "§aEisentüren wurden geöffnet."
block-already-connected: "§cBlock ist bereits verbunden." eisentueren-geschlossen: "§cEisentüren wurden geschlossen."
controller-placed: "§aController platziert." eisenfallturen-geoeffnet: "§aEisen-Falltüren wurden geöffnet."
controller-removed: "§cController entfernt." 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."
creaking-heart-aktiviert: "§aKnarrherz wurde aktiviert."
creaking-heart-deaktiviert: "§cKnarrherz wurde deaktiviert."
gitter-geoeffnet: "§aGitter wurden geöffnet."
gitter-geschlossen: "§cGitter wurden geschlossen."
glocke-gelaeutet: "§aGlocke wurde geläutet."
max-glocken-erreicht: "§cMaximale Anzahl an Glocken erreicht."
spender-ausgeloest: "§aSpender wurden ausgelöst."
werfer-ausgeloest: "§aWerfer wurden ausgelöst."
max-spender-erreicht: "§cMaximale Anzahl an Spendern erreicht."
max-werfer-erreicht: "§cMaximale Anzahl an Werfern 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 name: ButtonControl
version: 1.0 version: 1.9
main: viper.ButtonControl main: viper.ButtonControl
api-version: 1.21 api-version: 1.21
author: viper author: M_Viper
description: Ein Plugin, um Türen und Redstone-Lampen mit einem Button zu steuern. 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: commands:
bc: bc:
description: Zeigt Informationen zum Plugin an description: Hauptbefehl für ButtonControl
usage: /<command> info usage: "/bc <info|reload|note|list|rename|schedule|trust|untrust|public|private|undo|secret> (Secret: select|info|add|remove|clear|delay|animation)"
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