Compare commits

..

90 Commits

Author SHA1 Message Date
814a6dc701 Adds POM relocations
All checks were successful
KnarCraft/PaidSigns/pipeline/head This commit looks good
2024-05-03 15:40:12 +02:00
4f3c27e4bd Bumps dev version
All checks were successful
KnarCraft/PaidSigns/pipeline/head This commit looks good
2023-06-25 16:42:14 +02:00
2bc4ab22f5 Bumps version for release
Some checks failed
EpicKnarvik97/PaidSigns/pipeline/head This commit looks good
KnarCraft/PaidSigns/pipeline/head There was a failure building this commit
2023-06-25 16:41:16 +02:00
396bcfc68b Adds proper support for dual-side paid signs 2023-06-24 15:07:03 +02:00
35a5e40eeb Bumps version for release
All checks were successful
EpicKnarvik97/PaidSigns/pipeline/head This commit looks good
2023-06-24 12:52:17 +02:00
a3cce66848 Bumps KnarLib version
All checks were successful
EpicKnarvik97/PaidSigns/pipeline/head This commit looks good
2023-06-24 12:10:24 +02:00
8f3af1f288 Bumps version to 1.0.2-SNAPSHOT
All checks were successful
EpicKnarvik97/PaidSigns/pipeline/head This commit looks good
2023-06-24 11:40:18 +02:00
d8aabeee88 Updates for 1.20
Some checks failed
EpicKnarvik97/PaidSigns/pipeline/head There was a failure building this commit
Makes sure to unregister and refund on the sign edit event, as the sign might be registered as a paid sign already.
Updates Spigot
Updates Vault repository
2023-06-23 20:34:20 +02:00
05898d7d65 Adds Jenkinsfile
All checks were successful
EpicKnarvik97/PaidSigns/pipeline/head This commit looks good
2022-11-26 15:25:18 +01:00
7355b7fc60 Uses Economy.format to display costs 2022-11-14 00:36:23 +00:00
8e3a71a489 Fixes Vault repo URL http instead of https 2022-11-13 22:48:18 +00:00
1154d7ddaf Fixes README table formatting 2022-11-13 22:42:52 +00:00
dd47ce06eb Makes sure forced refunds always refund the full amount 2022-11-13 22:39:11 +00:00
e7c40fb4b0 Makes a method to check if a block is a sign
This will hopefully prevent any more mistakes related to checking if something is a sign or not.
2022-11-12 12:48:21 +01:00
cf66113ac1 Fixes some problems in the plugin destroyed sign check 2022-11-12 01:27:46 +01:00
4fee628469 Adds a delayed sign removed check #13
This commit adds a delayed check for whether the block at the sign change event's location is still a sign. If it isn't, it's assumed that a plugin blocked the sign creation by destroying the sign. In this case, the player is immediately refunded, even if refunds are disabled.
2022-11-08 12:53:26 +01:00
5c095e79f6 Updates code to account for KnarLib changes 2022-11-07 22:26:22 +01:00
a9c97f71f9 Changes things for the non-static Translator 2022-11-07 15:21:02 +01:00
bb63fb59d1 Uses KnarLib where possible 2022-11-07 11:29:33 +01:00
6a7d106d31 Bumps version to 1.0.1 2022-11-05 04:55:31 +01:00
79f3f8e3d2 Saves tracked signs after loading
Previously, if a tracked sign was removed during loading, while it wasn't added to tracked signs, tracked signs wasn't saved, so on the next startup it would be loaded again.
2022-11-05 04:12:51 +01:00
d50d9b42c4 Delays refunding on startup to prevent a crash 2022-11-05 03:26:25 +01:00
b0c3fea730 Improves some README information 2022-10-20 11:50:54 +02:00
04cfd1b89e Fixes two small misses in plugin.yml and the README 2022-10-19 18:22:07 +02:00
8f866d44b6 Bumps version to 1.0.0 2022-10-19 17:58:35 +02:00
7dcb215bf3 Builds against 1.19.2 2022-10-09 12:56:23 +02:00
668eec66e2 Adds minor improvements to README text 2022-10-09 12:51:41 +02:00
fb9f127307 Fixes tab completion for paid sign names 2022-08-06 17:21:19 +02:00
3eee197fb1 Lists paid signs 7 at a time, instead of all at once 2022-08-05 14:58:54 +02:00
337c4573e0 Adds in-game command info, and fixes some permission bugs 2022-08-05 14:28:03 +02:00
e5dfc1ccf6 Builds against Spigot 1.19.1 2022-08-05 13:47:04 +02:00
645d8f30eb Updates POM URL to the PaidSigns repository 2022-08-05 13:21:15 +02:00
40da191b45 Updates API to 1.19, and version to 0.7.0-ALPHA 2022-07-28 17:08:21 +02:00
38717a6b91 Improves behavior when signs share conditions
Makes sure that the paid sign matching the most conditions is always chosen as the matching paid sign
Makes sure to always choose the most expensive paid sign if two or more paid signs with the same amount of conditions are matching.
Changes some classes to records to reduce some boilerplate code
2022-07-20 18:09:09 +02:00
32ec713994 Fixes another formatting issue 2022-07-15 20:30:50 +02:00
26e4202541 Fixes/improves some formatting 2022-07-15 12:08:08 +02:00
8b53182006 Adds a small FAQ to the README 2022-07-14 23:33:53 +02:00
427169c51a Lists registered paid signs in the sorted order 2022-07-14 23:33:36 +02:00
5ce0e5e6a7 Merge branch 'master' of https://git.knarcraft.net/EpicKnarvik97/PaidSigns
 Conflicts:
	pom.xml
2022-07-13 15:54:42 +02:00
35e0666a1d Performs some minor changes
Changes some warning messages to info
Displays sign creation cost when unable to create a paid sign
Changes spigot dependency to 1.19
Refunds the player during reload/startup when a tracked sign's physical sign has been destroyed
2022-07-13 15:53:28 +02:00
ba81611e80 Uses proper multi-line formatting for translation strings 2022-06-19 18:16:59 +02:00
6ec701026f Adds LICENCE and HEADER files 2022-04-29 06:10:09 +02:00
7344af616b Adds links in the README for easier navigation 2022-04-28 13:39:18 +02:00
0ce37de509 Changes the Spigot .jar to 1.18.2 2022-04-28 13:27:07 +02:00
3353916647 Updates the README to improve readability 2022-04-28 13:26:19 +02:00
b1aefdd9d9 Updates README and version to 0.6.0 2022-03-15 14:31:25 +01:00
c44ff5c890 Fixes a bug preventing default option states for paid sign conditions 2022-03-15 14:20:14 +01:00
c6d3a771c3 Fixes some bugs in the edit command
Adds missing super call to EditTabCompleter
Adds missing super call to EditCommand
Fixes the index of an argument during tab completion
Fixes a bug that caused conditions to be lost when a paid sign is changed
2022-03-14 20:20:04 +01:00
3e31c8c648 Implements the untested edit command 2022-03-14 19:00:51 +01:00
9c6921b4cd Performs some necessary work required for the edit command
Adds an implementation for the edit tab completer
Adds an enum representing a paid sign property
Adds an enum representing a paid sign condition property
Adds aliases to all commands
2022-03-14 16:20:42 +01:00
3b5218cb98 Adds a notice about payment exempt OP players to the README 2022-03-03 11:52:25 +01:00
a1c9624fe1 Updates version to 0.5.0-ALPHA 2022-03-03 11:34:09 +01:00
a7d1da7f8a Fixes a bug in refunding 2022-03-03 11:33:50 +01:00
c2ffe5e903 Adds increased detection and optional refunding of signs broken by non-players 2022-03-02 14:05:20 +01:00
3d83458b9c Makes refundPercentage use short instead of int 2022-03-02 13:02:34 +01:00
abb080b065 Updates plugin version to 0.4.0-ALPHA 2022-03-02 00:41:06 +01:00
9bb234169d Adds various improvements, fixes and a new feature
Adds and option to match any paid sign condition instead of all conditions
Adds checks for whether line indices are outside the allowed range
Disallows any invalid regular expressions in sign conditions
2022-03-02 00:37:00 +01:00
664115b2b4 Translates the remaining strings and fixes a few minor bugs 2022-03-01 18:45:53 +01:00
4602ca71db Translates the list command's messages 2022-03-01 16:56:12 +01:00
30bca7b8f1 Translates the add condition command's messages 2022-03-01 16:56:02 +01:00
81e62eb664 Translates the add command's messages 2022-03-01 16:55:50 +01:00
5cbde82fff Adds translations for a bunch of messages 2022-03-01 16:55:30 +01:00
eb0e06f193 Adds code to be able to translate plugin messages 2022-03-01 16:03:08 +01:00
483ffaec2b Makes sure signs are no longer tracked if they no longer exist 2022-02-28 15:35:58 +01:00
797020aa7f Updates version to 0.3.0-ALPHA 2022-02-28 15:14:59 +01:00
15426a46f3 Registers the block break listener and adds some small improvements 2022-02-28 15:06:58 +01:00
a1b1a5d112 Adds sign tracking and refunds #3 #8 2022-02-28 14:41:14 +01:00
996d062674 Adds additional information to the README 2022-02-27 15:49:25 +01:00
990339499a Fixes list paid signs tab completion 2022-02-27 14:51:37 +01:00
3b7268d2ff Fixes various tab completion problems 2022-02-27 14:28:46 +01:00
91d477b45a Fixes various bugs in commands 2022-02-27 12:47:05 +01:00
0f958f0908 Makes sure tab completed paid sign names are quoted 2022-02-26 23:15:48 +01:00
36678fd2d0 Fixes the index of the cost argument in the add command 2022-02-26 22:50:03 +01:00
4e44909f80 Fixes some errors caused when calling a command without an argument 2022-02-25 22:30:48 +01:00
0ad953cc14 Updates version to 0.2.0-ALPHA 2022-02-25 22:18:30 +01:00
db869c0c5b Adds a tab completer for the list command 2022-02-25 22:16:11 +01:00
8a030276c5 Removes some redundancy between tab completers 2022-02-25 20:38:10 +01:00
7e01d77723 Finishes the implementation of the list command 2022-02-25 19:41:43 +01:00
86cb1c0fed Removes some redundancy between commands 2022-02-25 18:18:52 +01:00
5bc62d5bc0 Adds some TODOs 2022-02-19 18:29:38 +01:00
2f559ce2af Implements the remove condition command and its tab completer 2022-02-19 18:22:39 +01:00
90b5ff7304 Implements the add condition command and its tab completion 2022-02-19 17:51:27 +01:00
d76d5cdf93 Fixes tab completion for the add and remove commands #7 2022-02-19 00:00:50 +01:00
6f35da03e7 Makes some changes in preparation for some commands that need to be added
Prepares the new command layout, changes the .add permission to .manage and adds 4 empty command classes
2022-02-18 23:16:47 +01:00
eaa4f929ca Fixes the check for testing if a paid sign already exists 2022-02-18 21:04:23 +01:00
419a79bc9f Updates the plugin description and the add tab completer 2022-02-18 19:59:50 +01:00
16faa1ddb2 Rewrites a bunch of code to improve the structure as specified in #5, #4, #2, #1 2022-02-18 19:39:20 +01:00
d2f152334f Updates README with commands 2022-02-18 01:37:47 +01:00
4189053ed8 Fixes the usage of /removepaidsign 2022-02-18 01:37:35 +01:00
5e52e3d4de Changes version to make sure this is treated as an alpha version 2022-02-18 00:58:45 +01:00
43 changed files with 3682 additions and 448 deletions

15
HEADER Normal file
View File

@ -0,0 +1,15 @@
PaidSigns - A sign payment plugin for Spigot
Copyright (C) 2022 Kristian Knarvik (EpicKnarvik97)
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 <http://www.gnu.org/licenses/>.

33
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,33 @@
pipeline {
agent any
tools {
jdk 'JDK17'
}
stages {
stage('Build') {
steps {
echo 'Building...'
sh 'mvn clean & mvn validate & mvn compile'
}
}
stage('Test') {
steps {
echo 'Testing...'
sh 'mvn test'
}
}
stage('Verify') {
steps {
echo 'Verifying...'
sh 'mvn verify -Dmaven.test.skip=true'
}
}
stage('Deploy') {
steps {
echo 'Deploying...'
sh 'mvn deploy -Dmaven.install.skip=true -Dmaven.test.skip=true'
archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
}
}
}
}

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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:
<program> Copyright (C) <year> <name of author>
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/licenses/why-not-lgpl.html>.

183
README.md
View File

@ -1,12 +1,187 @@
# Paid signs # Paid signs
The paid-signs plugin is a plugin that lets you add costs for creating plugin signs. This allows restricting usage of The paid-signs plugin is a plugin that lets you add costs for creating plugin signs. This allows restricting usage of
signs such as CraftBook's gate/lift and other signs while still allowing every player to use the signs. signs such as CraftBook's gate/lift and other signs while still allowing every player to create the signs.
Note: OP players, and players with the '*' permission will have the `paidsigns.paymentexempt` permission and will not
see any payment messages. For testing, you'll need to un-set the `paidsigns.paymentexempt` permission.
## Limitations ## Limitations
As this plugin only listens to sign change events, there are some limitations: As this plugin only listens to sign change events, there are some limitations:
1. The plugin is not aware of whether the creation of a sign is successful 1. The plugin is not aware of whether the creation of a sign is successful. Setting the appropriate permission might
2. It is assumed that any protection plugins run before this plugin, but it's not guaranteed help, but it's not guaranteed
3. Plugins changing the lines on signs when successful might create confusion and mismatches 2. It is assumed that any protection plugins run before this plugin checks a sign, but it's not guaranteed
3. Plugins changing the lines on signs when successful might create confusion and mismatches if changes happen before
this plugin checks a sign
## FAQ
### A player placed down a sign, but some plugin broke it afterwards. What do I do?
There are three ways this can be solved:
1. Make the player (or anyone else) place an empty sign in the same location and break it again. The player should be
refunded as long as refunds are enabled.
2. Reload PaidSigns. The player should be refunded as long as refunds are enabled.
3. Restart the server. The player should be refunded as long as refunds are enabled. Never place a new paid sign in the
same location in this situation, as the old sign will be overwritten, and the ability to refund the player
automatically will be lost.
### Players have to pay for signs, even if the plugin sign isn't created
This is a known limitation of this plugin. As long as this plugin is agnostic to the plugin signs it's used for, it
cannot really know if a plugin sign has been created. The best you can do is finding the permission required for the
plugin sign and specifying it when creating the paid sign. It can be assumed that the player is able to create the
plugin sign, if the player has the correct permission.
### How can this plugin be used to enhance a server?
This plugin is known to be helpful with two tasks:
1. Allow normal players to create plugin signs, such as CraftBook's elevator signs, but at a cost. This can be done by
simply specifying the exact text required for the plugin sign in the paid sign conditions.
2. Add a swearing jar/swearing filter to the server. This can be done with a paid sign matching ANY condition, where the
list of swear words is given as a regEx. The same regEx should be added for each of the four sign lines.
## Custom parser
Before anything else, it's important to note that as an experiment, I used a custom command-parser for this plugin. This
means that sign names can contain spaces, as long as the name is in quotes. In most cases, this actually works fine,
even though it differs a lot from the default behavior, but if you tab-complete the first word in the name first, and
then try to tab-complete the rest, tab-completion will fill in the second world with the entire name, which is
unfortunate. That's really just how tab-completion works, and not something I think I can do anything with.
## Commands
An argument marked by "<>" is required to execute the command. An argument marked by "[]" is optional. For empty
arguments, such as no paid sign permission, you should use empty quotes ("").
| Command | Arguments | Permission | Description |
|------------------------------------------------------|---------------------------------------------------------------------------------------------------------|------------------|---------------------------------------------------------------------------|
| /paidsigns | | paidsigns.info | Used to display in-game information about all other commands |
| [/addpaidsign](#addpaidsign) | \<name> \<cost> \[permission] \[ignore case] \[ignore color] \[match any condition] | paidsigns.manage | Used to add a new paid sign |
| [/addpaidsigncondition](#addpaidsigncondition) | \<name (of a paid sign)> \<line number> \<string to match> \[executeRegEx] \[ignoreCase] \[ignoreColor] | paidsigns.manage | Used to add a condition to a paid sign |
| [/listpaidsigns](#listpaidsigns) | \[name (of a paid sign)] \[line number] | paidsigns.manage | Used to list registered paid signs or a registered paid sign's conditions |
| [/editpaidsign](#editpaidsign) | \<sign name> \<property>/\<line number> \[new value]/\<property> \[new value] | paidsigns.manage | Used to modify a registered paid sign or one of its conditions |
| [/removepaidsigncondition](#removepaidsigncondition) | \<name (of a paid sign)> \<line number> | paidsigns.manage | Used to remove a condition from a registered paid sign |
| [/removepaidsign](#removepaidsign) | \<name (of a paid sign)> | paidsigns.manage | Used to remove a registered paid sign |
| /reload | | paidsigns.reload | Used to reload the configuration file |
## Command explanation
### /addpaidsign
This command adds a new paid sign that does nothing until a condition is added.
`/addpaidsign <name> <cost> [permission] [ignore case] [ignore color] [match any condition]`
| Argument | Usage |
|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | A recognizable name only used to differentiate between registered paid signs |
| cost | The cost a player needs to pay to create any sign matching the paid sign |
| permission | If the paid sign is used to represent a plugin sign, the permission should be the permission necessary for creating the plugin sign. This is used to decide if the plugin sign was created, or if the player was denied. |
| ignore case | Whether any conditions of the paid sign should ignore case by default, when matching against text (default uses the config file value). |
| ignore color | Whether any condition of the paid sign should ignore color by default, when matching against text (default uses the config file value). |
| match any condition | Whether to trigger a paid sign match if a single one of the sign's conditions is true. This is mainly useful if several lines may contain the match string, or if trying to match a word. |
### /addpaidsigncondition
This adds a condition to a paid sign which is used to decide if a sign created by a player matches the paid sign. Adding
a paid sign condition to a line that already has one will replace the previous condition.
`/addpaidsigncondition <name (of a paid sign)> <line number> <string to match> [executeRegEx] [ignoreCase] [ignoreColor]`
| Argument | Usage |
|-----------------|---------------------------------------------------------------------------------------------------------------------|
| name | The name of the paid sign to add the condition to |
| line number | The line on the sign (1-4) to search for any matches |
| string to match | The string or regular expression to look for on a sign |
| executeRegEx | Whether to use a regular expression match instead of looking for the exact string |
| ignoreCase | Whether this condition should ignore case when trying to match the string (default uses the "parent" sign's value) |
| ignoreColor | Whether this condition should ignore color when trying to match the string (default uses the "parent" sign's value) |
### /listpaidsigns
This lists registered paid signs and paid sign conditions. No arguments will print a list of paid signs
`/listpaidsigns [page number]/[name (of a paid sign)] [line number]`
| Argument | Usage |
|-------------|---------------------------------------------------------------------------------|
| page number | Paid signs are listed 7 at a time, so the page number is used to see the next 7 |
| name | The name of the paid sign to see information about |
| line number | The line number of the condition to see information about |
### /editpaidsign
This command changes a property of a paid sign or a paid sign condition
`/editpaidsign <sign name> <property>/<line number> [new value]/<property> [new value]`
| Argument | Usage |
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | The name of the paid sign to edit |
| property / line number | The property to edit for the sign (name, cost, permission, ignoreCase, ignoreColor, matchAnyCondition), or the line of the condition to edit |
| new value / property | The new property value if a property was specified in the second argument, or a condition property (stringToMatch, executeRegEx, ignoreCase, ignoreColor) if a line number was specified in the second argument |
| new value | The new property value of the condition property specified in the third argument |
### /removepaidsigncondition
Removes a paid sign condition from a sign
`/removepaidsigncondition <name (of a paid sign)> <line number>`
| Argument | Usage |
|-------------|--------------------------------------------------------|
| name | The name of the paid sign to remove the condition from |
| line number | The line the condition is associated with |
### /removepaidsign
Removes a registered paid sign
`/removepaidsign <name (of a paid sign)>`
| Argument | Usage |
|----------|-------------------------------------|
| name | The name of the paid sign to remove |
## Permissions
| Node | Description |
|---------------------------|------------------------------------------------------|
| paidsigns.* | Grants all paid signs permissions |
| --paidsigns.manage | Grants the permission to add/remove a paid sign |
| --paidsigns.reload | Grants the permissions to reload the plugin |
| --paidsigns.paymentexempt | Makes this player exempt from the cost of paid signs |
## Configuration options
| Option | Description |
|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| language | The language to use for all messages displayed to players. Currently, only "en" is valid. |
| ignoreCase | Whether to ignore the case (lowercase/uppercase) of the paid sign text. The option can be set on a per-sign basis, but this value is used if not specified. The correct value depends on whether the plugin signs it should match are case-sensitive or not. |
| ignoreColor | Whether to ignore any color or formatting applied to the text when trying to match a paid sign's text. The option can be set on a per-sign basis, but this value is used if not specified. The correct value depends on whether the plugin signs it should match allow coloring or not. |
| refundsEnabled | Whether to enable refunds to the sign creator when a sign detected as a paid sign is broken (payment will always go to the original creator) |
| refundPercentage | The percentage of the paid sign cost to refund (0-100) |
| refundAlways | Whether to refund when signs that players have paid for are broken by anything. This includes tnt, creepers, pistons and similar |
## Language customization
All strings, even time units, are customizable. If you place a strings.yml file in the plugin folder, it will take
priority over built-in languages. If you want to change strings, look
at [PaidSigns/src/main/resources/strings.yml](https://git.knarcraft.net/EpicKnarvik97/PaidSigns/src/branch/master/src/main/resources/strings.yml)
for the proper keys. All strings have the format: ENUM: "Displayed string". The enum must be identical as it defines
which string you have changed. All strings belonging to a language are beneath the language code and indented with two
spaces.
The easiest way to add a new language is to copy an existing language and paste it into your custom strings.yml and
change strings as necessary. If you don't include all strings, the remaining will use the built-in English translation.
Remember to change the language code to whichever you use for your custom language.
## License
PaidSigns is licensed under the GNU Public License Version 3.0. This includes every source and resource file. See the
HEADER file for a more detailed license description.

74
pom.xml
View File

@ -6,17 +6,17 @@
<groupId>net.knarcraft</groupId> <groupId>net.knarcraft</groupId>
<artifactId>paidsigns</artifactId> <artifactId>paidsigns</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0.4-SNAPSHOT</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Paid Signs</name> <name>Paid Signs</name>
<description>Add costs for creating plugin signs</description> <description>Add costs for creating plugin signs</description>
<properties> <properties>
<java.version>17</java.version> <java.version>16</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<url>https://git.knarcraft.net</url> <url>https://git.knarcraft.net/EpicKnarvik97/PaidSigns</url>
<build> <build>
<plugins> <plugins>
@ -25,14 +25,14 @@
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version> <version>3.8.1</version>
<configuration> <configuration>
<source>16</source> <source>${java.version}</source>
<target>16</target> <target>${java.version}</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version> <version>3.3.0</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
@ -41,6 +41,36 @@
</goals> </goals>
<configuration> <configuration>
<createDependencyReducedPom>false</createDependencyReducedPom> <createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>net.knarcraft.knarlib</pattern>
<shadedPattern>net.knarcraft.paidsigns.lib.knarlib</shadedPattern>
</relocation>
<relocation>
<pattern>org.jetbrains.annotations</pattern>
<shadedPattern>net.knarcraft.paidsigns.lib.annotations</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>net.knarcraft:knarlib</artifact>
<includes>
<include>net/knarcraft/knarlib/**</include>
</includes>
</filter>
<filter>
<artifact>org.jetbrains:annotations</artifact>
<includes>
<include>org/jetbrains/annotations/**</include>
</includes>
</filter>
<filter>
<excludes>
<exclude>*.MF</exclude>
<exclude>*.yml</exclude>
</excludes>
</filter>
</filters>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
@ -55,6 +85,10 @@
</build> </build>
<repositories> <repositories>
<repository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</repository>
<repository> <repository>
<id>spigotmc-repo</id> <id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url> <url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
@ -64,28 +98,44 @@
<url>https://oss.sonatype.org/content/groups/public/</url> <url>https://oss.sonatype.org/content/groups/public/</url>
</repository> </repository>
<repository> <repository>
<id>vault-repo</id> <id>jitpack.io</id>
<url>http://nexus.hc.to/content/repositories/pub_releases</url> <url>https://jitpack.io</url>
</repository> </repository>
</repositories> </repositories>
<distributionManagement>
<repository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</repository>
<snapshotRepository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</snapshotRepository>
</distributionManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId> <artifactId>spigot-api</artifactId>
<version>1.18.1-R0.1-SNAPSHOT</version> <version>1.20.1-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.milkbowl.vault</groupId> <groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId> <artifactId>VaultAPI</artifactId>
<version>1.7</version> <version>1.7.1</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains</groupId> <groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId> <artifactId>annotations</artifactId>
<version>22.0.0</version> <version>24.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.knarcraft</groupId>
<artifactId>knarlib</artifactId>
<version>1.1</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -1,15 +1,31 @@
package net.knarcraft.paidsigns; package net.knarcraft.paidsigns;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.formatting.Translator;
import net.knarcraft.knarlib.util.UpdateChecker;
import net.knarcraft.paidsigns.command.AddCommand; import net.knarcraft.paidsigns.command.AddCommand;
import net.knarcraft.paidsigns.command.AddConditionCommand;
import net.knarcraft.paidsigns.command.AddConditionTabCompleter;
import net.knarcraft.paidsigns.command.AddTabCompleter; import net.knarcraft.paidsigns.command.AddTabCompleter;
import net.knarcraft.paidsigns.command.EditCommand;
import net.knarcraft.paidsigns.command.EditTabCompleter;
import net.knarcraft.paidsigns.command.ListCommand;
import net.knarcraft.paidsigns.command.ListTabCompleter;
import net.knarcraft.paidsigns.command.PaidSignsTabCommand;
import net.knarcraft.paidsigns.command.ReloadTabCommand; import net.knarcraft.paidsigns.command.ReloadTabCommand;
import net.knarcraft.paidsigns.command.RemoveCommand; import net.knarcraft.paidsigns.command.RemoveConditionCommand;
import net.knarcraft.paidsigns.command.RemoveTabCompleter; import net.knarcraft.paidsigns.command.RemoveConditionTabCompleter;
import net.knarcraft.paidsigns.command.RemoveTabCommand;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.listener.SignBreakListener;
import net.knarcraft.paidsigns.listener.SignListener; import net.knarcraft.paidsigns.listener.SignListener;
import net.knarcraft.paidsigns.manager.EconomyManager; import net.knarcraft.paidsigns.manager.EconomyManager;
import net.knarcraft.paidsigns.manager.PaidSignManager; import net.knarcraft.paidsigns.manager.PaidSignManager;
import net.knarcraft.paidsigns.manager.TrackedSignManager;
import net.milkbowl.vault.economy.Economy; import net.milkbowl.vault.economy.Economy;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
@ -23,9 +39,15 @@ import org.bukkit.plugin.java.JavaPlugin;
public final class PaidSigns extends JavaPlugin { public final class PaidSigns extends JavaPlugin {
private static PaidSigns paidSigns; private static PaidSigns paidSigns;
private static Translator translator;
private static StringFormatter stringFormatter;
private PaidSignManager signManager; private PaidSignManager signManager;
private String language;
private boolean ignoreCase; private boolean ignoreCase;
private boolean ignoreColor; private boolean ignoreColor;
private boolean refundsEnabled;
private short refundPercentage;
private boolean refundAlways;
/** /**
* Instantiates a new paid signs object * Instantiates a new paid signs object
@ -44,16 +66,46 @@ public final class PaidSigns extends JavaPlugin {
return paidSigns; return paidSigns;
} }
/**
* Gets the translator to use for this plugin
*
* @return <p>The translator to use for this plugin</p>
*/
public static Translator getTranslator() {
return translator;
}
/**
* Gets the string formatter to use for this plugin
*
* @return <p>The string formatter to use for this plugin</p>
*/
public static StringFormatter getStringFormatter() {
return stringFormatter;
}
@Override @Override
public void onEnable() { public void onEnable() {
setupVault(); setupVault();
signManager = new PaidSignManager(PaidSignManager.loadSigns());
loadConfig(); loadConfig();
//Initialize translator
translator = new Translator();
translator.registerMessageCategory(PaidSignsTranslatableMessage.BOOLEAN_TRUE);
translator.loadLanguages(this.getDataFolder(), "en", language);
stringFormatter = new StringFormatter(this.getDescription().getPrefix(), translator);
signManager = new PaidSignManager(PaidSignManager.loadSigns());
TrackedSignManager.loadTrackedSigns();
PluginManager pluginManager = getServer().getPluginManager(); PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new SignListener(), this); pluginManager.registerEvents(new SignListener(), this);
pluginManager.registerEvents(new SignBreakListener(), this);
registerCommands(); registerCommands();
UpdateChecker.checkForUpdate(this, "https://api.spigotmc.org/legacy/update.php?resource=105842",
() -> this.getDescription().getVersion(), null);
} }
@Override @Override
@ -66,7 +118,9 @@ public final class PaidSigns extends JavaPlugin {
public void reload() { public void reload() {
this.reloadConfig(); this.reloadConfig();
loadConfig(); loadConfig();
translator.loadLanguages(this.getDataFolder(), "en", language);
signManager = new PaidSignManager(PaidSignManager.loadSigns()); signManager = new PaidSignManager(PaidSignManager.loadSigns());
TrackedSignManager.loadTrackedSigns();
} }
/** /**
@ -96,27 +150,69 @@ public final class PaidSigns extends JavaPlugin {
return this.ignoreColor; return this.ignoreColor;
} }
/**
* Checks whether refunds are currently enabled
*
* @return <p>Whether refunds are currently enabled</p>
*/
public boolean areRefundsEnabled() {
return this.refundsEnabled;
}
/**
* Gets the percentage of the initial cost to refund the sign creator
*
* @return <p>The percentage of the cost to refund</p>
*/
public short getRefundPercentage() {
if (this.refundPercentage < 0) {
return 0;
} else if (refundPercentage > 100) {
return 100;
}
return this.refundPercentage;
}
/**
* Gets whether refunds should always happen, even if signs are not broken by players
*
* @return <p>True if refunds should always happen</p>
*/
public boolean refundAlways() {
return this.refundAlways;
}
/** /**
* Registers the commands used by this plugin * Registers the commands used by this plugin
*/ */
private void registerCommands() { private void registerCommands() {
PluginCommand addCommand = this.getCommand("addPaidSign"); TabExecutor paidSignsExecutor = new PaidSignsTabCommand();
if (addCommand != null) { registerCommand("paidSigns", paidSignsExecutor, paidSignsExecutor);
addCommand.setExecutor(new AddCommand()); registerCommand("addPaidSign", new AddCommand(), new AddTabCompleter());
addCommand.setTabCompleter(new AddTabCompleter()); registerCommand("listPaidSigns", new ListCommand(), new ListTabCompleter());
} registerCommand("addPaidSignCondition", new AddConditionCommand(), new AddConditionTabCompleter());
registerCommand("removePaidSignCondition", new RemoveConditionCommand(),
new RemoveConditionTabCompleter());
registerCommand("editPaidSign", new EditCommand(), new EditTabCompleter());
PluginCommand removeCommand = this.getCommand("removePaidSign"); TabExecutor removeTabExecutor = new RemoveTabCommand();
if (removeCommand != null) { registerCommand("removePaidSign", removeTabExecutor, removeTabExecutor);
removeCommand.setExecutor(new RemoveCommand());
removeCommand.setTabCompleter(new RemoveTabCompleter());
}
PluginCommand reloadCommand = this.getCommand("reload");
if (reloadCommand != null) {
TabExecutor reloadTabExecutor = new ReloadTabCommand(); TabExecutor reloadTabExecutor = new ReloadTabCommand();
reloadCommand.setExecutor(reloadTabExecutor); registerCommand("reload", reloadTabExecutor, reloadTabExecutor);
reloadCommand.setTabCompleter(reloadTabExecutor); }
/**
* Registers a command if possible
*
* @param command <p>The command to register</p>
* @param commandExecutor <p>The command executor for executing the command</p>
* @param tabCompleter <p>The tab completer for tab-completing the command</p>
*/
private void registerCommand(String command, CommandExecutor commandExecutor, TabCompleter tabCompleter) {
PluginCommand pluginCommand = this.getCommand(command);
if (pluginCommand != null) {
pluginCommand.setExecutor(commandExecutor);
pluginCommand.setTabCompleter(tabCompleter);
} }
} }
@ -127,8 +223,13 @@ public final class PaidSigns extends JavaPlugin {
FileConfiguration config = this.getConfig(); FileConfiguration config = this.getConfig();
config.options().copyDefaults(true); config.options().copyDefaults(true);
this.saveDefaultConfig(); this.saveDefaultConfig();
this.saveConfig();
language = config.getString("language", "en");
ignoreCase = config.getBoolean("ignoreCase", true); ignoreCase = config.getBoolean("ignoreCase", true);
ignoreColor = config.getBoolean("ignoreColor", false); ignoreColor = config.getBoolean("ignoreColor", false);
refundsEnabled = config.getBoolean("refundsEnabled", true);
refundPercentage = (short) config.getInt("refundPercentage", 100);
refundAlways = config.getBoolean("refundAlways", false);
} }
/** /**
@ -140,7 +241,7 @@ public final class PaidSigns extends JavaPlugin {
if (economyProvider != null) { if (economyProvider != null) {
EconomyManager.initialize(economyProvider.getProvider()); EconomyManager.initialize(economyProvider.getProvider());
} else { } else {
throw new IllegalStateException("[PaidSigns] Error: Vault could not be loaded"); throw new IllegalStateException("Error: Vault could not be loaded");
} }
} }

View File

@ -2,73 +2,86 @@ package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns; import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign; import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.manager.PaidSignManager; import net.knarcraft.paidsigns.manager.PaidSignManager;
import net.knarcraft.paidsigns.property.OptionState; import net.knarcraft.paidsigns.property.OptionState;
import net.knarcraft.paidsigns.utility.Tokenizer;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/** /**
* A representation of the command for adding a new paid sign * A representation of the command for adding a new paid sign
*/ */
public class AddCommand implements CommandExecutor { public class AddCommand extends TokenizedCommand {
@Override @Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) { @NotNull String[] args) {
if (args.length < 3) { super.onCommand(sender, command, label, args);
if (argumentSize < 2) {
return false; return false;
} }
PaidSignManager manager = PaidSigns.getInstance().getSignManager();
List<String> arguments = Tokenizer.tokenize(String.join(" ", args));
String id = arguments.get(0); String signName = arguments.get(0).trim();
short line;
double cost; double cost;
try { try {
line = (short) (Short.parseShort(arguments.get(1)) - 1); cost = Double.parseDouble(arguments.get(1));
cost = Double.parseDouble(arguments.get(2));
} catch (NumberFormatException exception) { } catch (NumberFormatException exception) {
sender.sendMessage("You provided an invalid number"); PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_INVALID_NUMBER);
return false; return false;
} }
String permission = "";
if (argumentSize > 2) {
permission = arguments.get(2);
}
OptionState ignoreCase = OptionState.DEFAULT; OptionState ignoreCase = OptionState.DEFAULT;
OptionState ignoreColor = OptionState.DEFAULT; OptionState ignoreColor = OptionState.DEFAULT;
if (arguments.size() > 3) { if (argumentSize > 3) {
ignoreCase = OptionState.fromString(arguments.get(3)); ignoreCase = OptionState.fromString(arguments.get(3));
} }
if (arguments.size() > 4) { if (argumentSize > 4) {
ignoreColor = OptionState.fromString(arguments.get(4)); ignoreColor = OptionState.fromString(arguments.get(4));
} }
boolean matchAnyCondition = false;
try { if (argumentSize > 5) {
PaidSign sign = new PaidSign(id, line, cost, ignoreCase, ignoreColor); matchAnyCondition = Boolean.parseBoolean(arguments.get(5));
for (PaidSign similarSign : manager.getPaidSigns(sign.getCleanId(), sign.getLineIndex())) {
if (sign.matches(similarSign)) {
sender.sendMessage("A paid sign with the same id and line already exists");
return false;
} }
return createPaidSign(sender, signName, cost, permission, ignoreCase, ignoreColor, matchAnyCondition);
}
/**
* Creates a new paid sign with the given user input
*
* @param sender <p>The command sender that called the add command</p>
* @param signName <p>The name of the new paid sign</p>
* @param cost <p>The cost of the new paid sign</p>
* @param permission <p>The permission required for creating the sign represented by the paid sign</p>
* @param ignoreCase <p>Whether to ignore case for the paid sign's conditions</p>
* @param ignoreColor <p>Whether to ignore color for the paid sign's conditions</p>
* @param matchAnyCondition <p>Whether to treat any matching condition as a sign match</p>
* @return <p>True if the paid sign was successfully created and registered</p>
*/
private boolean createPaidSign(CommandSender sender, String signName, double cost, String permission,
OptionState ignoreCase, OptionState ignoreColor, boolean matchAnyCondition) {
PaidSignManager manager = PaidSigns.getInstance().getSignManager();
try {
PaidSign sign = new PaidSign(signName, cost, permission, ignoreCase, ignoreColor, matchAnyCondition);
if (manager.getPaidSign(signName) != null) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_NAME_DUPLICATE);
return false;
} }
manager.addPaidSign(sign); manager.addPaidSign(sign);
sender.sendMessage("Successfully added new paid sign"); PaidSigns.getStringFormatter().displaySuccessMessage(sender, PaidSignsTranslatableMessage.SUCCESS_ADDED_PAID_SIGN);
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Logger logger = PaidSigns.getInstance().getLogger(); PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_EXCEPTION_OCCURRED);
logger.log(Level.SEVERE, "Exception encountered while trying to write " +
"to the data file");
logger.log(Level.SEVERE, Arrays.toString(e.getStackTrace()));
sender.sendMessage("An exception occurred. Please notify the server administrator or check the server log.");
return false; return false;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
sender.sendMessage("Invalid input: " + e.getMessage()); PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSigns.getStringFormatter().replacePlaceholder(
PaidSignsTranslatableMessage.ERROR_INVALID_INPUT, "{input}", e.getMessage()));
} }
return false; return false;
} }

View File

@ -0,0 +1,89 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.manager.PaidSignManager;
import net.knarcraft.paidsigns.property.OptionState;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
/**
* A representation of the command for adding a new match condition for a sign
*/
public class AddConditionCommand extends TokenizedCommand {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
super.onCommand(sender, command, label, args);
if (argumentSize < 3) {
return false;
}
String name = arguments.get(0);
short lineNumber;
try {
lineNumber = (short) (Short.parseShort(arguments.get(1)) - 1);
if (lineNumber < 0 || lineNumber > 3) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_INVALID_NUMBER);
return false;
}
} catch (NumberFormatException exception) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_INVALID_NUMBER);
return false;
}
String stringToMatch = arguments.get(2);
boolean executeRegEx = false;
if (argumentSize > 3) {
executeRegEx = Boolean.parseBoolean(arguments.get(3));
if (executeRegEx && isRegExInvalid(sender, stringToMatch)) {
return false;
}
}
OptionState ignoreCase = OptionState.DEFAULT;
if (argumentSize > 4) {
ignoreCase = OptionState.getFromBoolean(Boolean.parseBoolean(arguments.get(4)));
}
OptionState ignoreColor = OptionState.DEFAULT;
if (argumentSize > 5) {
ignoreColor = OptionState.getFromBoolean(Boolean.parseBoolean(arguments.get(5)));
}
return addCondition(name, lineNumber, stringToMatch, executeRegEx, ignoreCase, ignoreColor, sender);
}
/**
* Uses the given input to add a paid sign condition
*
* @param name <p>The name of the paid sign to add the condition to</p>
* @param lineNumber <p>The sign line the condition should check</p>
* @param stringToMatch <p>The string to look for on created signs</p>
* @param executeRegEx <p>Whether to treat the match string as a regular expression</p>
* @param ignoreCase <p>Whether to ignore case when matching</p>
* @param ignoreColor <p>Whether to ignore color when matching</p>
* @param sender <p>The command sender to notify when finished</p>
* @return <p>True if the condition was successfully added</p>
*/
private boolean addCondition(String name, short lineNumber, String stringToMatch, boolean executeRegEx,
OptionState ignoreCase, OptionState ignoreColor, CommandSender sender) {
PaidSignManager signManager = PaidSigns.getInstance().getSignManager();
PaidSign sign = signManager.getPaidSign(name);
if (sign == null) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND);
return false;
}
sign.addCondition(lineNumber, stringToMatch, executeRegEx, ignoreCase, ignoreColor);
try {
signManager.saveSigns();
} catch (IOException e) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_EXCEPTION_OCCURRED);
return false;
}
PaidSigns.getStringFormatter().displaySuccessMessage(sender, PaidSignsTranslatableMessage.SUCCESS_ADDED_PAID_SIGN_CONDITION);
return true;
}
}

View File

@ -0,0 +1,57 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import net.knarcraft.paidsigns.utility.PaidSignsTabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The tab completer for the add paid sign condition command
*/
public class AddConditionTabCompleter extends TokenizedTabCompleter {
private List<String> lineIndices;
private List<String> stringsToMatch;
private List<String> booleans;
private List<String> optionStates;
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
super.onTabComplete(sender, command, alias, args);
if (lineIndices == null) {
initializeValues();
}
if (argumentSize == 1) {
return TabCompletionHelper.filterMatchingStartsWith(PaidSignsTabCompleteHelper.getPaidSignNames(), arguments.get(0));
} else if (argumentSize == 2) {
return TabCompletionHelper.filterMatchingStartsWith(this.lineIndices, arguments.get(1));
} else if (argumentSize == 3) {
return TabCompletionHelper.filterMatchingStartsWith(stringsToMatch, arguments.get(2));
} else if (argumentSize == 4) {
return TabCompletionHelper.filterMatchingStartsWith(booleans, arguments.get(3));
} else if (argumentSize == 5) {
return TabCompletionHelper.filterMatchingStartsWith(optionStates, arguments.get(4));
} else if (argumentSize == 6) {
return TabCompletionHelper.filterMatchingStartsWith(optionStates, arguments.get(5));
}
return new ArrayList<>();
}
/**
* Initializes the values available for tab completion
*/
private void initializeValues() {
lineIndices = PaidSignsTabCompleteHelper.getSignLines();
stringsToMatch = PaidSignsTabCompleteHelper.getExampleConditionStrings();
booleans = PaidSignsTabCompleteHelper.getBooleans();
optionStates = PaidSignsTabCompleteHelper.getOptionStates();
}
}

View File

@ -1,9 +1,9 @@
package net.knarcraft.paidsigns.command; package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.utility.Tokenizer; import net.knarcraft.knarlib.util.TabCompletionHelper;
import net.knarcraft.paidsigns.utility.PaidSignsTabCompleteHelper;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -13,30 +13,34 @@ import java.util.List;
/** /**
* The tab completer for the add paid sign command * The tab completer for the add paid sign command
*/ */
public class AddTabCompleter implements TabCompleter { public class AddTabCompleter extends TokenizedTabCompleter {
private static List<String> ids; private static List<String> names;
private static List<String> lines;
private static List<String> costs; private static List<String> costs;
private static List<String> options; private static List<String> options;
private static List<String> booleans;
@Nullable @Nullable
@Override @Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) { @NotNull String[] args) {
if (ids == null) { super.onTabComplete(sender, command, alias, args);
if (names == null) {
initializeValues(); initializeValues();
} }
List<String> arguments = Tokenizer.tokenize(String.join(" ", args));
if (arguments.size() < 1) { if (argumentSize == 1) {
return ids; return TabCompletionHelper.filterMatchingStartsWith(names, arguments.get(0));
} else if (arguments.size() < 2) { } else if (argumentSize == 2) {
return lines; return TabCompletionHelper.filterMatchingStartsWith(costs, arguments.get(1));
} else if (arguments.size() < 3) { } else if (argumentSize == 3) {
return costs; return PaidSignsTabCompleteHelper.tabCompletePermission(arguments.get(arguments.size() - 1));
} else if (arguments.size() < 5) { } else if (argumentSize == 4) {
return options; return TabCompletionHelper.filterMatchingStartsWith(options, arguments.get(3));
} else if (argumentSize == 5) {
return TabCompletionHelper.filterMatchingStartsWith(options, arguments.get(4));
} else if (argumentSize == 6) {
return TabCompletionHelper.filterMatchingStartsWith(booleans, arguments.get(5));
} }
return new ArrayList<>(); return new ArrayList<>();
} }
@ -45,27 +49,10 @@ public class AddTabCompleter implements TabCompleter {
* Initializes the values available for tab completion * Initializes the values available for tab completion
*/ */
private static void initializeValues() { private static void initializeValues() {
ids = new ArrayList<>(); names = PaidSignsTabCompleteHelper.getExamplePaidSignNames();
ids.add("[Gate]"); costs = PaidSignsTabCompleteHelper.getCosts();
ids.add("\"[Lift Up]\""); options = PaidSignsTabCompleteHelper.getOptionStates();
ids.add("\"[Lift Down]\""); booleans = PaidSignsTabCompleteHelper.getBooleans();
lines = new ArrayList<>();
lines.add("1");
lines.add("2");
lines.add("3");
lines.add("4");
costs = new ArrayList<>();
costs.add("1");
costs.add("5");
costs.add("10");
costs.add("15");
options = new ArrayList<>();
options.add("default");
options.add("true");
options.add("false");
} }
} }

View File

@ -0,0 +1,182 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.container.PaidSignCondition;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.manager.PaidSignManager;
import net.knarcraft.paidsigns.property.OptionState;
import net.knarcraft.paidsigns.property.PaidSignConditionProperty;
import net.knarcraft.paidsigns.property.PaidSignProperty;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Map;
/**
* A representation of the command for editing a new paid sign
*/
public class EditCommand extends TokenizedCommand {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
super.onCommand(sender, command, label, args);
if (argumentSize < 3) {
return false;
}
PaidSign sign = PaidSigns.getInstance().getSignManager().getPaidSign(arguments.get(0));
if (sign == null) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND);
return false;
}
try {
try {
//First, assume a condition is changed
return parseGivenConditionLine(sign, sender);
} catch (NumberFormatException exception) {
//Fall back to assume a sign is changed
return parseGivenProperty(sign, sender);
}
} catch (IOException e) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_EXCEPTION_OCCURRED);
return false;
}
}
/**
* Parses the given condition line and the rest of the input
*
* @param sign <p>The paid sign the user is trying to edit</p>
* @param sender <p>The command sender to notify of any errors</p>
* @return <p>True if the command was executed successfully</p>
* @throws NumberFormatException <p>If the given argument is not a number</p>
*/
private boolean parseGivenConditionLine(@NotNull PaidSign sign,
@NotNull CommandSender sender) throws NumberFormatException {
short signLine = (short) (Short.parseShort(arguments.get(1)) - 1);
if (signLine < 0 || signLine > 3 || sign.getConditions().get(signLine) == null) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_NO_SUCH_CONDITION);
return false;
}
if (argumentSize < 4) {
return false;
}
PaidSignConditionProperty conditionProperty = PaidSignConditionProperty.getFromString(arguments.get(2));
if (conditionProperty == null) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_PROPERTY_NOT_RECOGNIZED);
return false;
}
String value = arguments.get(3);
return updateConditionProperty(sender, sign, signLine, conditionProperty, value);
}
/**
* Parses the given paid sign property and the rest of the input
*
* @param sign <p>The paid sign the user is trying to edit</p>
* @param sender <p>The command sender to notify of any errors</p>
* @return <p>True if the command was executed successfully</p>
* @throws IOException <p>If unable to remove or save the sign</p>
*/
private boolean parseGivenProperty(@NotNull PaidSign sign,
@NotNull CommandSender sender) throws IOException {
PaidSignProperty property = PaidSignProperty.getFromString(arguments.get(1));
if (property == null) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_PROPERTY_NOT_RECOGNIZED);
return false;
}
String value = arguments.get(2);
try {
updateProperty(sender, sign, property, value);
return true;
} catch (NumberFormatException exception) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_INVALID_NUMBER);
return false;
}
}
/**
* Updates a property for a paid sign
*
* @param sender <p>The command sender to notify of any errors or success</p>
* @param sign <p>The sign to be updated</p>
* @param property <p>The property to update</p>
* @param newValue <p>The new value of the property</p>
* @throws IOException <p>If unable to remove or save the sign</p>
*/
private void updateProperty(CommandSender sender, PaidSign sign, PaidSignProperty property,
String newValue) throws IOException {
String signName = property == PaidSignProperty.NAME ? newValue : sign.getName();
OptionState ignoreCase = property == PaidSignProperty.IGNORE_CASE ? OptionState.fromString(newValue) :
OptionState.getFromBoolean(sign.getIgnoreCase());
OptionState ignoreColor = property == PaidSignProperty.IGNORE_COLOR ? OptionState.fromString(newValue) :
OptionState.getFromBoolean(sign.getIgnoreColor());
boolean matchAnyCondition = property == PaidSignProperty.MATCH_ANY_CONDITION ? Boolean.parseBoolean(newValue) :
sign.matchAnyCondition();
double cost = property == PaidSignProperty.COST ? Double.parseDouble(newValue) : sign.getCost();
String permission = property == PaidSignProperty.PERMISSION ? newValue : sign.getPermission();
Map<Short, PaidSignCondition> conditions = sign.getConditions();
PaidSignManager manager = PaidSigns.getInstance().getSignManager();
PaidSign updatedSign = new PaidSign(signName, cost, permission, ignoreCase, ignoreColor, matchAnyCondition);
for (short line : conditions.keySet()) {
PaidSignCondition condition = conditions.get(line);
updatedSign.addCondition(line, condition.stringToMatch(), condition.executeRegex(),
OptionState.getFromBoolean(condition.ignoreCase()),
OptionState.getFromBoolean(condition.ignoreColor()));
}
manager.removePaidSign(sign.getName());
manager.addPaidSign(updatedSign);
PaidSigns.getStringFormatter().displaySuccessMessage(sender, PaidSignsTranslatableMessage.SUCCESS_UPDATED_PAID_SIGN);
}
/**
* Updates a property of a condition of a paid sign
*
* @param sender <p>The command sender to notify of any errors or success</p>
* @param sign <p>The sign the condition belongs to</p>
* @param conditionIndex <p>The line index that identifies the sign condition</p>
* @param property <p>The condition property to update</p>
* @param newValue <p>The new value of the property</p>
* @return <p>True if the property was successfully changed</p>
*/
private boolean updateConditionProperty(CommandSender sender, PaidSign sign, short conditionIndex,
PaidSignConditionProperty property, String newValue) {
PaidSignCondition condition = sign.getConditions().get(conditionIndex);
String stringToMatch = property == PaidSignConditionProperty.STRING_TO_MATCH ? newValue :
condition.stringToMatch();
boolean executeRegEx = property == PaidSignConditionProperty.EXECUTE_REG_EX ? Boolean.parseBoolean(newValue) :
condition.executeRegex();
boolean ignoreCase = property == PaidSignConditionProperty.IGNORE_CASE ? OptionState.getBooleanValue(
OptionState.fromString(newValue), sign.getIgnoreCase()) : condition.ignoreCase();
boolean ignoreColor = property == PaidSignConditionProperty.IGNORE_COLOR ? OptionState.getBooleanValue(
OptionState.fromString(newValue), sign.getIgnoreColor()) : condition.ignoreColor();
//Make sure to test the regular expression in case anything changed
if (executeRegEx && isRegExInvalid(sender, stringToMatch)) {
return false;
}
sign.addCondition(conditionIndex, stringToMatch, executeRegEx, OptionState.getFromBoolean(ignoreCase),
OptionState.getFromBoolean(ignoreColor));
try {
PaidSigns.getInstance().getSignManager().saveSigns();
} catch (IOException e) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_EXCEPTION_OCCURRED);
return false;
}
PaidSigns.getStringFormatter().displaySuccessMessage(sender, PaidSignsTranslatableMessage.SUCCESS_UPDATED_PAID_SIGN_CONDITION);
return true;
}
}

View File

@ -0,0 +1,177 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.property.PaidSignConditionProperty;
import net.knarcraft.paidsigns.property.PaidSignProperty;
import net.knarcraft.paidsigns.utility.PaidSignsTabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The tab completer for the edit paid sign command
*/
public class EditTabCompleter extends TokenizedTabCompleter {
private static Map<PaidSignProperty, List<String>> propertyExampleValues;
private static Map<PaidSignConditionProperty, List<String>> conditionPropertyExampleValues;
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
super.onTabComplete(sender, command, label, args);
if (propertyExampleValues == null) {
initializePropertyExampleValues();
initializeConditionPropertyExampleValues();
}
if (argumentSize == 1) {
return TabCompletionHelper.filterMatchingStartsWith(PaidSignsTabCompleteHelper.getPaidSignNames(), arguments.get(0));
} else if (argumentSize >= 2) {
PaidSign sign = PaidSigns.getInstance().getSignManager().getPaidSign(arguments.get(0));
if (sign != null) {
return tabCompleteForSign(sign);
}
}
return new ArrayList<>();
}
/**
* Returns tab completions for the given, non-null sign
*
* @param sign <p>The sign to tab complete for</p>
* @return <p>The tab complete options to give to users</p>
*/
private List<String> tabCompleteForSign(@NotNull PaidSign sign) {
if (argumentSize == 2) {
List<String> conditions = getAvailableSignConditions(sign);
conditions.addAll(getPaidSignProperties());
return TabCompletionHelper.filterMatchingStartsWith(conditions, arguments.get(1));
} else if (argumentSize >= 3) {
try {
return tabCompleteSignLine(sign);
} catch (NumberFormatException exception) {
if (argumentSize == 3) {
return tabCompleteProperty();
}
}
}
return new ArrayList<>();
}
/**
* Returns tab completions for the selected sign line
*
* @param sign <p>The paid sign the line belongs to</p>
* @return <p>The tab complete options to give to users</p>
*/
private List<String> tabCompleteSignLine(@NotNull PaidSign sign) {
short signLine = (short) (Short.parseShort(arguments.get(1)) - 1);
//Refuse to autocomplete if invalid input is given
if (signLine < 0 || signLine > 3 || sign.getConditions().get(signLine) == null) {
return new ArrayList<>();
} else if (argumentSize == 3) {
return TabCompletionHelper.filterMatchingStartsWith(getPaidSignConditionProperties(), arguments.get(2));
} else if (argumentSize == 4) {
PaidSignConditionProperty property = PaidSignConditionProperty.getFromString(arguments.get(2));
if (property != null) {
return TabCompletionHelper.filterMatchingStartsWith(conditionPropertyExampleValues.get(property),
arguments.get(3));
}
}
return new ArrayList<>();
}
/**
* Returns tab completions for the selected property
*
* @return <p>The tab complete options to give to users</p>
*/
private List<String> tabCompleteProperty() {
PaidSignProperty paidSignProperty = PaidSignProperty.getFromString(arguments.get(1));
if (paidSignProperty != null) {
if (paidSignProperty == PaidSignProperty.PERMISSION) {
return PaidSignsTabCompleteHelper.tabCompletePermission(arguments.get(2));
} else {
return TabCompletionHelper.filterMatchingStartsWith(propertyExampleValues.get(paidSignProperty),
arguments.get(2));
}
} else {
return new ArrayList<>();
}
}
/**
* Gets all paid sign condition properties
*
* @return <p>All paid sign condition properties</p>
*/
private List<String> getPaidSignConditionProperties() {
List<String> properties = new ArrayList<>();
for (PaidSignConditionProperty property : PaidSignConditionProperty.values()) {
properties.add(property.getStringRepresentation());
}
return properties;
}
/**
* Gets all paid sign properties
*
* @return <p>All paid sign properties</p>
*/
private List<String> getPaidSignProperties() {
List<String> properties = new ArrayList<>();
for (PaidSignProperty property : PaidSignProperty.values()) {
properties.add(property.getStringRepresentation());
}
return properties;
}
/**
* Gets all sign conditions available for the given sign
*
* @param sign <p>The sign to get available sign conditions for</p>
* @return <p>The available sign conditions</p>
*/
private List<String> getAvailableSignConditions(PaidSign sign) {
List<String> availableConditions = new ArrayList<>();
for (Short signLine : sign.getConditions().keySet()) {
availableConditions.add(String.valueOf(signLine + 1));
}
return availableConditions;
}
/**
* Initializes the map for paid sign property example tab completions
*/
private void initializePropertyExampleValues() {
propertyExampleValues = new HashMap<>();
propertyExampleValues.put(PaidSignProperty.COST, PaidSignsTabCompleteHelper.getCosts());
propertyExampleValues.put(PaidSignProperty.NAME, PaidSignsTabCompleteHelper.getPaidSignNames());
propertyExampleValues.put(PaidSignProperty.IGNORE_CASE, PaidSignsTabCompleteHelper.getOptionStates());
propertyExampleValues.put(PaidSignProperty.IGNORE_COLOR, PaidSignsTabCompleteHelper.getOptionStates());
propertyExampleValues.put(PaidSignProperty.MATCH_ANY_CONDITION, PaidSignsTabCompleteHelper.getBooleans());
}
/**
* Initializes the map for paid sign condition property example tab completions
*/
private void initializeConditionPropertyExampleValues() {
conditionPropertyExampleValues = new HashMap<>();
conditionPropertyExampleValues.put(PaidSignConditionProperty.STRING_TO_MATCH,
PaidSignsTabCompleteHelper.getExampleConditionStrings());
conditionPropertyExampleValues.put(PaidSignConditionProperty.IGNORE_COLOR, PaidSignsTabCompleteHelper.getOptionStates());
conditionPropertyExampleValues.put(PaidSignConditionProperty.IGNORE_CASE, PaidSignsTabCompleteHelper.getOptionStates());
conditionPropertyExampleValues.put(PaidSignConditionProperty.EXECUTE_REG_EX, PaidSignsTabCompleteHelper.getBooleans());
}
}

View File

@ -0,0 +1,179 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.container.PaidSignCondition;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static net.knarcraft.knarlib.formatting.StringFormatter.replacePlaceholder;
import static net.knarcraft.knarlib.formatting.StringFormatter.replacePlaceholders;
/**
* A representation of the command for listing information about paid signs
*/
public class ListCommand extends TokenizedCommand {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
super.onCommand(sender, command, label, args);
if (argumentSize < 1) {
displaySigns(sender, 0);
return true;
} else if (argumentSize < 3) {
try {
int pageNumber = Integer.parseInt(arguments.get(0)) - 1;
displaySigns(sender, pageNumber);
return true;
} catch (NumberFormatException exception) {
return parsePaidSignSelection(sender);
}
}
return false;
}
/**
* Displays all available signs
*
* @param sender <p>The command sender to display the signs to</p>
* @param pageNumber <p>The page of the sign list to display</p>
*/
private void displaySigns(CommandSender sender, int pageNumber) {
StringBuilder signs = new StringBuilder();
Set<String> signNames = PaidSigns.getInstance().getSignManager().getAllPaidSigns().keySet();
List<String> signNameList = new ArrayList<>(signNames);
Collections.sort(signNameList);
String infoFormat = PaidSigns.getTranslator().getTranslatedMessage(PaidSignsTranslatableMessage.PAID_SIGNS_INFO_FORMAT);
//Display up to 10 signs per page
int signsPerPage = 7;
int alreadyDisplayed = pageNumber * signsPerPage;
for (int signIndex = alreadyDisplayed; signIndex < alreadyDisplayed + signsPerPage; signIndex++) {
if (signIndex >= signNameList.size()) {
break;
}
String signName = signNameList.get(signIndex);
signs.append(replacePlaceholder(infoFormat, "{name}", signName));
}
//Display that another page exists, if necessary
boolean hasNextPage = alreadyDisplayed + signsPerPage < signNameList.size();
String nextPagePrompt = "";
if (hasNextPage) {
nextPagePrompt = replacePlaceholder(PaidSigns.getTranslator().getTranslatedMessage(
PaidSignsTranslatableMessage.PAID_SIGNS_NEXT_PROMPT), "{nextPage}", String.valueOf((pageNumber + 2)));
}
sender.sendMessage(StringFormatter.replacePlaceholders(PaidSigns.getTranslator().getTranslatedMessage(
PaidSignsTranslatableMessage.PAID_SIGNS_INFO), new String[]{"{signs}", "{nextPagePrompt}"},
new String[]{signs.toString(), nextPagePrompt}));
}
/**
* Parses the given input and displays the wanted paid-sign information
*
* @param sender <p>The command sender to display the information to</p>
* @return <p>True if successful. False if the input contained errors</p>
*/
private boolean parsePaidSignSelection(CommandSender sender) {
PaidSign paidSign = PaidSigns.getInstance().getSignManager().getPaidSign(arguments.get(0));
if (paidSign == null) {
PaidSigns.getStringFormatter().displayErrorMessage(sender,
PaidSignsTranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND);
return false;
}
if (argumentSize < 2) {
displayPaidSign(sender, paidSign);
} else {
short lineNumber;
try {
lineNumber = (short) (Short.parseShort(arguments.get(1)) - 1);
if (lineNumber < 0 || lineNumber > 3) {
PaidSigns.getStringFormatter().displayErrorMessage(sender,
PaidSignsTranslatableMessage.ERROR_INVALID_NUMBER);
return false;
}
} catch (NumberFormatException exception) {
PaidSigns.getStringFormatter().displayErrorMessage(sender,
PaidSignsTranslatableMessage.ERROR_INVALID_NUMBER);
return false;
}
if (!paidSign.getConditions().containsKey(lineNumber)) {
PaidSigns.getStringFormatter().displayErrorMessage(sender,
PaidSigns.getStringFormatter().replacePlaceholder(
PaidSignsTranslatableMessage.ERROR_NO_SUCH_CONDITION, "{line}",
String.valueOf(lineNumber)));
return false;
}
PaidSignCondition condition = paidSign.getConditions().get(lineNumber);
displayPaidSignCondition(sender, paidSign.getName(), lineNumber, condition);
}
return true;
}
/**
* Displays information about a paid sign condition
*
* @param sender <p>The command sender to display the information to</p>
* @param signName <p>The name of the sign to display the condition for</p>
* @param signLine <p>The line the condition is for</p>
* @param condition <p>The condition to display information about</p>
*/
private void displayPaidSignCondition(CommandSender sender, String signName, short signLine,
PaidSignCondition condition) {
sender.sendMessage(PaidSigns.getStringFormatter().replacePlaceholders(
PaidSignsTranslatableMessage.PAID_SIGN_CONDITION_INFO, new String[]{"{name}", "{line}", "{match}",
"{regex}", "{case}", "{color}"}, new String[]{signName, String.valueOf(signLine + 1),
condition.stringToMatch(), translateBoolean(condition.executeRegex()),
translateBoolean(condition.ignoreCase()), translateBoolean(condition.ignoreColor())}));
}
/**
* Displays information about a paid sign
*
* @param sender <p>The command sender to display the information to</p>
* @param paidSign <p>The paid sign to display information about</p>
*/
private void displayPaidSign(CommandSender sender, PaidSign paidSign) {
Map<Short, PaidSignCondition> signConditions = paidSign.getConditions();
StringBuilder conditions = new StringBuilder();
for (short lineIndex : signConditions.keySet()) {
String format = PaidSigns.getTranslator().getTranslatedMessage(
PaidSignsTranslatableMessage.PAID_SIGN_INFO_CONDITION_FORMAT);
conditions.append(StringFormatter.replacePlaceholders(format, new String[]{"{line}", "{condition}"},
new String[]{String.valueOf((lineIndex + 1)), signConditions.get(lineIndex).stringToMatch()}));
}
sender.sendMessage(replacePlaceholders(PaidSigns.getTranslator().getTranslatedMessage(
PaidSignsTranslatableMessage.PAID_SIGN_INFO), new String[]{"{name}", "{cost}", "{permission}", "{case}",
"{color}", "{any}", "{conditions}"}, new String[]{paidSign.getName(), String.valueOf(paidSign.getCost()),
paidSign.getPermission(), translateBoolean(paidSign.getIgnoreCase()),
translateBoolean(paidSign.getIgnoreColor()), translateBoolean(paidSign.matchAnyCondition()),
conditions.toString()}));
}
/**
* Translates the given boolean value
*
* @param booleanValue <p>The boolean value to translate</p>
* @return <p>The translation of the boolean value</p>
*/
public static String translateBoolean(boolean booleanValue) {
if (booleanValue) {
return PaidSigns.getTranslator().getTranslatedMessage(PaidSignsTranslatableMessage.BOOLEAN_TRUE);
} else {
return PaidSigns.getTranslator().getTranslatedMessage(PaidSignsTranslatableMessage.BOOLEAN_FALSE);
}
}
}

View File

@ -0,0 +1,40 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.utility.PaidSignsTabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The tab completer for the list paid signs command
*/
public class ListTabCompleter extends TokenizedTabCompleter {
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
super.onTabComplete(sender, command, alias, args);
if (argumentSize == 1) {
return TabCompletionHelper.filterMatchingStartsWith(PaidSignsTabCompleteHelper.getPaidSignNames(), arguments.get(0));
} else if (argumentSize == 2) {
PaidSign sign = PaidSigns.getInstance().getSignManager().getPaidSign(arguments.get(0));
if (sign != null) {
List<String> availableConditions = new ArrayList<>();
for (Short signLine : sign.getConditions().keySet()) {
availableConditions.add(String.valueOf(signLine + 1));
}
return TabCompletionHelper.filterMatchingStartsWith(availableConditions, arguments.get(1));
}
}
return new ArrayList<>();
}
}

View File

@ -0,0 +1,81 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class PaidSignsTabCommand implements TabExecutor {
private static final ChatColor successColor = ChatColor.GREEN;
private static final ChatColor commandColor = ChatColor.YELLOW;
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
sender.sendMessage(commandColor + "[] denote optional parameters");
sender.sendMessage(commandColor + "<> denote required parameters");
sender.sendMessage(commandColor + "{} denote required permission");
sender.sendMessage(commandColor + "In some cases, commands with required parameters can be called with no parameters");
if (sender instanceof Player) {
showPlayerCommands(sender);
}
return true;
}
/**
* Shows all commands available to the sending player
*
* @param sender <p>The player which sent the command</p>
*/
private void showPlayerCommands(CommandSender sender) {
sender.sendMessage(commandColor + "Commands:");
showCommandInfo("addPaidSign", sender);
showCommandInfo("addPaidSignCondition", sender);
showCommandInfo("listPaidSigns", sender);
showCommandInfo("editPaidSign", sender);
showCommandInfo("removePaidSignCondition", sender);
showCommandInfo("removePaidSign", sender);
showCommandInfo("reload", sender);
}
/**
* Shows information about the given command
*
* @param command <p>The command to get information about</p>
* @param sender <p>The sender asking to see command info</p>
*/
private void showCommandInfo(String command, CommandSender sender) {
PluginCommand pluginCommand = PaidSigns.getInstance().getCommand(command);
if (pluginCommand != null) {
String permission = pluginCommand.getPermission();
if (permission == null || sender.hasPermission(permission)) {
String commandInfo = "\n" + commandColor + pluginCommand.getUsage().replace("<command>",
pluginCommand.getName()) + ": " + successColor + pluginCommand.getDescription();
if (sender.hasPermission("paidsigns.admin")) {
if (permission == null) {
permission = "None";
}
commandInfo += commandColor + " {" + permission + "}";
}
sender.sendMessage(commandInfo);
}
}
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
return new ArrayList<>();
}
}

View File

@ -1,6 +1,7 @@
package net.knarcraft.paidsigns.command; package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns; import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
@ -18,7 +19,8 @@ public class ReloadTabCommand implements TabExecutor {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) { @NotNull String[] args) {
PaidSigns.getInstance().reload(); PaidSigns.getInstance().reload();
return false; PaidSigns.getStringFormatter().displaySuccessMessage(sender, PaidSignsTranslatableMessage.SUCCESS_RELOADED);
return true;
} }
@Override @Override

View File

@ -1,55 +0,0 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.utility.Tokenizer;
import org.apache.commons.lang.ArrayUtils;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A representation of the command for removing a paid sign
*/
public class RemoveCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (args.length < 1) {
return false;
}
List<String> arguments = Tokenizer.tokenize(String.join(" ", args));
String[] input = arguments.get(0).split("\\|");
short line;
try {
line = (short) (Short.parseShort(input[0]) - 1);
} catch (NumberFormatException exception) {
sender.sendMessage("Invalid line number given");
return false;
}
String id = String.join("|", (String[]) ArrayUtils.remove(input, 0));
try {
if (PaidSigns.getInstance().getSignManager().removePaidSign(id, line)) {
sender.sendMessage("Successfully removed paid sign");
} else {
sender.sendMessage("No matching paid sign was found");
}
return true;
} catch (IOException e) {
Logger logger = PaidSigns.getInstance().getLogger();
logger.log(Level.SEVERE, "Exception encountered while trying to write " +
"to the data file");
logger.log(Level.SEVERE, Arrays.toString(e.getStackTrace()));
sender.sendMessage("An exception occurred. Please notify the server administrator or check the server log.");
}
return false;
}
}

View File

@ -0,0 +1,64 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.manager.PaidSignManager;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
/**
* A representation of the command for removing a condition from a sign
*/
public class RemoveConditionCommand extends TokenizedCommand {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
super.onCommand(sender, command, label, args);
if (argumentSize < 2) {
return false;
}
String name = arguments.get(0);
short lineNumber;
try {
lineNumber = (short) (Short.parseShort(arguments.get(1)) - 1);
if (lineNumber < 0 || lineNumber > 3) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_INVALID_NUMBER);
return false;
}
} catch (NumberFormatException exception) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_INVALID_NUMBER);
return false;
}
PaidSignManager signManager = PaidSigns.getInstance().getSignManager();
PaidSign sign = signManager.getPaidSign(name);
if (sign == null) {
PaidSigns.getStringFormatter().displayErrorMessage(sender,
PaidSignsTranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND);
return false;
}
if (!sign.getConditions().containsKey(lineNumber)) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSigns.getStringFormatter().replacePlaceholder(
PaidSignsTranslatableMessage.ERROR_NO_SUCH_CONDITION, "{line}",
String.valueOf((lineNumber + 1))));
return false;
}
sign.removeCondition(lineNumber);
try {
signManager.saveSigns();
} catch (IOException e) {
PaidSigns.getStringFormatter().displayErrorMessage(sender, PaidSignsTranslatableMessage.ERROR_EXCEPTION_OCCURRED);
return false;
}
PaidSigns.getStringFormatter().displaySuccessMessage(sender, PaidSignsTranslatableMessage.SUCCESS_REMOVED_CONDITION);
return true;
}
}

View File

@ -0,0 +1,53 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.utility.PaidSignsTabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The tab completer for the remove paid sign condition command
*/
public class RemoveConditionTabCompleter extends TokenizedTabCompleter {
private List<String> lineIndices;
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
super.onTabComplete(sender, command, alias, args);
if (lineIndices == null) {
initializeValues();
}
if (argumentSize == 1) {
return TabCompletionHelper.filterMatchingStartsWith(PaidSignsTabCompleteHelper.getPaidSignNames(), arguments.get(0));
} else if (argumentSize == 2) {
PaidSign sign = PaidSigns.getInstance().getSignManager().getPaidSign(arguments.get(0));
if (sign != null) {
List<String> availableConditions = new ArrayList<>();
for (Short signLine : sign.getConditions().keySet()) {
availableConditions.add(String.valueOf(signLine + 1));
}
return TabCompletionHelper.filterMatchingStartsWith(availableConditions, arguments.get(1));
}
}
return new ArrayList<>();
}
/**
* Initializes the values available for tab completion
*/
private void initializeValues() {
lineIndices = PaidSignsTabCompleteHelper.getSignLines();
}
}

View File

@ -0,0 +1,61 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.utility.PaidSignsTabCompleteHelper;
import net.knarcraft.paidsigns.utility.Tokenizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A representation of the command for removing a paid sign
*/
public class RemoveTabCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
List<String> arguments = Tokenizer.tokenize(String.join(" ", args));
if (arguments.size() < 1) {
return false;
}
String name = arguments.get(0);
try {
if (PaidSigns.getInstance().getSignManager().removePaidSign(name)) {
PaidSigns.getStringFormatter().displaySuccessMessage(sender,
PaidSignsTranslatableMessage.SUCCESS_REMOVED_PAID_SIGN);
} else {
PaidSigns.getStringFormatter().displaySuccessMessage(sender,
PaidSignsTranslatableMessage.ERROR_PAID_SIGN_NOT_FOUND);
}
return true;
} catch (IOException e) {
PaidSigns.getStringFormatter().displayErrorMessage(sender,
PaidSignsTranslatableMessage.ERROR_EXCEPTION_OCCURRED);
}
return false;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
List<String> arguments = Tokenizer.tokenize(String.join(" ", args));
int argumentSize = args[args.length - 1].isEmpty() ? arguments.size() : arguments.size() - 1;
if (argumentSize < 1) {
return PaidSignsTabCompleteHelper.getPaidSignNames();
} else {
return new ArrayList<>();
}
}
}

View File

@ -1,31 +0,0 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The tab completer for the remove command
*/
public class RemoveTabCompleter implements TabCompleter {
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
List<PaidSign> allPaidSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns();
List<String> signIds = new ArrayList<>();
for (PaidSign sign : allPaidSigns) {
signIds.add("\"" + (sign.getLineIndex() + 1) + "|" + sign.getId() + "\"");
}
return signIds;
}
}

View File

@ -0,0 +1,48 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.utility.Tokenizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* A command executor with tokenized arguments
*/
public class TokenizedCommand implements CommandExecutor {
protected List<String> arguments;
protected int argumentSize;
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
arguments = Tokenizer.tokenize(String.join(" ", args));
argumentSize = arguments.size();
return true;
}
/**
* Tests whether the given regular expression is valid
*
* @param sender <p>The command sender to notify if the regular expression is invalid</p>
* @param regularExpression <p>The regular expression to test</p>
* @return <p>True if the regular expression is invalid</p>
*/
protected boolean isRegExInvalid(CommandSender sender, String regularExpression) {
try {
Pattern.compile(regularExpression);
return false;
} catch (PatternSyntaxException exception) {
PaidSigns.getStringFormatter().displayErrorMessage(sender,
PaidSignsTranslatableMessage.ERROR_INVALID_REGULAR_EXPRESSION);
return true;
}
}
}

View File

@ -0,0 +1,36 @@
package net.knarcraft.paidsigns.command;
import net.knarcraft.paidsigns.utility.Tokenizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* A tab completer with tokenized arguments
*/
public class TokenizedTabCompleter implements TabCompleter {
protected List<String> arguments;
protected int argumentSize;
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
arguments = Tokenizer.tokenize(String.join(" ", args));
if (args.length == 0) {
argumentSize = 0;
} else {
if (args[args.length - 1].isEmpty()) {
arguments.add("");
}
argumentSize = arguments.size();
}
return null;
}
}

View File

@ -2,58 +2,59 @@ package net.knarcraft.paidsigns.container;
import net.knarcraft.paidsigns.PaidSigns; import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.property.OptionState; import net.knarcraft.paidsigns.property.OptionState;
import net.knarcraft.paidsigns.utility.ColorHelper;
import net.md_5.bungee.api.ChatColor; import java.util.HashMap;
import java.util.Map;
/** /**
* A representation of a paid sign * A representation of a paid sign
*/ */
public class PaidSign { public class PaidSign {
private final String id; private final String name;
private final String cleanId;
private final short lineIndex;
private final double cost; private final double cost;
private final String permission;
private final OptionState ignoreCase; private final OptionState ignoreCase;
private final OptionState ignoreColor; private final OptionState ignoreColor;
private final boolean matchAnyCondition;
private final Map<Short, PaidSignCondition> conditions = new HashMap<>();
/** /**
* Instantiates a new paid sign * Instantiates a new paid sign
* *
* @param id <p>The string that identifies this type of paid sign</p> * @param name <p>A recognizable, unique, name used to identify this paid sign</p>
* @param lineIndex <p>The line the id has to be on to trigger payment</p>
* @param cost <p>The cost of creating this paid sign</p> * @param cost <p>The cost of creating this paid sign</p>
* @param permission <p>The permission required to create the sign this paid sign matches</p>
* @param ignoreCase <p>Whether to ignore case when looking for this permission sign</p> * @param ignoreCase <p>Whether to ignore case when looking for this permission sign</p>
* @param ignoreColor <p>Whether to ignore color when looking for this permission sign</p> * @param ignoreColor <p>Whether to ignore color when looking for this permission sign</p>
* @param matchAnyCondition <p>Whether to treat a match for any condition as a sign match</p>
*/ */
public PaidSign(String id, short lineIndex, double cost, OptionState ignoreCase, OptionState ignoreColor) { public PaidSign(String name, double cost, String permission, OptionState ignoreCase, OptionState ignoreColor,
if (id == null || id.trim().isBlank()) { boolean matchAnyCondition) {
throw new IllegalArgumentException("Id cannot be empty"); if (name == null || name.trim().isBlank()) {
throw new IllegalArgumentException("Name cannot be empty");
} }
if (cost <= 0) { if (cost <= 0) {
throw new IllegalArgumentException("Cost must be larger than 0"); throw new IllegalArgumentException("Cost must be larger than 0");
} }
if (lineIndex < 0 || lineIndex > 3) {
throw new IllegalArgumentException("Sign line must be between 0 and 3");
}
if (ignoreCase == null || ignoreColor == null) { if (ignoreCase == null || ignoreColor == null) {
throw new IllegalArgumentException("Ignore case and ignore color options cannot be null"); throw new IllegalArgumentException("Ignore case and ignore color options cannot be null");
} }
this.id = id; this.name = name;
this.lineIndex = lineIndex;
this.cost = cost; this.cost = cost;
this.permission = permission;
this.ignoreCase = ignoreCase; this.ignoreCase = ignoreCase;
this.ignoreColor = ignoreColor; this.ignoreColor = ignoreColor;
this.cleanId = getCleanString(id); this.matchAnyCondition = matchAnyCondition;
} }
/** /**
* Gets the id string of this paid sign * Gets the name identifying this paid sign
* *
* @return <p>The id string of this paid sign</p> * @return <p>The name identifying this paid sign</p>
*/ */
public String getId() { public String getName() {
return id; return name;
} }
/** /**
@ -66,66 +67,21 @@ public class PaidSign {
} }
/** /**
* Gets the line on the sign the id must be on to trigger payment * Gets the permission required by a player to create the sign this paid sign matches
* *
* @return <p>The sign line to search for the id</p> * @return <p>The permission required by a player to create the sign this paid sign matches</p>
*/ */
public short getLineIndex() { public String getPermission() {
return lineIndex; return this.permission;
} }
/** /**
* Gets the clean id of this paid sign * Gets all conditions registered for this paid sign
* *
* @return <p>The clean id of this paid sign</p> * @return <p>All conditions registered for this paid sign</p>
*/ */
public String getCleanId() { public Map<Short, PaidSignCondition> getConditions() {
return cleanId; return new HashMap<>(this.conditions);
}
/**
* Gets the "clean" version of the given string
*
* <p>The "cleaning" removes color codes and lower-cases the string.</p>
*
* @param string <p>The string to clean</p>
* @return <p>The cleaned string</p>
*/
public static String getCleanString(String string) {
return ChatColor.stripColor(ColorHelper.translateAllColorCodes(string.toLowerCase()));
}
/**
* Checks whether this paid sign matches the given line
*
* @param lineNumber <p>The line number of the given line</p>
* @param line <p>The line to compare against this paid sign's id</p>
* @return <p>True if the line matches this sign</p>
*/
public boolean matches(short lineNumber, String line) {
if (lineNumber != this.lineIndex) {
return false;
}
String idCopy = id;
if (getIgnoreCase()) {
idCopy = idCopy.toLowerCase();
line = line.toLowerCase();
}
if (getIgnoreColor()) {
idCopy = ColorHelper.stripColorCodes(idCopy);
line = ColorHelper.stripColorCodes(line);
}
return idCopy.equals(line);
}
/**
* Checks whether this paid sign matches another paid sign
*
* @param paidSign <p>The other paid sign to compare to</p>
* @return <p>True if the signs match</p>
*/
public boolean matches(PaidSign paidSign) {
return matches(paidSign.lineIndex, paidSign.id);
} }
/** /**
@ -134,11 +90,7 @@ public class PaidSign {
* @return <p>Whether the text case should be ignored for this paid sign</p> * @return <p>Whether the text case should be ignored for this paid sign</p>
*/ */
public boolean getIgnoreCase() { public boolean getIgnoreCase() {
if (this.ignoreCase == OptionState.DEFAULT) { return OptionState.getBooleanValue(this.ignoreCase, PaidSigns.getInstance().ignoreCase());
return PaidSigns.getInstance().ignoreCase();
} else {
return OptionState.getBooleanValue(this.ignoreCase);
}
} }
/** /**
@ -147,11 +99,73 @@ public class PaidSign {
* @return <p>Whether the text color should be ignored for this paid sign</p> * @return <p>Whether the text color should be ignored for this paid sign</p>
*/ */
public boolean getIgnoreColor() { public boolean getIgnoreColor() {
if (this.ignoreColor == OptionState.DEFAULT) { return OptionState.getBooleanValue(this.ignoreColor, PaidSigns.getInstance().ignoreColor());
return PaidSigns.getInstance().ignoreColor();
} else {
return OptionState.getBooleanValue(this.ignoreColor);
} }
/**
* Gets whether to treat a match for any condition as a sign match
*
* @return <p>Whether to treat a match for any condition as a sign match</p>
*/
public boolean matchAnyCondition() {
return this.matchAnyCondition;
}
/**
* Checks whether this paid sign matches the given set of sign lines
*
* @param lines <p>The sign lines to test against</p>
* @return <p>True if this paid sign matches the given lines</p>
*/
public boolean matches(String[] lines) {
//Make sure a paid sign without a condition never matches anything
if (this.conditions.isEmpty()) {
return false;
}
boolean matchAny = matchAnyCondition();
boolean success = !matchAny;
for (short i = 0; i < 4; i++) {
PaidSignCondition condition = this.conditions.get(i);
if (condition != null) {
boolean conditionMatches = condition.test(lines[i]);
if (matchAny) {
success |= conditionMatches;
} else {
success &= conditionMatches;
}
}
}
return success;
}
/**
* Adds a condition to this paid sign
*
* @param line <p>The line on the sign the matched text must be on</p>
* @param stringToMatch <p>The string that should be matched for the new condition to be true</p>
* @param executeRegex <p>Whether to execute the string as RegEx</p>
* @param ignoreCase <p>Whether to ignore case when matching against the condition</p>
* @param ignoreColor <p>Whether to ignore color when matching against the condition</p>
*/
public void addCondition(short line, String stringToMatch, boolean executeRegex, OptionState ignoreCase,
OptionState ignoreColor) {
if (line < 0 || line > 3) {
throw new IllegalArgumentException("Invalid sign line given for new paid sign condition");
}
boolean ignoreCaseBoolean = OptionState.getBooleanValue(ignoreCase, this.getIgnoreCase());
boolean ignoreColorBoolean = OptionState.getBooleanValue(ignoreColor, this.getIgnoreColor());
this.conditions.put(line, new PaidSignCondition(stringToMatch, executeRegex, ignoreCaseBoolean,
ignoreColorBoolean));
}
/**
* Removes a condition from this paid sign
*
* @param line <p>The sign line the condition belongs to</p>
*/
public void removeCondition(short line) {
this.conditions.remove(line);
} }
} }

View File

@ -0,0 +1,47 @@
package net.knarcraft.paidsigns.container;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
/**
* A condition for deciding if a paid sign matches a sign line
*
* @param stringToMatch <p>The string/regular expression the line has to match to fulfill this condition</p>
* @param executeRegex <p>Whether to execute the match string as a regular expression</p>
* @param ignoreCase <p>Whether to ignore uppercase/lowercase when comparing against this condition</p>
* @param ignoreColor <p>Whether to ignore color codes when comparing against this condition</p>
*/
public record PaidSignCondition(String stringToMatch, boolean executeRegex, boolean ignoreCase,
boolean ignoreColor) {
/**
* Tests whether the given line matches this condition
*
* @param line <p>The sign line to test</p>
* @return <p>True if this condition matches the given line</p>
*/
public boolean test(String line) {
String stringToMatch = this.stringToMatch;
//Strip color codes if they shouldn't matter
if (this.ignoreColor) {
stringToMatch = ColorHelper.stripColorCodes(stringToMatch, ColorConversion.NORMAL);
line = ColorHelper.stripColorCodes(line, ColorConversion.NORMAL);
}
if (this.executeRegex) {
//Match using RegEx
if (this.ignoreCase) {
return line.matches("(?i)" + stringToMatch);
} else {
return line.matches(stringToMatch);
}
} else {
//Match regularly
if (this.ignoreCase) {
return stringToMatch.equalsIgnoreCase(line);
} else {
return stringToMatch.equals(line);
}
}
}
}

View File

@ -0,0 +1,19 @@
package net.knarcraft.paidsigns.container;
import org.jetbrains.annotations.NotNull;
/**
* A container class for number of condition matches for a paid sign
*
* @param paidSign <p>The paid sign this class keeps track of matches for</p>
* @param conditionMatches <p>The number of conditions matched for the paid sign</p>
*/
public record PaidSignConditionMatch(PaidSign paidSign,
int conditionMatches) implements Comparable<PaidSignConditionMatch> {
@Override
public int compareTo(@NotNull PaidSignConditionMatch other) {
return Integer.compare(this.conditionMatches, other.conditionMatches);
}
}

View File

@ -0,0 +1,45 @@
package net.knarcraft.paidsigns.container;
import org.bukkit.block.sign.Side;
import java.util.UUID;
/**
* A representation of a sign placed by a player that matched a paid sign
*
* @param playerId <p>The unique id of the player that created the sign</p>
* @param frontCost <p>The cost the player paid for creating the front of the sign</p>
* @param backCost <p>The cost the player paid for creating the back of the sign</p>
*/
public record TrackedSign(UUID playerId, double frontCost, double backCost) {
/**
* Gets an instance of a tracked sign
*
* @param playerId <p>The unique id of the player that created the sign</p>
* @param side <p>The side the player paid for</p>
* @param cost <p>The cost the player paid for creating the side of the sign</p>
* @return <p>A tracked sign instance</p>
*/
public static TrackedSign getInstance(UUID playerId, Side side, double cost) {
if (side == Side.FRONT) {
return new TrackedSign(playerId, cost, 0);
} else {
return new TrackedSign(playerId, 0, cost);
}
}
/**
* Gets the cost paid for the given side
*
* @param side <p>The side to check the cost for</p>
* @return <p>The cost the player paid</p>
*/
public double getCost(Side side) {
return switch (side) {
case FRONT -> frontCost;
case BACK -> backCost;
};
}
}

View File

@ -0,0 +1,145 @@
package net.knarcraft.paidsigns.formatting;
import net.knarcraft.knarlib.formatting.TranslatableMessage;
/**
* An enum representing all translatable messages
*/
public enum PaidSignsTranslatableMessage implements TranslatableMessage {
/**
* The message to display when a paid sign is successfully added
*/
SUCCESS_ADDED_PAID_SIGN,
/**
* The message to display when a paid sign condition is successfully added
*/
SUCCESS_ADDED_PAID_SIGN_CONDITION,
/**
* The message to display when a paid sign is successfully updated
*/
SUCCESS_UPDATED_PAID_SIGN,
/**
* The message to display when a paid sign condition is successfully updated
*/
SUCCESS_UPDATED_PAID_SIGN_CONDITION,
/**
* The message to display when a paid sign has been successfully removed
*/
SUCCESS_REMOVED_PAID_SIGN,
/**
* The message to display when a paid sign condition has been successfully removed
*/
SUCCESS_REMOVED_CONDITION,
/**
* The message to display after the plugin has been reloaded
*/
SUCCESS_RELOADED,
/**
* The message to display when a player has been charged for creating a paid sign
*/
SUCCESS_PAID_FOR_SIGN,
/**
* The message to display when a player has been refunded for breaking a sign
*/
SUCCESS_REFUNDED,
/**
* The info text used to display all paid signs
*/
PAID_SIGNS_INFO,
/**
* The format used for displaying a paid sign's name
*/
PAID_SIGNS_INFO_FORMAT,
/**
* The prompt displayed when the paid sign list has more items
*/
PAID_SIGNS_NEXT_PROMPT,
/**
* The info text used to display information about a paid sign
*/
PAID_SIGN_INFO,
/**
* The info text used to display information about a paid sign condition
*/
PAID_SIGN_CONDITION_INFO,
/**
* The format used for displaying one of a paid sign's conditions
*/
PAID_SIGN_INFO_CONDITION_FORMAT,
/**
* The message to display for a true boolean value
*/
BOOLEAN_TRUE,
/**
* The message to display for a false boolean value
*/
BOOLEAN_FALSE,
/**
* The error to display when a command argument contains an invalid number
*/
ERROR_INVALID_NUMBER,
/**
* The error to display if a paid sign name duplicate is encountered
*/
ERROR_NAME_DUPLICATE,
/**
* The error to display if a severe exception occurs
*/
ERROR_EXCEPTION_OCCURRED,
/**
* The error to display if some input is invalid
*/
ERROR_INVALID_INPUT,
/**
* The error to display if a specified paid sign is not found
*/
ERROR_PAID_SIGN_NOT_FOUND,
/**
* The error to display if a paid sign condition is specified, but does not exist
*/
ERROR_NO_SUCH_CONDITION,
/**
* The error to display if a player cannot pay for a sign matching a paid sign
*/
ERROR_CANNOT_AFFORD,
/**
* The error to display if an invalid regular expression is provided
*/
ERROR_INVALID_REGULAR_EXPRESSION,
/**
* The error to display if an invalid property is given to the edit command
*/
ERROR_PROPERTY_NOT_RECOGNIZED,
;
@Override
public TranslatableMessage[] getAllMessages() {
return PaidSignsTranslatableMessage.values();
}
}

View File

@ -0,0 +1,103 @@
package net.knarcraft.paidsigns.listener;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.manager.TrackedSignManager;
import net.knarcraft.paidsigns.utility.SignHelper;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockPistonExtendEvent;
import org.bukkit.event.block.BlockPistonRetractEvent;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import java.io.IOException;
import java.util.List;
/**
* A listener that listens for any tracked signs being broken
*/
public class SignBreakListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onBlockBreak(BlockBreakEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSign(event.getBlock(), true);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onExplosion(BlockExplodeEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSigns(event.blockList());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEntityExplosion(EntityExplodeEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSigns(event.blockList());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPistonPush(BlockPistonExtendEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSigns(event.getBlocks());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPistonPull(BlockPistonRetractEvent event) {
if (event.isCancelled()) {
return;
}
removeTrackedSigns(event.getBlocks());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEntityBlockChange(EntityChangeBlockEvent event) {
if (event.isCancelled()) {
return;
}
if (event.getEntity() instanceof Player) {
return;
}
removeTrackedSign(event.getBlock(), PaidSigns.getInstance().refundAlways());
}
/**
* Removes all tracked signs from the given blocks
*
* @param blocks <p>The blocks to search for tracked signs</p>
*/
private void removeTrackedSigns(List<Block> blocks) {
for (Block block : blocks) {
removeTrackedSign(block, PaidSigns.getInstance().refundAlways());
}
}
/**
* Tries to remove any tracked sign at the given block
*
* @param block <p>The block that might be a sign</p>
* @param refund <p>Whether to perform a refund after un-tracking the sign</p>
*/
private void removeTrackedSign(Block block, boolean refund) {
if (SignHelper.isSign(block)) {
try {
TrackedSignManager.removeTrackedSign(block.getLocation(), refund, false);
} catch (IOException ignored) {
}
}
}
}

View File

@ -1,69 +1,185 @@
package net.knarcraft.paidsigns.listener; package net.knarcraft.paidsigns.listener;
import net.knarcraft.knarlib.formatting.TranslatableMessage;
import net.knarcraft.paidsigns.PaidSigns; import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign; import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.container.PaidSignConditionMatch;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.manager.EconomyManager; import net.knarcraft.paidsigns.manager.EconomyManager;
import net.knarcraft.paidsigns.manager.PaidSignManager; import net.knarcraft.paidsigns.manager.TrackedSignManager;
import net.knarcraft.paidsigns.utility.SignHelper;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.SignChangeEvent; import org.bukkit.event.block.SignChangeEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.logging.Level;
/** /**
* A listener for listening to registered paid signs * A listener for listening to registered paid signs
*/ */
public class SignListener implements Listener { public class SignListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.LOW)
public void onSignChange(SignChangeEvent event) { public void onSignChange(SignChangeEvent event) {
if (event.isCancelled() || event.getPlayer().hasPermission("paidsigns.paymentexempt")) { if (event.isCancelled() || event.getPlayer().hasPermission("paidsigns.paymentexempt")) {
return; return;
} }
String[] lines = event.getLines(); // As signs can be edited now, this event might be triggered on an already registered sign, so unregister
PaidSignManager signManager = PaidSigns.getInstance().getSignManager(); if (SignHelper.isSign(event.getBlock())) {
for (short lineIndex = 0; lineIndex < lines.length; lineIndex++) { try {
//Get all "weak" matches (any paid sign with a clean id matching the clean line) TrackedSignManager.removeTrackedSign(event.getBlock().getLocation(), true, false,
List<PaidSign> matchingSigns = signManager.getPaidSigns(PaidSign.getCleanString(lines[lineIndex]), lineIndex); event.getSide());
if (matchingSigns.isEmpty()) { } catch (IOException ignored) {
continue;
} }
if (testMatchingSigns(lineIndex, lines[lineIndex], matchingSigns, event)) { }
String[] lines = event.getLines();
Map<String, PaidSign> allPaidSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns();
//Generate the sorted list of the number of paid sign conditions matched for every paid sign
List<PaidSignConditionMatch> allConditionMatches = new ArrayList<>();
for (PaidSign paidSign : allPaidSigns.values()) {
calculateConditionMatches(paidSign, lines, allConditionMatches);
}
allConditionMatches.sort(Collections.reverseOrder());
//Make the player pay for the paid sign
PaidSign mostExpensive = getMatchingSign(allConditionMatches);
if (mostExpensive != null) {
performPaidSignCheck(mostExpensive, event);
}
}
/**
* Gets the most specific paid sign match
*
* <p>This method chooses the most specific match, if any. If two matches have the same number of matched
* conditions, it will choose the most expensive one.</p>
*
* @param allConditionMatches <p>A sorted list of all paid sign condition matches</p>
* @return <p>The best matching paid sign, or null if no paid sign matches</p>
*/
private PaidSign getMatchingSign(List<PaidSignConditionMatch> allConditionMatches) {
PaidSign mostExpensive = null;
int bestMatch = 1;
for (PaidSignConditionMatch paidSignConditionMatch : allConditionMatches) {
int conditionMatches = paidSignConditionMatch.conditionMatches();
/* Stop if there is no matching paid signs. Also stop if a paid sign is encountered with fewer conditions
matching */
if (conditionMatches >= bestMatch) {
PaidSign paidSign = paidSignConditionMatch.paidSign();
//Choose the most expensive paid sign between those with the same number of matched conditions
if (mostExpensive == null || paidSign.getCost() > mostExpensive.getCost()) {
mostExpensive = paidSign;
bestMatch = conditionMatches;
}
} else {
break;
}
}
return mostExpensive;
}
/**
* Calculates the number of conditions matching for the given paid sign
*
* <p>This calculates the number of matches, allowing the most specific paid sign to be chosen</p>
*
* @param paidSign <p>The paid sign to calculate matches for</p>
* @param lines <p>The lines of a sign</p>
* @param allConditionMatches <p>The number of conditions matched</p>
*/
private void calculateConditionMatches(PaidSign paidSign, String[] lines, List<PaidSignConditionMatch> allConditionMatches) {
if (paidSign.matches(lines)) {
if (paidSign.matchAnyCondition()) {
//For any match, it can only be assumed one lines matches
allConditionMatches.add(new PaidSignConditionMatch(paidSign, 1));
} else {
//For a normal match, the number of conditions is equal to the number of matches
allConditionMatches.add(new PaidSignConditionMatch(paidSign, paidSign.getConditions().size()));
}
} else {
allConditionMatches.add(new PaidSignConditionMatch(paidSign, 0));
}
}
/**
* Performs necessary checks and performs the paid sign transaction
*
* @param paidSign <p>The paid sign to test against the sign lines</p>
* @param event <p>The triggered sign change event to cancel if necessary</p>
*/
private void performPaidSignCheck(PaidSign paidSign, SignChangeEvent event) {
Player player = event.getPlayer();
String permission = paidSign.getPermission();
//If a match is found, but the player is missing the permission, assume no plugin sign was created
if (permission != null && !permission.trim().isEmpty() && !player.hasPermission(permission)) {
return; return;
} }
performPaidSignTransaction(paidSign, player, event);
Location signLocation = event.getBlock().getLocation();
if (!event.isCancelled()) {
//Immediately refund if a plugin destroyed the sign within 5 ticks of the creation
Bukkit.getScheduler().scheduleSyncDelayedTask(PaidSigns.getInstance(), () -> {
Block block = signLocation.getBlock();
if (!SignHelper.isSign(block)) {
try {
TrackedSignManager.removeTrackedSign(block.getLocation(), true, true);
} catch (IOException e) {
PaidSigns.getInstance().getLogger().log(Level.WARNING, String.format("Unable to save changes " +
"about removed tracked sign at %s", block.getLocation()));
}
}
}, 5);
}
}
/**
* Performs the transaction to pay for the paid sign
*
* @param paidSign <p>The paid sign a match has been found for</p>
* @param player <p>The player that created the sign</p>
* @param event <p>The sign change event that caused the sign to be created</p>
*/
private void performPaidSignTransaction(PaidSign paidSign, Player player, SignChangeEvent event) {
double cost = paidSign.getCost();
boolean canAfford = EconomyManager.canAfford(player, cost);
if (!canAfford) {
PaidSigns.getStringFormatter().displayErrorMessage(player, replaceCost(cost, PaidSignsTranslatableMessage.ERROR_CANNOT_AFFORD));
event.setCancelled(true);
} else {
EconomyManager.withdraw(player, cost);
PaidSigns.getStringFormatter().displaySuccessMessage(player, replaceCost(cost, PaidSignsTranslatableMessage.SUCCESS_PAID_FOR_SIGN));
try {
TrackedSignManager.addTrackedSign(event.getBlock().getLocation(), player.getUniqueId(), cost, event.getSide());
} catch (IOException ignored) {
}
} }
} }
/** /**
* Tests all weak matches of paid signs to check if a strong match is found * Replaces cost and unit placeholders in the given message
* *
* @param lineIndex <p>The index of the currently managed line</p> * @param cost <p>The cost to insert</p>
* @param line <p>The text on the currently managed line</p> * @param message <p>The original message to replace the cost placeholder for</p>
* @param matchingSigns <p>The signs that weakly match the </p> * @return <p>The message with the cost instead of the cost placeholder</p>
* @param event <p>The triggered sign change event</p>
* @return <p>True if a match was found and thw work is finished</p>
*/ */
private boolean testMatchingSigns(short lineIndex, String line, List<PaidSign> matchingSigns, SignChangeEvent event) { private String replaceCost(double cost, TranslatableMessage message) {
for (PaidSign paidSign : matchingSigns) { return PaidSigns.getStringFormatter().replacePlaceholder(message,
if (paidSign.matches(lineIndex, line)) { "{cost}", EconomyManager.format(cost));
Player player = event.getPlayer();
double cost = paidSign.getCost();
boolean canAfford = EconomyManager.canAfford(player, cost);
if (!canAfford) {
player.sendMessage("[PaidSigns] You cannot afford to create this sign");
event.setCancelled(true);
} else {
String unit = EconomyManager.getCurrency(cost != 1);
player.sendMessage(String.format("[PaidSigns] You paid %.2f %s to create the sign", cost, unit));
EconomyManager.withdraw(player, cost);
}
return true;
}
}
return false;
} }
} }

View File

@ -2,7 +2,6 @@ package net.knarcraft.paidsigns.manager;
import net.milkbowl.vault.economy.Economy; import net.milkbowl.vault.economy.Economy;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
/** /**
* A manager that performs all Economy tasks * A manager that performs all Economy tasks
@ -36,17 +35,13 @@ public final class EconomyManager {
} }
/** /**
* Gets the name of the used currency * Formats the given amount of currency according to the economy plugin's format
* *
* @param plural <p>Whether to get the plural name or the singular name</p> * @param amount <p>The amount of currency to format</p>
* @return <p>The name of the used currency</p> * @return <p>The formatted string</p>
*/ */
public static String getCurrency(boolean plural) { public static String format(double amount) {
if (plural) { return economy.format(amount);
return economy.currencyNamePlural();
} else {
return economy.currencyNameSingular();
}
} }
/** /**
@ -55,8 +50,18 @@ public final class EconomyManager {
* @param player <p>The player to withdraw money from</p> * @param player <p>The player to withdraw money from</p>
* @param cost <p>The amount of money to withdraw</p> * @param cost <p>The amount of money to withdraw</p>
*/ */
public static void withdraw(Player player, double cost) { public static void withdraw(OfflinePlayer player, double cost) {
economy.withdrawPlayer(player, cost); economy.withdrawPlayer(player, cost);
} }
/**
* Deposits a given sum into the given player's account
*
* @param player <p>The player to deposit money to</p>
* @param sum <p>The amount of money to deposit</p>
*/
public static void deposit(OfflinePlayer player, double sum) {
economy.depositPlayer(player, sum);
}
} }

View File

@ -2,32 +2,33 @@ package net.knarcraft.paidsigns.manager;
import net.knarcraft.paidsigns.PaidSigns; import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.PaidSign; import net.knarcraft.paidsigns.container.PaidSign;
import net.knarcraft.paidsigns.container.PaidSignCondition;
import net.knarcraft.paidsigns.property.OptionState; import net.knarcraft.paidsigns.property.OptionState;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.Arrays;
import java.util.List; import java.util.HashMap;
import java.util.function.Predicate; import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger;
/** /**
* A manager that keeps track of all registered paid signs * A manager that keeps track of all registered paid signs
*/ */
public final class PaidSignManager { public final class PaidSignManager {
private final List<PaidSign> paidSigns; private final Map<String, PaidSign> paidSigns;
private static final File signsFile = new File(PaidSigns.getInstance().getDataFolder(), "data.yml"); private static final File signsFile = new File(PaidSigns.getInstance().getDataFolder(), "data.yml");
private static final String signLineIdSeparator = "-,_,-";
/** /**
* Instantiate a new paid sign manager * Instantiate a new paid sign manager
* *
* @param paidSigns <p>The paid signs this manager should manage</p> * @param paidSigns <p>The paid signs this manager should manage</p>
*/ */
public PaidSignManager(List<PaidSign> paidSigns) { public PaidSignManager(Map<String, PaidSign> paidSigns) {
this.paidSigns = paidSigns; this.paidSigns = paidSigns;
} }
@ -38,36 +39,35 @@ public final class PaidSignManager {
* @throws IOException <p>If unable to write to the signs file</p> * @throws IOException <p>If unable to write to the signs file</p>
*/ */
public void addPaidSign(PaidSign paidSign) throws IOException { public void addPaidSign(PaidSign paidSign) throws IOException {
this.paidSigns.add(paidSign); this.paidSigns.put(paidSign.getName(), paidSign);
saveSigns(this.paidSigns); saveSigns();
} }
/** /**
* Removes a paid sign from this paid sign manager * Removes a paid sign from this paid sign manager
* *
* @param id <p>The identifier for the paid sign to remove</p> * @param name <p>The name of the paid sign to remove</p>
* @param line <p>The line the identifier has to match to be valid</p>
* @return <p>True if a sign was removed</p> * @return <p>True if a sign was removed</p>
* @throws IOException <p>If unable to write to the signs file</p> * @throws IOException <p>If unable to write to the signs file</p>
*/ */
public boolean removePaidSign(String id, short line) throws IOException { public boolean removePaidSign(String name) throws IOException {
boolean removed = this.paidSigns.removeIf((sign) -> sign.getId().equals(id) && sign.getLineIndex() == line); boolean removed = this.paidSigns.remove(name) != null;
if (!removed) { if (!removed) {
return false; return false;
} } else {
saveSigns(this.paidSigns); saveSigns();
return true; return true;
} }
}
/** /**
* Gets the paid signs that match the given properties * Gets the paid sign with the given name
* *
* @param cleanId <p>The clean id to search for</p> * @param name <p>The paid sign with the given name</p>
* @param line <p>The line number to search for</p> * @return <p>The paid sign with the given name, or null if it does not exist</p>
* @return <p>The paid signs that matched the given properties</p>
*/ */
public List<PaidSign> getPaidSigns(String cleanId, short line) { public PaidSign getPaidSign(String name) {
return filterPaidSigns(filterPaidSigns(paidSigns, line), cleanId); return paidSigns.get(name);
} }
/** /**
@ -75,19 +75,8 @@ public final class PaidSignManager {
* *
* @return <p>All registered paid signs</p> * @return <p>All registered paid signs</p>
*/ */
public List<PaidSign> getAllPaidSigns() { public Map<String, PaidSign> getAllPaidSigns() {
return new ArrayList<>(paidSigns); return new HashMap<>(paidSigns);
}
/**
* Filters a list of paid signs to match the given line number
*
* @param paidSigns <p>The list of paid signs to start with</p>
* @param line <p>The line number to filter by</p>
* @return <p>The filtered list of paid signs</p>
*/
private static List<PaidSign> filterPaidSigns(List<PaidSign> paidSigns, short line) {
return filterPaidSigns(paidSigns, (paidSign) -> paidSign.getLineIndex() == line);
} }
/** /**
@ -95,66 +84,105 @@ public final class PaidSignManager {
* *
* @return <p>The loaded paid signs</p> * @return <p>The loaded paid signs</p>
*/ */
public static List<PaidSign> loadSigns() { public static Map<String, PaidSign> loadSigns() {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile); YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
ConfigurationSection signSection = configuration.getConfigurationSection("paidSigns"); ConfigurationSection signSection = configuration.getConfigurationSection("paidSigns");
if (signSection == null) { if (signSection == null) {
PaidSigns.getInstance().getLogger().log(Level.WARNING, "Signs section not found in data.yml"); PaidSigns.getInstance().getLogger().log(Level.INFO, "Signs section not found in data.yml");
return new ArrayList<>(); return new HashMap<>();
} }
List<PaidSign> paidSigns = new ArrayList<>(); Map<String, PaidSign> paidSigns = new HashMap<>();
for (String combinedId : signSection.getKeys(false)) { for (String name : signSection.getKeys(false)) {
String[] idParts = combinedId.split(signLineIdSeparator); double cost = signSection.getDouble(name + ".cost");
short lineNumber = Short.parseShort(idParts[0]); String permission = signSection.getString(name + ".permission");
String id = idParts[1]; OptionState ignoreCase = OptionState.getFromBoolean(signSection.getBoolean(name + ".ignoreCase"));
double cost = signSection.getDouble(combinedId + ".cost"); OptionState ignoreColor = OptionState.getFromBoolean(signSection.getBoolean(name + ".ignoreColor"));
OptionState ignoreCase = OptionState.getFromBoolean(signSection.getBoolean(combinedId + ".ignoreCase")); boolean matchAnyCondition = signSection.getBoolean(name + ".matchAnyCondition");
OptionState ignoreColor = OptionState.getFromBoolean(signSection.getBoolean(combinedId + ".ignoreColor")); PaidSign sign = new PaidSign(name, cost, permission, ignoreCase, ignoreColor, matchAnyCondition);
paidSigns.add(new PaidSign(id, lineNumber, cost, ignoreCase, ignoreColor)); loadConditions(signSection, sign);
paidSigns.put(name, sign);
} }
return paidSigns; return paidSigns;
} }
/**
* Saves all signs registered to this paid sign manager
*
* @throws IOException <p>If unable to write to the signs file</p>
*/
public void saveSigns() throws IOException {
saveSigns(this.paidSigns);
}
/** /**
* Saves the given paid signs to the signs file * Saves the given paid signs to the signs file
* *
* @param signs <p>The signs to save</p> * @param signs <p>The signs to save</p>
* @throws IOException <p>If unable to write to the signs file</p> * @throws IOException <p>If unable to write to the signs file</p>
*/ */
public static void saveSigns(List<PaidSign> signs) throws IOException { public static void saveSigns(Map<String, PaidSign> signs) throws IOException {
try {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile); YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
ConfigurationSection signSection = configuration.createSection("paidSigns"); ConfigurationSection signSection = configuration.createSection("paidSigns");
for (PaidSign sign : signs) { for (PaidSign sign : signs.values()) {
String signId = sign.getLineIndex() + signLineIdSeparator + sign.getId(); saveSign(signSection, sign);
signSection.set(signId + ".cost", sign.getCost());
signSection.set(signId + ".ignoreCase", sign.getIgnoreCase());
signSection.set(signId + ".ignoreColor", sign.getIgnoreColor());
} }
configuration.save(signsFile); configuration.save(signsFile);
} catch (IOException exception) {
Logger logger = PaidSigns.getInstance().getLogger();
logger.log(Level.SEVERE, "Exception encountered while trying to write " +
"to the data file");
logger.log(Level.SEVERE, Arrays.toString(exception.getStackTrace()));
throw exception;
}
} }
/** /**
* Filters a list of paid signs to match the given clean id * Saves the given sign to the given configuration section
* *
* @param paidSigns <p>The list of paid signs to start with</p> * @param signSection <p>The configuration section to save the sign to</p>
* @param cleanId <p>The clean id to filter by</p> * @param sign <p>The sign to save</p>
* @return <p>The filtered list of paid signs</p>
*/ */
private static List<PaidSign> filterPaidSigns(List<PaidSign> paidSigns, String cleanId) { private static void saveSign(ConfigurationSection signSection, PaidSign sign) {
return filterPaidSigns(paidSigns, (paidSign) -> paidSign.getCleanId().equals(cleanId)); String name = sign.getName();
signSection.set(name + ".cost", sign.getCost());
signSection.set(name + ".permission", sign.getPermission());
signSection.set(name + ".ignoreCase", sign.getIgnoreCase());
signSection.set(name + ".ignoreColor", sign.getIgnoreColor());
signSection.set(name + ".matchAnyCondition", sign.matchAnyCondition());
ConfigurationSection conditionsSection = signSection.createSection(name + ".conditions");
Map<Short, PaidSignCondition> signConditions = sign.getConditions();
for (short lineIndex : signConditions.keySet()) {
PaidSignCondition condition = signConditions.get(lineIndex);
conditionsSection.set(lineIndex + ".stringToMatch", condition.stringToMatch());
conditionsSection.set(lineIndex + ".executeRegEx", condition.executeRegex());
conditionsSection.set(lineIndex + ".ignoreCase", condition.ignoreCase());
conditionsSection.set(lineIndex + ".ignoreColor", condition.ignoreColor());
}
} }
/** /**
* Filters a list of paid signs using the given predicate * Loads any saved paid sign conditions and applies them to the given sign
* *
* @param paidSigns <p>The list of paid signs to start with</p> * @param signSection <p>The configuration section containing sign information</p>
* @param predicate <p>The predicate used to filter paid signs</p> * @param sign <p>The sign to load conditions for</p>
* @return <p>The filtered list of paid signs</p>
*/ */
private static List<PaidSign> filterPaidSigns(List<PaidSign> paidSigns, Predicate<PaidSign> predicate) { private static void loadConditions(ConfigurationSection signSection, PaidSign sign) {
return new ArrayList<>(paidSigns).stream().filter(predicate).toList(); ConfigurationSection conditionSection = signSection.getConfigurationSection(sign.getName() + ".conditions");
if (conditionSection != null) {
for (String lineIndex : conditionSection.getKeys(false)) {
short lineNumber = Short.parseShort(lineIndex);
String stringToMatch = conditionSection.getString(lineIndex + ".stringToMatch");
boolean executeRegEx = conditionSection.getBoolean(lineIndex + ".executeRegEx");
boolean ignoreConditionCase = conditionSection.getBoolean(lineIndex + ".ignoreCase");
boolean ignoreConditionColor = conditionSection.getBoolean(lineIndex + ".ignoreColor");
sign.addCondition(lineNumber, stringToMatch, executeRegEx,
OptionState.getFromBoolean(ignoreConditionCase),
OptionState.getFromBoolean(ignoreConditionColor));
}
}
} }
} }

View File

@ -0,0 +1,260 @@
package net.knarcraft.paidsigns.manager;
import net.knarcraft.paidsigns.PaidSigns;
import net.knarcraft.paidsigns.container.TrackedSign;
import net.knarcraft.paidsigns.formatting.PaidSignsTranslatableMessage;
import net.knarcraft.paidsigns.utility.SignHelper;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.block.sign.Side;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
/**
* A manager for keeping track of plugin-signs created by players
*/
public final class TrackedSignManager {
private static Map<Location, TrackedSign> trackedSigns = new HashMap<>();
private static final File signsFile = new File(PaidSigns.getInstance().getDataFolder(), "data.yml");
private TrackedSignManager() {
}
/**
* Adds a tracked sign to the manager
*
* @param signLocation <p>The location the sign was created at</p>
* @param playerId <p>The unique id of the player that created the sign</p>
* @param cost <p>The cost the player paid</p>
* @param side <p>The side of the sign the player paid for</p>
* @throws IOException <p>If unable to save the tracked signs</p>
*/
public static void addTrackedSign(Location signLocation, UUID playerId, double cost, Side side) throws IOException {
if (trackedSigns.containsKey(signLocation)) {
// If there is already a tracked sign, overwrite it, while updating only the correct cost
TrackedSign oldSign = trackedSigns.get(signLocation);
if (side == Side.FRONT) {
trackedSigns.put(signLocation, new TrackedSign(playerId, cost, oldSign.backCost()));
} else {
trackedSigns.put(signLocation, new TrackedSign(playerId, oldSign.frontCost(), cost));
}
} else {
trackedSigns.put(signLocation, TrackedSign.getInstance(playerId, side, cost));
}
saveTrackedSigns();
}
/**
* Removes a tracked sign from the manager
*
* @param signLocation <p>The location the sign was removed from</p>
* @param refund <p>Whether to perform a refund after un-tracking the sign</p>
* @param forceRefund <p>Whether to force a refund, even if refunding is disabled</p>
* @param side <p>The side of the sign to un-track</p>
* @throws IOException <p>If unable to save the tracked signs</p>
*/
public static void removeTrackedSign(Location signLocation, boolean refund, boolean forceRefund,
Side side) throws IOException {
if (!trackedSigns.containsKey(signLocation)) {
return;
}
TrackedSign trackedSign = trackedSigns.get(signLocation);
Side oppositeSide = getOppositeSide(side);
double oppositeCost = trackedSign.getCost(oppositeSide);
boolean isInUse = oppositeCost > 0;
if (!isInUse) {
trackedSigns.remove(signLocation);
} else {
// If the opposite side has a cost, create a new sign with only that cost
trackedSigns.put(signLocation, TrackedSign.getInstance(trackedSign.playerId(), oppositeSide, oppositeCost));
}
saveTrackedSigns();
if (trackedSign.getCost(side) > 0) {
refund(trackedSign, refund, forceRefund, side);
}
}
/**
* Gets the opposite sign side of the given side
*
* @param side <p>The side to get the opposite of</p>
* @return <p>The opposite sign side</p>
*/
private static Side getOppositeSide(Side side) {
if (side == Side.FRONT) {
return Side.BACK;
} else {
return Side.FRONT;
}
}
/**
* Removes a tracked sign from the manager
*
* @param signLocation <p>The location the sign was removed from</p>
* @param refund <p>Whether to perform a refund after un-tracking the sign</p>
* @param forceRefund <p>Whether to force a refund, even if refunding is disabled</p>
* @throws IOException <p>If unable to save the tracked signs</p>
*/
public static void removeTrackedSign(Location signLocation, boolean refund, boolean forceRefund) throws IOException {
if (!trackedSigns.containsKey(signLocation)) {
return;
}
TrackedSign trackedSign = trackedSigns.get(signLocation);
trackedSigns.remove(signLocation);
saveTrackedSigns();
refundBothSides(trackedSign, refund, forceRefund);
}
/**
* Loads all tracked signs from the data file
*/
public static void loadTrackedSigns() {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
ConfigurationSection signSection = configuration.getConfigurationSection("trackedSigns");
trackedSigns = new HashMap<>();
if (signSection == null) {
PaidSigns.getInstance().getLogger().log(Level.INFO, "Tracked signs section not found in data.yml");
return;
}
for (String key : signSection.getKeys(false)) {
loadSign(signSection, key);
}
//Save tracked signs in case some were invalidated after loading
try {
TrackedSignManager.saveTrackedSigns();
} catch (IOException e) {
PaidSigns.getInstance().getLogger().log(Level.WARNING, "Unable to save tracked signs");
}
}
/**
* Loads a sign from the save file
*
* @param signSection <p>The configuration section containing signs</p>
* @param key <p>The sign key which is also the sign's location</p>
*/
private static void loadSign(ConfigurationSection signSection, String key) {
String[] locationParts = key.split(",");
Location signLocation;
try {
signLocation = new Location(Bukkit.getWorld(UUID.fromString(locationParts[0])),
Double.parseDouble(locationParts[1]), Double.parseDouble(locationParts[2]),
Double.parseDouble(locationParts[3]));
} catch (NumberFormatException exception) {
PaidSigns.getInstance().getLogger().log(Level.SEVERE, "Unable to load tracked sign " + key + ": " +
exception.getMessage());
return;
}
double frontCost;
double backCost = 0;
if (signSection.contains(key + ".cost")) {
// Migrate from single side cost to two side cost
frontCost = signSection.getDouble(key + ".cost");
signSection.set(key + ".cost", null);
} else {
frontCost = signSection.getDouble(key + ".frontCost");
}
if (signSection.contains(key + ".backCost")) {
backCost = signSection.getDouble(key + ".backCost");
}
UUID playerId = UUID.fromString(Objects.requireNonNull(signSection.getString(key + ".playerId")));
TrackedSign trackedSign = new TrackedSign(playerId, frontCost, backCost);
//Prevent destroyed signs from being tracked indefinitely
if (!SignHelper.isSign(signLocation.getBlock())) {
Bukkit.getScheduler().scheduleSyncDelayedTask(PaidSigns.getInstance(), () -> {
PaidSigns.getInstance().getLogger().log(Level.WARNING, "The sign at " + signLocation +
" no longer exists. Removing from sign tracker. Refunding the player.");
refundBothSides(trackedSign, true, false);
}, 100);
return;
}
trackedSigns.put(signLocation, trackedSign);
}
/**
* Saves the managed tracked signs to the data file
*
* @throws IOException <p>If unable to write to the data file</p>
*/
public static void saveTrackedSigns() throws IOException {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(signsFile);
ConfigurationSection signSection = configuration.createSection("trackedSigns");
for (Location signLocation : trackedSigns.keySet()) {
TrackedSign sign = trackedSigns.get(signLocation);
String locationString = Objects.requireNonNull(signLocation.getWorld()).getUID() + "," +
signLocation.getBlockX() + "," + signLocation.getBlockY() + "," + signLocation.getBlockZ();
signSection.set(locationString + ".frontCost", sign.frontCost());
signSection.set(locationString + ".backCost", sign.backCost());
signSection.set(locationString + ".playerId", sign.playerId().toString());
}
configuration.save(signsFile);
}
/**
* Refunds the player for the sign if refunds are enabled and refund is set to true
*
* @param trackedSign <p>The tracked sign to refund for</p>
* @param refund <p>Whether to actually refund</p>
* @param forceRefund <p>Whether to force a refund, even if refunding is disabled</p>
*/
private static void refund(TrackedSign trackedSign, boolean refund, boolean forceRefund, Side signSide) {
if ((!PaidSigns.getInstance().areRefundsEnabled() || !refund) && !forceRefund) {
return;
}
double cost = trackedSign.getCost(signSide);
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(trackedSign.playerId());
double refundSum;
if (forceRefund) {
//In the case where a refund is forced, the normal refund rate should not apply
refundSum = cost;
} else {
refundSum = cost / 100 * PaidSigns.getInstance().getRefundPercentage();
}
EconomyManager.deposit(offlinePlayer, refundSum);
if (offlinePlayer instanceof Player player) {
PaidSigns.getStringFormatter().displaySuccessMessage(player,
PaidSigns.getStringFormatter().replacePlaceholder(PaidSignsTranslatableMessage.SUCCESS_REFUNDED,
"{cost}", EconomyManager.format(refundSum)));
}
}
/**
* Refunds the costs for both sides of a paid sign
*
* @param trackedSign <p>The tracked sign to refund for</p>
* @param refund <p>Whether to perform a refund after un-tracking the sign</p>
* @param forceRefund <p>Whether to force a refund, even if refunding is disabled</p>
*/
private static void refundBothSides(TrackedSign trackedSign, boolean refund, boolean forceRefund) {
if (trackedSign.frontCost() > 0) {
refund(trackedSign, refund, forceRefund, Side.FRONT);
}
if (trackedSign.backCost() > 0) {
refund(trackedSign, refund, forceRefund, Side.BACK);
}
}
}

View File

@ -24,13 +24,14 @@ public enum OptionState {
* Gets the boolean value of the given option state if it's boolean compatible * Gets the boolean value of the given option state if it's boolean compatible
* *
* @param optionState <p>The option state to get the boolean from</p> * @param optionState <p>The option state to get the boolean from</p>
* @return <p>The boolean value, or an illegal argument exception if called on DEFAULT</p> * @param defaultValue <p>The default value to use if the option state is DEFAULT</p>
* @return <p>The boolean value</p>
*/ */
public static boolean getBooleanValue(OptionState optionState) { public static boolean getBooleanValue(OptionState optionState, boolean defaultValue) {
return switch (optionState) { return switch (optionState) {
case TRUE -> true; case TRUE -> true;
case FALSE -> false; case FALSE -> false;
case DEFAULT -> throw new IllegalArgumentException("No boolean value available for DEFAULT"); case DEFAULT -> defaultValue;
}; };
} }

View File

@ -0,0 +1,63 @@
package net.knarcraft.paidsigns.property;
/**
* A representation of a paid sign condition's editable properties
*/
public enum PaidSignConditionProperty {
/**
* The string used to trigger the paid sign condition
*/
STRING_TO_MATCH("stringToMatch"),
/**
* Whether to execute the match string as a regular expression
*/
EXECUTE_REG_EX("executeRegEx"),
/**
* Whether to ignore case for the sign condition
*/
IGNORE_CASE("ignoreCase"),
/**
* Whether to ignore color for the sign condition
*/
IGNORE_COLOR("ignoreColor");
private final String stringRepresentation;
/**
* Instantiates a new paid sign condition property
*
* @param stringRepresentation <p>The string representation of the paid sign condition property</p>
*/
PaidSignConditionProperty(String stringRepresentation) {
this.stringRepresentation = stringRepresentation;
}
/**
* Gets the string representation of this paid sign condition property
*
* @return <p>The string representation of this paid sign condition property</p>
*/
public String getStringRepresentation() {
return this.stringRepresentation;
}
/**
* Gets the paid sign property matching the given string
*
* @param propertyString <p>The string representing a paid sign condition property</p>
* @return <p>The matching paid sign condition property, or null if no such property exists</p>
*/
public static PaidSignConditionProperty getFromString(String propertyString) {
for (PaidSignConditionProperty paidSignProperty : PaidSignConditionProperty.values()) {
if (paidSignProperty.getStringRepresentation().equalsIgnoreCase(propertyString)) {
return paidSignProperty;
}
}
return null;
}
}

View File

@ -0,0 +1,73 @@
package net.knarcraft.paidsigns.property;
/**
* A representation of a paid sign's editable properties
*/
public enum PaidSignProperty {
/**
* The name of a paid sign
*/
NAME("name"),
/**
* The cost of creating a sign matching the paid sign
*/
COST("cost"),
/**
* The permission required to create the plugin sign matching the paid sign
*/
PERMISSION("permission"),
/**
* Whether to ignore case for any sign conditions added to the sign
*/
IGNORE_CASE("ignoreCase"),
/**
* Whether to ignore color for any sign conditions added to the sign
*/
IGNORE_COLOR("ignoreColor"),
/**
* Whether to treat a single condition match as a full match
*/
MATCH_ANY_CONDITION("matchAnyCondition");
private final String stringRepresentation;
/**
* Instantiates a new paid sign property
*
* @param stringRepresentation <p>The string representation of the paid sign property</p>
*/
PaidSignProperty(String stringRepresentation) {
this.stringRepresentation = stringRepresentation;
}
/**
* Gets the string representation of this paid sign property
*
* @return <p>The string representation of this paid sign property</p>
*/
public String getStringRepresentation() {
return this.stringRepresentation;
}
/**
* Gets the paid sign property matching the given string
*
* @param propertyString <p>The string representing a paid sign property</p>
* @return <p>The matching paid sign property, or null if no such property exists</p>
*/
public static PaidSignProperty getFromString(String propertyString) {
for (PaidSignProperty paidSignProperty : PaidSignProperty.values()) {
if (paidSignProperty.getStringRepresentation().equalsIgnoreCase(propertyString)) {
return paidSignProperty;
}
}
return null;
}
}

View File

@ -1,43 +0,0 @@
package net.knarcraft.paidsigns.utility;
import net.md_5.bungee.api.ChatColor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A helper class for dealing with colors
*/
public final class ColorHelper {
private ColorHelper() {
}
/**
* Strips all color codes from the given message
*
* @param message <p>The message to strip color codes from</p>
* @return <p>The message without color codes</p>
*/
public static String stripColorCodes(String message) {
return ChatColor.stripColor(translateAllColorCodes(message));
}
/**
* Translates all found color codes to formatting in a string
*
* @param message <p>The string to search for color codes</p>
* @return <p>The message with color codes translated</p>
*/
public static String translateAllColorCodes(String message) {
message = ChatColor.translateAlternateColorCodes('&', message);
Pattern pattern = Pattern.compile("(#[a-fA-F0-9]{6})");
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
message = message.replace(matcher.group(), "" + ChatColor.of(matcher.group()));
}
return message;
}
}

View File

@ -0,0 +1,178 @@
package net.knarcraft.paidsigns.utility;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import net.knarcraft.paidsigns.PaidSigns;
import org.bukkit.Bukkit;
import org.bukkit.permissions.Permission;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
/**
* A helper class for providing common tab complete options
*/
public final class PaidSignsTabCompleteHelper {
private static List<String> plugins;
private static Map<String, List<String>> permissions;
private PaidSignsTabCompleteHelper() {
}
/**
* Gets the available boolean values for tab completion
*
* @return <p>The available boolean values</p>
*/
public static List<String> getBooleans() {
List<String> booleans = new ArrayList<>();
booleans.add("true");
booleans.add("false");
return booleans;
}
/**
* Gets the available option states for tab completion
*
* @return <p>The available option states</p>
*/
public static List<String> getOptionStates() {
List<String> optionStates = getBooleans();
optionStates.add("default");
return optionStates;
}
/**
* Gets the names of all registered paid signs
*
* @return <p>The names of all registered paid signs</p>
*/
public static List<String> getPaidSignNames() {
Set<String> paidSignNames = PaidSigns.getInstance().getSignManager().getAllPaidSigns().keySet();
List<String> quotedNames = new ArrayList<>(paidSignNames.size());
for (String paidSignName : paidSignNames) {
quotedNames.add("\"" + paidSignName + "\"");
}
return quotedNames;
}
/**
* Gets all the choices for lines on a sign
*
* @return <p>All the line indices of a sign</p>
*/
public static List<String> getSignLines() {
List<String> lines = new ArrayList<>();
for (int i = 1; i < 5; i++) {
lines.add(String.valueOf(i));
}
return lines;
}
/**
* Gets some valid costs as example values
*
* @return <p>Some valid costs</p>
*/
public static List<String> getCosts() {
List<String> costs = new ArrayList<>();
costs.add("1");
costs.add("5");
costs.add("10");
costs.add("15");
return costs;
}
/**
* Gets some example paid sign names for auto-completion
*
* @return <p>Some example paid sign names</p>
*/
public static List<String> getExamplePaidSignNames() {
List<String> names = new ArrayList<>();
names.add("sign1");
names.add("\"lift up sign\"");
names.add("\"lift down sign\"");
return names;
}
/**
* Gets some example condition matching strings for tab completion
*
* @return <p>Some example "stringToMatch" values</p>
*/
public static List<String> getExampleConditionStrings() {
List<String> conditionStrings = new ArrayList<>();
conditionStrings.add("[Gate]");
conditionStrings.add("\"[Lift Up]\"");
conditionStrings.add("\"[Lift Down]\"");
return conditionStrings;
}
/**
* Gets the tab complete value for the permission typed
*
* @param typedNode <p>The full permission node typed by the player</p>
* @return <p>All known valid auto-complete options</p>
*/
public static List<String> tabCompletePermission(String typedNode) {
if (plugins == null) {
loadAvailablePermissions();
}
List<String> output;
if (typedNode.contains(".")) {
List<String> matchingPermissions = permissions.get(typedNode.substring(0, typedNode.lastIndexOf(".")));
if (matchingPermissions == null) {
output = new ArrayList<>();
} else {
//Filter by the typed text
output = TabCompletionHelper.filterMatchingStartsWith(matchingPermissions, typedNode);
}
} else {
output = plugins;
}
//Add previous permissions in the comma-separated lists as a prefix
return output;
}
/**
* Loads all permissions available from bukkit plugins
*/
private static void loadAvailablePermissions() {
plugins = new ArrayList<>();
permissions = new HashMap<>();
for (Permission permission : Bukkit.getPluginManager().getPermissions()) {
loadPermission(permission.getName());
}
}
/**
* Loads a given permission into the proper lists and maps
*
* @param permissionName <p>The permission to load</p>
*/
private static void loadPermission(String permissionName) {
String[] permissionParts = permissionName.split("\\.");
if (permissionParts.length == 1 && !plugins.contains(permissionParts[0])) {
plugins.add(permissionParts[0]);
} else if (permissionParts.length > 1) {
StringJoiner pathJoiner = new StringJoiner(".");
for (int j = 0; j < permissionParts.length - 1; j++) {
pathJoiner.add(permissionParts[j]);
}
String path = pathJoiner.toString();
List<String> permissionList = permissions.computeIfAbsent(path, k -> new ArrayList<>());
permissionList.add(permissionName);
loadPermission(path);
}
}
}

View File

@ -0,0 +1,25 @@
package net.knarcraft.paidsigns.utility;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
/**
* A helper class for dealing with signs
*/
public final class SignHelper {
private SignHelper() {
}
/**
* Checks whether the given block is a sign
*
* @param block <p>The block to check</p>
* @return <p>True if the block is a sign</p>
*/
public static boolean isSign(Block block) {
return block.getState() instanceof Sign;
}
}

View File

@ -6,15 +6,30 @@ import java.util.List;
/** /**
* A tokenizer for being able to support quotes in commands * A tokenizer for being able to support quotes in commands
*/ */
public class Tokenizer { public final class Tokenizer {
private Tokenizer() {
}
/** /**
* Tokenizes a string * Tokenizes a string
* *
* @param input <p>A string.</p> * @param input <p>A string</p>
* @return <p>A list of tokens.</p> * @return <p>A list of tokens</p>
*/ */
public static List<String> tokenize(String input) { public static List<String> tokenize(String input) {
return tokenize(input, true);
}
/**
* Tokenizes a string
*
* @param input <p>A string</p>
* @param allowEmptyQuotes <p>Whether to treat "" as a token</p>
* @return <p>A list of tokens</p>
*/
public static List<String> tokenize(String input, boolean allowEmptyQuotes) {
List<String> tokens = new ArrayList<>(); List<String> tokens = new ArrayList<>();
boolean startedQuote = false; boolean startedQuote = false;
StringBuilder currentToken = new StringBuilder(); StringBuilder currentToken = new StringBuilder();
@ -29,7 +44,7 @@ public class Tokenizer {
case '"': case '"':
if (startedQuote) { if (startedQuote) {
//This quote signifies the end of the argument //This quote signifies the end of the argument
if (isNotEmpty(currentToken)) { if (allowEmptyQuotes || isNotEmpty(currentToken)) {
tokens.add(currentToken.toString()); tokens.add(currentToken.toString());
currentToken = new StringBuilder(); currentToken = new StringBuilder();
} }

View File

@ -1,8 +1,23 @@
# The currently enabled language. More languages can be added to the language file
language: en
# Whether to ignore the case (lowercase/uppercase) of the paid sign text. The option can be set on a per-sign basis, but # Whether to ignore the case (lowercase/uppercase) of the paid sign text. The option can be set on a per-sign basis, but
# this value is used if not specified. The correct value depends on whether the plugin signs it should match are case-sensitive or not. # this value is used if not specified. The correct value depends on whether the plugin signs it should match are
# case-sensitive or not.
ignoreCase: true ignoreCase: true
# Whether to ignore any color or formatting applied to the text when trying to match a paid sign's text. The option can # Whether to ignore any color or formatting applied to the text when trying to match a paid sign's text. The option can
# be set on a per-sign basis, but this value is used if not specified. The correct value depends on whether the plugin # be set on a per-sign basis, but this value is used if not specified. The correct value depends on whether the plugin
# signs it should match allow coloring or not. # signs it should match allow coloring or not.
ignoreColor: false ignoreColor: false
# Whether to enable refunds to the sign creator when a sign detected as a paid sign is broken (payment will always go
# to the original creator)
refundsEnabled: true
# The percentage of the paid sign cost to refund (0-100)
refundPercentage: 100
# Whether to refund when signs that players have paid for are broken by anything. This includes tnt, creepers, pistons
# and similar
refundAlways: false

View File

@ -1,35 +1,78 @@
name: PaidSigns name: PaidSigns
version: '${project.version}' version: '${project.version}'
main: net.knarcraft.paidsigns.PaidSigns main: net.knarcraft.paidsigns.PaidSigns
api-version: 1.18 api-version: '1.20'
prefix: PaidSigns prefix: PaidSigns
depend: [ Vault ] depend: [ Vault ]
authors: [ EpicKnarvik97 ] authors: [ EpicKnarvik97 ]
description: Add costs for creating plugin signs description: Add costs for creating plugin signs
website: https://git.knarcraft.net website: https://git.knarcraft.net/EpicKnarvik97/PaidSigns
commands: commands:
paidsigns:
description: Displays information about paid signs commands
usage: /<command>
permission: paidsigns.info
addpaidsign: addpaidsign:
aliases:
- aps
description: Used to add a new paid sign description: Used to add a new paid sign
usage: /<command> <id (the text to look for)> <line> <cost> [ignore case] [ignore color] usage: /<command> <name> <cost> [permission] [ignore case] [ignore color] [match any condition]
permission: paidsigns.add permission: paidsigns.manage
addpaidsigncondition:
aliases:
- apsc
description: Used to add a new match condition for a paid sign
usage: /<command> <name (of a paid sign)> <line number> <string to match> [executeRegEx] [ignoreCase] [ignoreColor]
permission: paidsigns.manage
listpaidsigns:
aliases:
- lps
description: Lists all previously added paid signs
usage: /<command> [sign name] [line number]
permission: paidsigns.manage
editpaidsign:
aliases:
- eps
description: Edits a property of a paid sign or a paid sign condition
usage: /<command> <sign name> <property>/<line number> [new value]/<property> [new value]
permission: paidsigns.manage
removepaidsigncondition:
aliases:
- rpsc
description: Used to remove a match condition from a paid sign
usage: /<command> <name (of a paid sign)> <line number>
permission: paidsigns.manage
removepaidsign: removepaidsign:
aliases:
- rps
description: Used to remove a paid sign description: Used to remove a paid sign
usage: /<command> <id (the text to look for)> <line> usage: /<command> <name (of a paid sign)>
permission: paidsigns.remove permission: paidsigns.manage
reload: reload:
aliases:
- r
description: Reloads paid signs from disk description: Reloads paid signs from disk
usage: /<command> usage: /<command>
permision: paidsigns.reload permission: paidsigns.reload
permissions: permissions:
paidsigns.*: paidsigns.*:
description: Alias to paidsigns.admin
default: false
children:
paidsigns.admin: true
paidsigns.admin:
description: Grants all paid signs permissions description: Grants all paid signs permissions
default: op default: op
children: children:
paidsigns.create: true paidsigns.info: true
paidsigns.paymentexempt: true paidsigns.paymentexempt: true
paidsigns.manage: true
paidsigns.reload: true paidsigns.reload: true
paidsigns.add: paidsigns.info:
description: Grants the permission to add/remove a paid sign description: Grants the permission to see command info
default: false
paidsigns.manage:
description: Grants the permission to add/remove/edit a paid sign
default: false default: false
paidsigns.reload: paidsigns.reload:
description: Grants the permissions to reload the plugin description: Grants the permissions to reload the plugin

View File

@ -0,0 +1,51 @@
en:
SUCCESS_ADDED_PAID_SIGN: "&bSuccessfully added new paid sign"
SUCCESS_ADDED_PAID_SIGN_CONDITION: "&bSuccessfully added new paid sign condition"
SUCCESS_UPDATED_PAID_SIGN: "&bSuccessfully updated the paid sign property"
SUCCESS_UPDATED_PAID_SIGN_CONDITION: "&bSuccessfully updated the paid sign condition property"
SUCCESS_REMOVED_PAID_SIGN: "&bSuccessfully removed paid sign"
SUCCESS_REMOVED_CONDITION: "&bSuccessfully removed paid sign condition"
SUCCESS_RELOADED: "&bSuccessfully reloaded configuration"
SUCCESS_PAID_FOR_SIGN: "&bYou paid &3{cost} &bto create the sign"
SUCCESS_REFUNDED: "&bYou were refunded &3{cost} &bfor your broken sign"
PAID_SIGNS_INFO: |
&f---&3Paid signs&f---
{signs}{nextPagePrompt}
&f-----------
PAID_SIGNS_INFO_FORMAT: |
&f| &b{name}
PAID_SIGNS_NEXT_PROMPT: |
&f| &3Use /lps {nextPage} to see more
PAID_SIGN_INFO: |
&f---&3Paid sign&f---
&f| &bName: &3{name}
&f| &bCost: &3{cost}
&f| &bPermission: &3{permission}
&f| &bIgnore case: &3{case}
&f| &bIgnore color: &3{color}
&f| &bMatch any condition: &3{any}
&f| &bSign conditions:
&3{conditions}
&f---------------
PAID_SIGN_INFO_CONDITION_FORMAT: |
&f| &b{line}: &3{condition}
PAID_SIGN_CONDITION_INFO: |
&f---&3Paid sign condition&f---
&f| &bPaid sign name: &3{name}
&f| &bCondition line: &3{line}
&f| &bMatch string: &3{match}
&f| &bExecute RegEx: &3{regex}
&f| &bIgnore case: &3{case}
&f| &bIgnore color: &3{color}
&f---------------
BOOLEAN_TRUE: "&2true"
BOOLEAN_FALSE: "&4false"
ERROR_INVALID_NUMBER: "&bYou provided an invalid number"
ERROR_NAME_DUPLICATE: "&bA paid sign with the same name already exists"
ERROR_EXCEPTION_OCCURRED: "&bAn exception occurred. Please notify the server administrator or check the server log."
ERROR_INVALID_INPUT: "&bInvalid input: {input}"
ERROR_PAID_SIGN_NOT_FOUND: "&bNo such paid sign exists"
ERROR_NO_SUCH_CONDITION: "&bThe paid sign you specified has no condition for line {line}"
ERROR_CANNOT_AFFORD: "&bYou cannot afford to create this sign. The cost is {cost}"
ERROR_INVALID_REGULAR_EXPRESSION: "&bThe provided regular expression is invalid"
ERROR_PROPERTY_NOT_RECOGNIZED: "&bThe property you tried to change was not recognized"