84 Commits
Author SHA1 Message Date
RednedEpic 697015bd18 BattleTracker v2.5.11.0 w/ full 1.15 support 2019-12-12 18:35:58 -06:00
Redned 196e4daed4 Update repo in Jenkinsfile 2019-10-13 16:23:14 -05:00
Redned 2831179915 Update README.md 2019-08-16 18:11:32 -05:00
RednedEpic 7d1d72e60b Add in missing shaded library 2019-08-10 15:57:17 -05:00
RednedEpic c35c8d6101 Add support to retrieve name of player from top stats 2019-08-02 16:38:17 -05:00
Redned 735b11d308 Add stats to README
- Add stats image from bStats to README
2019-07-04 16:16:17 -05:00
Redned 3f7b0052ad Update README.md 2019-06-22 17:03:17 -05:00
Clovis 11d2dc0ed6 Standardization + Updates 2019-06-17 06:33:48 -06:00
Redned b5108c04ac BattleTracker v2.5.10.4 w/ new BP repo URL 2019-06-16 18:28:22 -05:00
jarosenthal c9e80e26be Update BattleBukkitLib 2019-06-15 23:07:32 -04:00
jarosenthal fb02a4839d Update BattleCore and BukkitAdapter 2019-06-15 22:44:24 -04:00
jarosenthal 9893eb7f1c Update pom
Update pom
2019-06-15 22:39:11 -04:00
jarosenthal c460d0d90d Jenkins 2019-06-14 21:43:17 -04:00
RednedEpic bad3b5af9e Urgent sign fix
- Changed code to check if the BLOCKSTATE was an instance of a Sign, NOT the block itself.
2019-05-24 20:36:50 -05:00
RednedEpic f0bff019b4 BattleTracker v2.5.10.2 w/ basic 1.14 support 2019-05-22 22:56:15 -05:00
RednedEpic 12da5ad47b Add bStats support
- bStats is entirely anonymous and tracks only basic information about the server such as how many people are running the plugin, what type of software the server is running, etc. You can learn more about bStats here: https://bstats.org/. However if you would like to disable bStats integration, you can disable it within the "plugins/bStats/config.yml" file.
2019-05-02 18:08:13 -05:00
RednedEpic d45229fd33 BattleTracker v2.5.10.0 2019-05-02 17:59:33 -05:00
RednedEpic 0b727ddb7c Add api-version to plugin.yml
- This is so Bukkit recognizes BattleTracker as a 1.13+ plugin and doesn't try and use the legacy API.
2019-05-02 17:54:33 -05:00
RednedEpic e03ad5619c Add PlaceholderAPI support 2019-05-02 17:53:45 -05:00
RednedEpic a3702884ba 1.13 support 2019-05-02 17:51:32 -05:00
RednedEpic 5009a9ba21 Fix NPE when player is killed with a lored item
- Reported on Discord, apparently items with lores spit out some kind of error in console when a player is killed with them.
2019-05-02 17:35:29 -05:00
RednedEpic fe32a3a43f Update invalid package names 2019-05-02 17:31:49 -05:00
RednedEpic 081be955b9 Bump dependency versions in pom 2019-05-02 17:26:54 -05:00
Zach443 f272c0bc3e Removed Maven Antrun plugin
@europia79
2019-03-21 20:39:47 -05:00
Zach443 70343033dc Merge branch 'master' of https://github.com/BattlePlugins/BattleTracker 2019-03-21 20:34:55 -05:00
Redned 73bf885643 Update README.md 2019-03-10 12:51:26 -05:00
Zach443 3bfe013a1e Updated maven plugin versions
Made this more congruent with the other BattlePlugins
2018-12-02 21:22:42 -06:00
Zach443 9c35202bb2 BattleTracker v2.5.9.4 2016-03-27 18:15:11 -05:00
Zach443 4d5be5da33 Fixed issue causing some weird death messages 2016-03-27 18:12:01 -05:00
Zach443 ef35db18b2 Updated more deprecated methods 2016-03-14 01:59:30 -04:00
Zach443 14ec0c8860 Updated pom with spigot repository 2016-03-12 17:06:00 -05:00
Zach443 586860d426 BattleTracker v 2.5.9.3
- Updated getShooter and getOnlinePlayers to 1.9 compatible versions
	- Addressed #15
2016-03-12 16:51:57 -05:00
Zach443 697fd31e86 Updated usage of getOnlinePlayers() method in light of 1.9
getOnlinePlayers() now gets a collection rather than an array
2016-03-12 16:48:35 -05:00
Zach Fiorito 1437782712 Changed to updated version of getShooter()
The getShooter() method has been deprecated for some time now, as it returns LivingEntity as opposed to
ProjectileSource. In Spigot 1.9 this outdated version was removed and thus this update has finally been provoked.
2016-03-05 00:17:13 -06:00
Europia79 f94909a85a BattleTracker v2.5.9.2
- formats KD ratio to two decimal places for the cmd: /pvp
2015-08-12 19:12:27 -05:00
Europia79 47cbe28d46 Format KD ratio to two decimal places
for the commands:
./pvp
./pvp top [x] [team size]

Any php scripts will still get the float and have to format itself.
2015-08-12 19:08:16 -05:00
Europia79 db39c32cc1 BattleTracker v2.5.9.1
- added %l (Lore) for messages.yml
- updated to BattleBukkitLib v2.3.1
2015-08-12 18:44:38 -05:00
Europia79 ba053961ce Added Lore example to messages.yml
Example is under the yaml node: "pvp.bow" -- which is a list of random
things to say with a bow is used in a pvp kill/death.
2015-08-12 18:42:34 -05:00
Europia79 6147452518 Added Lore %l
https://github.com/BattlePlugins/BattleTracker/issues/10

- messages.yml

You can now add item lore to msgs with %l (that symbol is a lowercase
elle, which stands for Lore).
2015-08-12 18:39:17 -05:00
Europia79 c02c720ec0 BattleTracker v2.5.9
- made some signs easier to read
- refactored the build in pom.xml
2015-07-03 11:54:22 -05:00
Europia79 164f731b92 Made signs easier to see
https://github.com/BattlePlugins/BattleTracker/issues/8

The new Minecraft colors for &6-gold, &a-neon_green, &e-yellow are harder
to see in 1.8, so they were changed.

Also, there was a problem with light-gray being displayed by default
(instead of black by default). So anything that previously had no color
speicfied, is now &0 - black.

Ideally, this should all be configurable tho.
2015-06-17 13:58:24 -05:00
Europia79 5b62c8e120 Merge branch 'master' of https://github.com/BattlePluginsDev/BattleTracker 2015-06-13 02:44:17 -05:00
Europia79 fe75ccf9bd BattleTracker v2.5.8.11 updated to MC 1.8.7
Updating BattleTracker to Minecraft 1.8.7 pretty much meant updating
BattleBukkitLib. I pretty much just fixed all the compilation errors.

This is not tested.
2015-06-13 02:42:05 -05:00
clovisd 046f04bc46 Converting to GitHub Format (.md) 2015-05-26 22:03:42 +02:00
Clovis 39b7e5de53 Initial Update of Readme. More info in time. 2015-05-26 22:00:55 +02:00
Europia79 0c9a8aa488 Fixed typo in pom.xml
ant copy-file script:
changed BattleTracker.java to BattleTracker.jar
2014-12-27 02:26:50 -06:00
Europia79 ad171dfd4d BattleTracker v2.5.8.10
Removed BattlePluginsAPI

formatted Tracker.java

pom.xml:
- added repo
- added ant copy-file
- removed mc.battleplugins dependency
2014-12-27 02:22:26 -06:00
Europia79 8a1c5db7d4 Removed BattlePluginsAPI
People have been reporting this impossible error as a BattleArena bug.

Yes, it was impossible for BattleArena to produce this error.

Finally, someone produced a FULL server.log including the error and I was
able to trace it to BattleTracker.

Finally fixed. Another one bite the dust!

Good job team!
2014-12-27 02:11:21 -06:00
alkarin.v 47b0cbc0c4 Removed update to 1.2 2014-04-09 17:12:46 -05:00
alkarinv 0121f98bae Added LeaderboardHologram from MCForger 2014-03-17 19:01:18 -05:00
alkarinv acaa4bc171 Fix for pom 2014-03-01 10:57:49 -06:00
alkarinv 2c7cb8c936 Linked with BattlePluginsAPI 2014-02-28 09:50:05 -06:00
alkarinv 7ba70808cc 2.5.8.5:
Fixes:
* Fix for MyPets and Spigot NPE
2014-02-11 12:01:24 -06:00
alkarinv 12a06e7664 Fixed NPE error for signs, Fixed ignoreEntities problem 2014-02-03 11:44:06 -06:00
alkarinv 0f57b83ebd Updated pom version 2014-01-22 14:19:03 -06:00
alkarinv c2ff4a477a Fixed problem with plugin.yml not using values from pom 2014-01-22 14:12:02 -06:00
alkarinv e0c508a133 2.5.8:
New Features:
* Updated for 1.7.2 and 1.7.4
* new option to auto update plugin
2014-01-17 19:10:09 -06:00
alkarinv e3502eff6c Updated for 1.7.2 and 1.7.4, mainly through changes to BattleBukkitLib 2014-01-13 20:02:52 -06:00
alkarinv 0daeb59266 Jenkins commit 2013-12-11 18:51:20 -06:00
alkarin 4298d63852 Version 2.5.7.2:
Jenkins test
2013-09-29 20:21:51 -05:00
alkarin f930d9ef5f Testing jenkins/github 2013-09-29 20:17:36 -05:00
alkarin c27c125f25 Changed up messages and pom 2013-09-26 02:15:12 -05:00
alkarin 2d459b6c94 Updated poms 2013-09-20 11:00:33 -05:00
alkarin c2df5a7589 2.5.6
New Features;
* Better item displays ( removed the underscore )
* bow's can now be displayed separately in the messages.yml
Fixes:
* Fix for sqlite high loads
2013-09-18 23:50:04 -05:00
alkarin d6dbb20fb5 2.5.3.8:
Fixes:
* Fix for Updating
* Fix for Updating issue for KDRatio
2013-06-10 23:56:40 -05:00
alkarin 74e4f342a6 2.5.3.7:
New Features:
* New option for Ignore Worlds

Fixes:
* Fix for Updating
* Fix for Updating issue for KDRatio
* Fix for a NPE error with null statsigns
* Fix for an error message on server shutdown (harmless but annoying)
2013-05-22 23:26:19 -05:00
alkarin 4a0464ebd1 2.5.3.2:
Fixes:
* Fix for a NPE error with null statsigns
* Fix for an error message on server shutdown (harmless but annoying)

2.5.3:
Fixes:
* Sign updates are now done in a separate thread
* Sign updates should now take nearly no cpu usage
* Fix for multiworlds with signs being loaded too quickly
2013-04-28 23:15:03 -05:00
alkarin bc5c45903c 2.5.2.5:
New Features:
* Allow only killer/killee to get messages
* Signs can now be turned off completely in the config

2.5.2:
New Features:
* Added the command /bt resetRatings <db> : (requires op)
Fixes:
* Fixes for Sign update errors
2013-04-08 22:44:08 -05:00
alkarin 73f4888c2b * Added Top Signs
* Added Personal Signs
2013-03-25 02:38:11 -05:00
alkarin 265ca6e410 2.4
New Features:
* More messages
* more async calls to the database
* You can now ignore specified entities
* Top messages can now be customized
* Players can be hidden from top lists
** /bt hide <db> <player> <true | false>
2013-03-16 23:33:21 -05:00
alkarin 626b271dea 2.4
New Features:
* More messages
* more async calls to the database
* You can now ignore specified entities
* Top messages can now be customized
* Players can be hidden from top lists
** /bt hide <db> <player> <true | false>
2013-03-16 23:32:54 -05:00
alkarin 3c6bc07c50 New Events 2013-02-18 23:26:27 -06:00
alkarin 21bda247e1 Updated defaults 2012-12-20 22:56:36 -06:00
alkarin 4b3e787d06 Minor fixes 2012-12-02 14:22:54 -06:00
alkarin 3c8eaba790 2.2.0
New Features:
* added support for custom names on items

Fixes:
* changed ranking to rating where appropriate
2012-11-30 13:19:19 -06:00
alkarin b83a148ae6 Version 2.1.5.3
Fixed a problem with creating database names
2012-11-01 23:21:29 -05:00
alkarin 07519ad094 Version: 2.1.5
New Features:
* new command to set pvp and pve messages on/off through a command
** /bt set <pvp | pve> msg <true | false>
* PvE messages are now configurable
* PvP messages are now more configurable ( you can add in messages for specific items)
* Streak and Rampage messages are now more configurable

Fixes
* Some command fixes
2012-10-24 01:50:55 -05:00
alkarin 6dc0cdc3f3 changed executor 2012-10-23 14:25:00 -05:00
alkarin 6eb33760c7 Version 2.1.2.1:
Minor fixes
2012-10-02 01:53:41 -05:00
alkarin 2162a6630e Fix for VersusRecords when names had > 32 chars 2012-09-04 12:12:50 -05:00
alkarin b7fd2f5619 Fix for VersusRecords when names had > 32 chars 2012-09-04 12:09:40 -05:00
alkarin d900d1fcd0 Added sqlite support, new api methods 2012-08-24 14:31:51 -05:00
alkarin 1dab3b4baf added readme 2012-08-10 17:57:21 -05:00
alkarin f3c8285e8b first commit 2012-08-10 17:55:08 -05:00
127 changed files with 5830 additions and 10045 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
.gitea/CODEOWNERS linguist-detectable=false
-3
View File
@@ -1,3 +0,0 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
+1 -44
View File
@@ -1,45 +1,2 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
.idea/
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
### Run Server ###
run/
Vendored
+8
View File
@@ -0,0 +1,8 @@
library identifier: 'BattlePipelineLib@master', retriever: modernSCM(
[$class: 'GitSCMSource',
remote: 'https://github.com/BattlePlugins/BattlePipelineLib',
credentialsId: 'github-login'])
compileAndDeployMaven {
repo = 'BattleTracker-Legacy'
}
-674
View File
@@ -1,674 +0,0 @@
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>.
+38 -48
View File
@@ -1,63 +1,53 @@
# BattleTracker
![BattleTracker](https://wiki.battleplugins.org/images/0/0c/BattleTracker.png "BattleTracker")
==========
A standalone plugin that tracks PVP & PVE statistics along with a suite of combat features to enhance server gameplay.
### Please note, this is a legacy repository for the upcoming [BattleTracker rewrite](https://github.com/BattlePlugins/BattleTracker/). As of right now, this repository is still being maintained and is the main version of BattleTracker, but in the future please redirect all issues and reports [here](https://github.com/BattlePlugins/BattleTracker/).
BattleTracker records SMP activity only. Deaths, kills, ratings, KDR changes, and damage recaps from Duels-OG or Spleef-OG are excluded.
An SQL tracker for Minecraft and [BattleArena](https://github.com/BattlePlugins/BattleArena). Allows overall stats, tallies, and individual records.
# Features
## PVP and PVE Records
One of the core features of BattleTracker is its robust tracking system for PVP and PVE statistics. BattleTracker tracks a variety of statistics including kills, deaths, killstreaks, and more. These statistics are saved in MySQL or SQLite, allowing them to be shared across Minecraft servers, or accessed by web applications.
Reporting Issues:
---------
We have had problems with bug reports that lack sufficient information.
## Leaderboards
Another feature BattleTracker includes is a leaderboard system that allows players to view the top players on the server based on various statistics.
## Death Messages
BattleTracker provides customizable death messages that can be displayed to players when they are killed in PVP or PVE combat. These are easily configurable for each tracker type, and can be customized to fit the theme of your server.
The problem is that people aren't giving us enough information to
replicate bugs. If we cannot identify exactly under what conditions
a bug is present, then it becomes harder to fix.
## Damage Recap
BattleTracker also includes a damage recap feature that displays a summary of the damage dealt and received by a player during combat. This can be summarized in multiple ways, such as which item dealt the most damage, a breakdown of players that dealt damage, or all the types of damage a player received.
## Combat Logging
BattleTracker includes a combat logging feature that places players "in combat" when they attack another player. If they log out, they will be killed and their attacker will receive credit for the kill.
Please, if you report a bug, provide ample information about your situation.
## Damage Indicators
Inside BattleTracker, there is also support for damage indicators, which show to a player how much damage they inflicted on another player or entity.
# PlaceholderAPI
BattleTracker registers the PlaceholderAPI identifier `bt`. Placeholders use the tracker name as the first segment. The default trackers are `pvp` and `pve`.
If you need an answer in a timely manner, please contact us on [Discord](https://discord.gg/tMVPVJf):
## Player Stats
- `%bt_<tracker>_<stat>%`
We really appreciate when you report bugs, and we would like you to report them to us whenever you find them.
However please be mindful of the information that you give us and/or how you contact us. If you really have no
idea where to start, maybe a 1 on 1 conversation on Discord may be the way to go. If you know exactly how to replicate your problem, or
you have a good amount of information about it, creating a new issue might be a better solution:
Examples:
- `%bt_pvp_kills%`
- `%bt_pve_deaths%`
- `%bt_pvp_max_streak%`
## Leaderboards
- `%bt_<tracker>_top_<stat>_<place>%` - value for the player in that leaderboard position
- `%bt_<tracker>_top_<stat>_<place>_name%` - player name for that leaderboard position
- `%bt_<tracker>_top_<stat>_<place>_uuid%` - player UUID for that leaderboard position
* https://github.com/BattlePlugins/BattleTracker/issues
Examples:
- `%bt_pvp_top_kills_1%`
- `%bt_pvp_top_kills_1_name%`
- `%bt_pve_top_deaths_10_uuid%`
Links
------------
Live Chat on Discord:
* [BattlePlugins Discord](https://discord.gg/tMVPVJf): Join our Discord server to get support, talk about dev stuff, or just say hi!
## Built-in Stats
- `kills`
- `deaths`
- `ties`
- `streak`
- `max_streak`
- `ranking`
- `max_ranking`
- `rating`
- `max_rating`
- `kd_ratio`
- `max_kd_ratio`
# Links
- Website: https://www.battleplugins.org
- Download: https://modrinth.com/plugin/battletracker
- Discord: [BattlePlugins Discord](https://discord.com/invite/J3Hjjb8)
Battle Arena Links:
* [Bukkit Page](http://dev.bukkit.org/bukkit-plugins/battlearena/) (BattleArena on Bukkit!)
* [Spigot Page](http://spigotmc.org/resources/battlearena.2164/) (BattleArena on Spigot!)
Battle Tracker Links:
* [Bukkit Page](http://dev.bukkit.org/bukkit-plugins/battletracker/) (BattleTracker on Bukkit!)
* [Spigot Page](http://spigotmc.org/resources/battletracker.2165/) (BattleTracker on Spigot!)
Other Links:
* [BattleDocs](http://docs.battleplugins.org) (BattlePlugins Documentation)
* [Ci Server](http://ci.battleplugins.org/job/BattlePlugins/job/BattleTracker/) (Jenkins Build Server)
* [Battle Plugin Donations] (Donation link, coming soon)
* [JavaDocs](http://rainbowcraft.sytes.net/javadocs/battletracker/apidocs/) (WIP)
Stats
------------
![Plugin Stats](https://bstats.org/signatures/bukkit/BattleTracker.svg "Plugin Stats")
-143
View File
@@ -1,143 +0,0 @@
plugins {
id("maven-publish")
id("java")
id("java-library")
id("xyz.jpenilla.run-paper") version "2.3.0"
id("com.modrinth.minotaur") version "2.+"
id("com.gradleup.shadow") version "8.3.9"
eclipse
}
group = "org.battleplugins"
version = "4.0.2-SNAPSHOT"
val supportedVersions = listOf(
"1.19.4",
"1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6",
"1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4"
)
java.toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
vendor.set(JvmVendorSpec.GRAAL_VM)
}
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.purpurmc.org/snapshots")
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
}
dependencies {
implementation(libs.bstats.bukkit)
implementation(libs.commons.dbcp)
implementation(libs.commons.pool)
implementation(libs.mysql.connector)
compileOnlyApi(libs.purpur.api)
compileOnlyApi(libs.placeholderapi)
}
java {
withJavadocJar()
withSourcesJar()
}
tasks {
runServer {
minecraftVersion("1.19.4")
}
jar {
enabled = false
}
shadowJar {
from("src/main/java/resources") {
include("*")
}
relocate("org.bstats", "org.battleplugins.tracker.util.shaded.bstats")
relocate("org.apache", "org.battleplugins.tracker.util.shaded.apache")
archiveFileName.set("BattleTracker.jar")
archiveClassifier.set("")
}
build {
dependsOn(shadowJar)
}
javadoc {
(options as CoreJavadocOptions).addBooleanOption("Xdoclint:none", true)
}
processResources {
filesMatching("plugin.yml") {
expand("version" to rootProject.version)
}
}
}
publishing {
val isSnapshot = "SNAPSHOT" in version.toString()
repositories {
maven {
name = "battleplugins"
url = uri("https://repo.battleplugins.org/${if (isSnapshot) "snapshots" else "releases"}")
credentials(PasswordCredentials::class)
authentication {
create<BasicAuthentication>("basic")
}
}
publications {
create<MavenPublication>("mavenJava") {
artifactId = "tracker"
from(components["java"])
pom {
packaging = "jar"
url.set("https://github.com/BattlePlugins/BattleTracker")
scm {
connection.set("scm:git:git://github.com/BattlePlugins/BattleTracker.git")
developerConnection.set("scm:git:ssh://github.com/BattlePlugins/BattleTracker.git")
url.set("https://github.com/BattlePlugins/BattleTracker");
}
licenses {
license {
name.set("GNU General Public License v3.0")
url.set("https://www.gnu.org/licenses/gpl-3.0.html")
}
}
developers {
developer {
name.set("BattlePlugins Team")
organization.set("BattlePlugins")
organizationUrl.set("https://github.com/BattlePlugins")
}
}
}
}
}
}
}
modrinth {
val snapshot = "SNAPSHOT" in rootProject.version.toString()
token.set(System.getenv("MODRINTH_TOKEN") ?: "")
projectId.set("battletracker")
versionNumber.set(rootProject.version as String + if (snapshot) "-" + System.getenv("BUILD_NUMBER") else "")
versionType.set(if (snapshot) "beta" else "release")
changelog.set(System.getenv("CHANGELOG") ?: "")
uploadFile.set(tasks.shadowJar)
gameVersions.set(supportedVersions)
}
+67
View File
@@ -0,0 +1,67 @@
version: 1.0.4
# Updates will be retrieved from the latest plugin on the bukkit site. Valid Options : none, release, beta, all
# none: (don't auto update), release: (only get release version, ignore beta and alpha)
# beta: (get release and beta versions, ignore alpha), all: (get all new updates)
autoUpdate: release
# show newer versions. Valid Options: none, console, ops
# none: (don't show new versions), console: (show only to console log on startup)
# ops (announce to ops on join, will only show this message once per server start)
announceUpdate: console
showBukkitPVPMessages: false # turn on and off normal bukkit death messages for pvp
showBukkitPVEMessages: false # turn on and off normal bukkit death messages for pve
sendPVPDeathMessages: true # send Custom Messages on PvP deaths
sendPVEDeathMessages: true # send Custom Messages on PvE deaths
### If server wide PvP or Pve messages are disabled you can turn
### these to true to allow the killer and the dead person
### to still see the messages
sendInvolvedPvPMessages: false
sendInvolvedPvEMessages: false
### Track Player vs Player stats
trackPvP: true
### Track Player vs Environment stats
trackPvE: false
### The default messages when top command is used
topHeaderMsg: '&4Top &6{interfaceName}&4 {stat} TeamSize:{teamSize}'
topBodyMsg: '&e#{rank}&4 {name} - {wins}:{losses}&6[{rating}]'
### ignore the following entities when they kill or are killed
ignoreEntities: []
### ignore the following worlds
ignoreWorlds: []
### Should we even use leaderboard signs and personal signs?
useSigns: true
### If showing PvP messages should we use a Herochat channel
useHeroChat: false
chatChannel: "Global" ## Which channel to use
msgRadius: 0 ## Set to 0 for global
### If number of seconds between kills is less than
### this number, show on a rampage message
rampageTime: 7
### Display a player is on a streak messages every x kills
streakMessagesEvery: 15
SQLOptions:
type: sqlite # sqlite or mysql
db: tracker # db name
url : "localhost" # ignored if not mysql
port : "3306" # ignored if not mysql
username: "root"
password: ""
## These are the default elo options, they can be overridden by calling plugins
elo:
default: 1250
spread: 400
+165
View File
@@ -0,0 +1,165 @@
## Options
# %k : the killer (can be a player, mob, or environment)
# %d : the dead player
# %i : item used to kill the player (if one exists)
# %n : number (used for streaks, rampages)
### The list of pvp item deaths is incomplete, feel free to add more
### color codes
# &0 Black
# &1 Dark Blue
# &2 Dark Green
# &3 Dark Aqua
# &4 Dark Red
# &5 Purple
# &6 Orange
# &7 Grey
# &8 Dark Grey
# &9 Indigo
# &a Bright Green
# &b Aqua
# &c Red
# &d Pink
# &e Yellow
# &f White
version: 1.0.1
pve:
prefix: "&e[PvE]&f "
meleeDeaths:
- "&6%d&f was killed by a &6%k&f"
rangeDeaths:
- "&6%d&f was killed by a &6%k&f"
fire:
- "&6%d&f was fried to a crisp"
- "&6%d&f burned to death"
fire_tick:
- "&6%d&f tried to extinguish the flames, but failed"
lava:
- "&6%d&f was no match for a pool of lava"
fall:
- "&6%d&f found out what the ground feels like"
contact:
- "&6%d&f should learn to not jump on a cactus"
block_explosion:
- "&6%d&f has been shredded by TNT"
primedtnt:
- "&6%d&f has been shredded by TNT"
suffocation:
- "&6%d&f ran out of breath"
starvation:
- "&6%d&f should learn to eat"
unknown:
- "&6%d&f has died of unknown causes"
lightning:
- "&6%d&f was struck down by a bolt of lightning"
suicide:
- "hari kari suited &6%d&f"
- "&6%d&f chose the ignoble way out"
drowning:
- "&6%d&f needs to learn to swim"
- "dog paddling wasn't enough for &6%d&f"
- "&2water&f was the end of poor &6%d&f"
void:
- "The &0void&f has claimed &6%d&f"
creeper:
- "The &2creeper&f blast vaporized &6%d&f"
zombie:
- "&6%d&f has left this world as a &2zombie"
- "&2Zombies&f have devoured &6%d&f"
- "The &2zombie&f horde have claimed &6%d&f"
skeleton:
- "&6%d&f was pierced by a &7skeleton&f arrow"
- "&6%d&f was no match for the &7skeleton&f"
spider:
- "Spiders have drained &6%d&f"
cavespider:
- "Cavespiders have swarmed &6%d&f"
enderdragon:
- "The &8ender dragon&f has annihilated &6%d&f"
blaze:
- "The &4blaze&f has crisped &6%d&f"
silverfish:
- "&6%d&f was overwhelmed by silverfish"
enderman:
- "&8Endermen&f have taken &6%d&f to the nether"
- '&6%d&f looked into the eyes of the&8Endermen'
snowman:
- '&8Endermen&f have taken &6%d&f to the nether'
ozelot:
- 'The cute ocelot showed its power to &6%d&f'
- '&6%d&f discovered that ocelots have fangs'
anvil:
- '&6%d&f was crushed by an anvil'
- '&6%d&f found to use anvils, not stand under them'
wolf:
- "&6%d&f was hunted down by the pack"
- "&6%d&f was devored by wolves"
giant:
- "&6%d&f was smashed by a Giant"
- "A giant has flattened &6%d&f"
slime:
- "&6%d&f was liquified by a &aslime"
- "&6%d&f was enveloped by &aslime"
ghast:
- "&6%d&f was burned by ghasts"
pigzombie:
- "The PigZombie horde has claimed &6%d&f"
lavaslime:
- "&6%d&f has been lavaslimed!"
pvp:
prefix: "&4[PvP]&f "
meleeDeaths:
- "&6%k&f has slain &6%d&f with a &6%i&f!"
- "&6%k&f is victorious over &6%d&f!"
- "&6%k&f has overpowered &6%d&f in combat!"
- "&6%k&f killed &6%d&f with a &6%i&f!"
- "&6%k&f's &6%i&f has ended poor &6%d&f!"
- "&6%k&f evicerated &6%d&f!"
- "&6%k&f had fun slicing and dicing &6%d&f!"
- "&6%k&f sent &6%d&f to pay Charon!"
- "&6%k&f impaled &6%d&f with a &6%i&f!"
- "&6%k&f destroyed &6%d&f!"
- "&6%k&f gutted the unfortunate &6%d&f!"
- "&6%k&f has sent &6%d&f to the heavenly realm!"
- "&6%k&f showed &6%d&f the meaning of pain!"
- "&6%k&f split &6%d&f in twain!"
- "&6%k&f overcame &6%d&f in battle!"
- "&6%k&f defeated &6%d&f in mighty struggle!"
- "&6%k&f ended all dreams for &6%d&f!"
rangeDeaths:
- "&6%k&f has riddled &6%d&f with arrows!"
- "With a well aimed shot &6%k&f punctured &6%d&f's heart!"
bow:
- '&6%k&f feathered &6%d&f with arrows from &e%i &5%l'
- '&6%k&fs arrow pierced through &6%d&f with &e%i &5%l'
air:
- "&6%k&f pummeled &6%d&f with their bare hands!"
- "&6%d&f was no match for the combat skills of &6%k&f"
- "&6%k&f showed &6%d&f that fists are weapons too"
- "&6%k&f fists broke down &6%d&f"
diamond_sword:
- "&6%k&f sliced through &6%d&f with a &6%i&f"
- "&6%k&f's &6%i&f split &6%d&f in two"
iron_sword:
- "&6%k&f slayed &6%d&f with an &6%i&f"
wood_sword:
- "&6%k&f hacked &6%d&f to death with a &6%i&f"
diamond_axe:
- "&6%k&f used &6%d&f as mere lumber"
- "&6%k&f cleaved &6%d&f with a &6%i&f"
special:
rampage:
default: "&6%k&f has slain &6%d&f on a rampage of &c%n&f kills!!!"
"2": "&6%k&f has started a rampage on &6%d&f !!!"
"3": "&6%k&f is on a rampage!!!"
streak:
default: "&6%k&f has slain &6%d&f in a streak of &c%n&f kills!!!"
"10": "&6%k&f is on a killing spree! slaying &6%d&f in a streak of &c%n&f kills!!!"
"20": "&6%k&f is unstoppable killing &6%d&f in a streak of &c%n&f kills!!!"
messages:
prefix: "&5[Tracker]&f "
xBetween: "&cx must be between 1 and %s"
recordNotFound: "&cA record for player &6%s&4 couldn't be found"
playerNotFound: "&cThe player %s was not found"
noRecordsInTable: "&cthere are no records in the &6%s&c table"
-14
View File
@@ -1,14 +0,0 @@
[versions]
bstats-bukkit = "3.0.2"
purpur-api = "1.19.4-R0.1-SNAPSHOT"
commons = "2.12.0"
placeholderapi = "2.11.6"
mysql-connector = "9.6.0"
[libraries]
bstats-bukkit = { group = "org.bstats", name = "bstats-bukkit", version.ref = "bstats-bukkit" }
purpur-api = { group = "org.purpurmc.purpur", name = "purpur-api", version.ref = "purpur-api" }
commons-dbcp = { group = "org.apache.commons", name = "commons-dbcp2", version.ref = "commons" }
commons-pool = { group = "org.apache.commons", name = "commons-pool2", version.ref = "commons" }
mysql-connector = { group = "com.mysql", name = "mysql-connector-j", version.ref = "mysql-connector" }
placeholderapi = { group = "me.clip", name = "placeholderapi", version.ref = "placeholderapi" }
Binary file not shown.
-7
View File
@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
-251
View File
@@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
-94
View File
@@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+21
View File
@@ -0,0 +1,21 @@
name: ${project.name}
version: ${project.version}
api-version: 1.13
main: mc.alk.tracker.Tracker
author: alkarin
softdepend: [BattleCore, Essentials, Herochat, TownyChat, PlaceholderAPI]
description: A tracker for PvP player related events with leaderboard signs. Includes database backend.
commands:
battleTracker:
aliases: [bt]
description: Show help menu
usage: /bt
btpvp:
aliases: [pvp]
description: Show how players have matched up
usage: /pvp player1 [player2]
btpve:
aliases: [pve]
description: Show how players have matched up vs mobs/environment
usage: /pve player1 [player2]
+220
View File
@@ -0,0 +1,220 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>mc.alk</groupId>
<artifactId>BattleTracker</artifactId>
<packaging>jar</packaging>
<version>2.5.11.0</version>
<name>BattleTracker</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.build.timestamp.format>yyyyMMdd</maven.build.timestamp.format>
<doclint>none</doclint>
<source>https://github.com/BattlePlugins/BattleTracker</source>
<download>https://www.spigotmc.org/resources/2165/</download>
<documentation>https://docs.battleplugins.org/docs/bt/</documentation>
<maven.test.skip>true</maven.test.skip>
</properties>
<!-- Locations for jar repositories -->
<repositories>
<repository>
<id>battleplugins-repo</id>
<url>https://repo.battleplugins.org/artifactory/libs-release</url>
</repository>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>placeholderapi-repo</id>
<url>http://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
<repository>
<id>codemc-repo</id>
<url>https://repo.codemc.org/repository/maven-public</url>
</repository>
</repositories>
<!-- Dependencies -->
<dependencies>
<!-- Dependencies that have maven repos -->
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>1.9-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mc.euro</groupId>
<artifactId>Version</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mc.euro</groupId>
<artifactId>BukkitAdapter</artifactId>
<version>1.2.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mc.euro</groupId>
<artifactId>BukkitInterface</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mc.alk</groupId>
<artifactId>BattleCore</artifactId>
<version>1.8.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mc.alk</groupId>
<artifactId>BattleBukkitLib</artifactId>
<version>4.5.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mc.alk</groupId>
<artifactId>BattlePluginUpdater</artifactId>
<version>2.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>1.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.9.2</version>
<scope>provided</scope>
</dependency>
<!-- unknown plugins that I had to add to the repo -->
<dependency>
<groupId>unknown.plugins</groupId>
<artifactId>Herochat</artifactId>
<version>5.6.6</version>
<scope>provided</scope>
</dependency>
<!-- test dependencies -->
</dependencies>
<!-- How we will compile the plugin -->
<build>
<finalName>${project.name}</finalName>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>2.4</version>
</extension>
</extensions>
<defaultGoal>install</defaultGoal>
<!-- Where our source directory is -->
<sourceDirectory>${basedir}/src/java</sourceDirectory>
<!-- Include all the resources we need -->
<resources>
<resource>
<targetPath>.</targetPath>
<filtering>true</filtering>
<directory>${basedir}</directory>
<includes>
<include>plugin.yml</include>
<include>default_files/*</include>
</includes>
</resource>
<!-- Exclude JUnit test directory -->
<resource>
<directory>src/test</directory>
<excludes>
<exclude>**/*</exclude>
</excludes>
</resource>
</resources>
<plugins>
<!-- Specify how we build the plugin.jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<!-- Shade (includes these in the final jar) -->
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>mc.euro.version</pattern>
<shadedPattern>mc.alk.tracker.alib.version</shadedPattern>
</relocation>
<relocation>
<pattern>mc.euro.bukkitadapter</pattern>
<shadedPattern>mc.alk.tracker.alib.bukkitadapter</shadedPattern>
</relocation>
<relocation>
<pattern>mc.euro.bukkitinterface</pattern>
<shadedPattern>mc.alk.tracker.alib.bukkitinterface</shadedPattern>
</relocation>
<relocation>
<pattern>mc.alk.v1r7</pattern>
<shadedPattern>mc.alk.tracker.battlecore</shadedPattern>
</relocation>
<relocation>
<pattern>mc.alk.battlebukkitlib</pattern>
<shadedPattern>mc.alk.tracker.alib.battlebukkitlib</shadedPattern>
</relocation>
<relocation>
<pattern>mc.alk.battlepluginupdater</pattern>
<shadedPattern>mc.alk.tracker.alib.battlepluginupdater</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats.bukkit</pattern>
<!-- Replace this with your package! -->
<shadedPattern>mc.alk.tracker.alib.metrics</shadedPattern>
</relocation>
</relocations>
<artifactSet>
<includes>
<include>mc.euro:Version</include>
<include>mc.euro:BukkitAdapter</include>
<include>mc.euro:BukkitInterface</include>
<include>mc.alk:BattleCore</include>
<include>mc.alk:BattleBukkitLib</include>
<include>mc.alk:BattlePluginUpdater</include>
<include>org.bstats:bstats-bukkit</include>
</includes>
</artifactSet>
<!-- Specify to remove all unneeded classes -->
<minimizeJar>false</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>central</id>
<name>BattlePlugins Maven Server-releases</name>
<url>https://repo.battleplugins.org/artifactory/libs-release-local</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<name>BattlePlugins Maven Server-snapshots</name>
<url>https://repo.battleplugins.org/artifactory/libs-snapshot-local</url>
</snapshotRepository>
</distributionManagement>
</project>
-15
View File
@@ -1,15 +0,0 @@
rootProject.name = "BattleTracker"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
dependencyResolutionManagement {
repositories {
mavenCentral()
// Spigot
maven("https://hub.spigotmc.org/nexus/content/groups/public/")
// Paper, Velocity
maven("https://repo.papermc.io/repository/maven-public")
}
}
+40
View File
@@ -0,0 +1,40 @@
package mc.alk.tracker;
import mc.alk.battlepluginupdater.PluginUpdater.AnnounceUpdateOption;
import mc.alk.battlepluginupdater.PluginUpdater.UpdateOption;
public class Defaults {
public static final int SAVE_EVERY_X_SECONDS = 200;
public static final String PVP_INTERFACE = "PvP";
public static final String PVE_INTERFACE = "PvE";
public static int STREAK_EVERY = 15;
public static int RAMPAGE_TIME = 7000;
public static boolean PVP_MESSAGES = true;
public static boolean PVE_MESSAGES = true;
public static boolean BUKKIT_PVP_MESSAGES = false;
public static boolean BUKKIT_PVE_MESSAGES = false;
public final static boolean DEBUG = false;
public static boolean DEBUG_ADD_RECORDS = false;
public static int RADIUS = -1;
/// Message Defaults
public static String MSG_TOP_HEADER = "&4Top &6{interfaceName}&4 {stat} TeamSize:{teamSize}";
public static String MSG_TOP_BODY ="&e#{rank}&4 {name} - {wins}:{losses}&6[{rating}]";
public static boolean INVOLVED_PVP_MESSAGES = false;
public static boolean INVOLVED_PVE_MESSAGES = false;
public static final String ADMIN_PERM = "tracker.admin";
public static boolean USE_SIGNS = true;
public static UpdateOption AUTO_UPDATE = UpdateOption.RELEASE;
public static AnnounceUpdateOption ANNOUNCE_UPDATE = AnnounceUpdateOption.CONSOLE;
}
+177
View File
@@ -0,0 +1,177 @@
package mc.alk.tracker;
import mc.alk.tracker.controllers.ConfigController;
import mc.alk.tracker.controllers.MessageController;
import mc.alk.tracker.controllers.SignController;
import mc.alk.tracker.controllers.TrackerController;
import mc.alk.tracker.controllers.TrackerImpl;
import mc.alk.tracker.controllers.TrackerImpl.DBConnectionException;
import mc.alk.tracker.executors.BattleTrackerExecutor;
import mc.alk.tracker.executors.TrackerExecutor;
import mc.alk.tracker.listeners.BTEntityListener;
import mc.alk.tracker.listeners.BTPluginListener;
import mc.alk.tracker.listeners.SignListener;
import mc.alk.tracker.objects.StatSign;
import mc.alk.tracker.serializers.SignSerializer;
import mc.alk.tracker.serializers.YamlConfigUpdater;
import mc.alk.tracker.serializers.YamlMessageUpdater;
import mc.alk.v1r7.core.MCPlugin;
import mc.alk.v1r7.core.Version;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Tracker extends MCPlugin {
static Tracker plugin;
TrackerController sc;
final static Map<String, TrackerInterface> interfaces = Collections.synchronizedMap(
new ConcurrentHashMap<String, TrackerInterface>());
static SignController signController = new SignController();
SignSerializer signSerializer;
// The bukkit id for this project. https://api.curseforge.com/servermods/projects?search=battletracker
static final int bukkitId = 43688;
@Override
public void onEnable() {
super.onEnable();
plugin = this;
plugin.setEnabled(true);
createPluginFolder();
loadConfigs();
Bukkit.getPluginManager().registerEvents(new BTEntityListener(), this);
Bukkit.getPluginManager().registerEvents(new BTPluginListener(), this);
getCommand("battleTracker").setExecutor(new BattleTrackerExecutor());
getCommand("btpvp").setExecutor(new TrackerExecutor(getInterface(Defaults.PVP_INTERFACE)));
getCommand("btpve").setExecutor(new TrackerExecutor(getInterface(Defaults.PVE_INTERFACE)));
// PluginUpdater.update(this, bukkitId, this.getFile(), Defaults.AUTO_UPDATE, Defaults.ANNOUNCE_UPDATE);
BTPluginListener.loadPlugins();
new Metrics(this);
}
@Override
public void onDisable() {
plugin.setEnabled(false);
saveConfig();
}
@Override
public void onLoad() {
ConfigurationSerialization.registerClass(StatSign.class);
}
@Override
public void reloadConfig() {
super.reloadConfig();
this.loadConfigs();
}
public void loadConfigs() {
if (!plugin.isEnabled()) {
return;
}
/// Load
ConfigController.setConfig(load("/default_files/config.yml", getDataFolder().getPath() + "/config.yml"));
MessageController.setConfig(load("/default_files/messages.yml", getDataFolder().getPath() + "/messages.yml"));
/// on some servers with non bukkit worlds, this is too quick. delay this till after all plugins
/// are loaded
Bukkit.getScheduler().scheduleSyncDelayedTask(this, new Runnable() {
@Override
public void run() {
signSerializer = new SignSerializer(signController);
signSerializer.setConfig(getDataFolder().getPath() + "/signs.yml");
signSerializer.loadAll();
}
}, 22);
/// Update
YamlConfigUpdater cu = new YamlConfigUpdater();
cu.update(ConfigController.getConfig(), ConfigController.getFile());
YamlMessageUpdater mu = new YamlMessageUpdater();
mu.update(MessageController.getConfig(), MessageController.getFile());
/// Reload
ConfigController.setConfig(ConfigController.getFile());
MessageController.setConfig(MessageController.getFile());
if (Defaults.USE_SIGNS) {
getServer().getPluginManager().registerEvents(new SignListener(signController), this);
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, new Runnable() {
@Override
public void run() {
signController.updateSigns();
}
}, 20, 1000);
}
}
public static Tracker getSelf() {
return plugin;
}
@Override
public void saveConfig() {
super.saveConfig();
synchronized (interfaces) {
for (TrackerInterface ti : interfaces.values()) {
ti.flush();
}
}
/// can happen if tracker never loads properly (like starting and immediately stopping)
if (signSerializer != null) {
signSerializer.saveAll();
}
}
public static TrackerInterface getPVPInterface() {
return getInterface(Defaults.PVP_INTERFACE, new TrackerOptions());
}
public static TrackerInterface getPVEInterface() {
return getInterface(Defaults.PVE_INTERFACE, new TrackerOptions());
}
public static TrackerInterface getInterface(String interfaceName) {
return getInterface(interfaceName, new TrackerOptions());
}
public static TrackerInterface getInterface(String interfaceName, TrackerOptions trackerOptions) {
try {
String iname = interfaceName.toLowerCase();
if (!interfaces.containsKey(iname)) {
interfaces.put(iname, new TrackerImpl(interfaceName, trackerOptions));
}
return interfaces.get(iname);
} catch (DBConnectionException e) {
e.printStackTrace();
}
return null;
}
public static Version getVersionObject() {
String strv = getSelf().getDescription().getVersion();
return new Version(strv);
}
public static boolean hasInterface(String interfaceName) {
String iname = interfaceName.toLowerCase();
return interfaces.containsKey(iname);
}
public static Collection<TrackerInterface> getAllInterfaces() {
return new ArrayList<TrackerInterface>(interfaces.values());
}
}
@@ -0,0 +1,242 @@
package mc.alk.tracker;
import mc.alk.tracker.objects.PlayerStat;
import mc.alk.tracker.objects.Stat;
import mc.alk.tracker.objects.StatType;
import mc.alk.tracker.objects.TeamStat;
import mc.alk.tracker.objects.WLT;
import mc.alk.tracker.objects.WLTRecord;
import mc.alk.tracker.ranking.RatingCalculator;
import mc.alk.v1r7.core.Version;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Collection;
import java.util.List;
import java.util.Set;
public interface TrackerInterface{
public void printTopX(CommandSender sender, StatType statType, int x);
public void printTopX(CommandSender sender, StatType statType, int x,String headerMsg, String bodyMsg);
public void printTopX(CommandSender sender, StatType statType, int x, int teamSize);
public void printTopX(CommandSender sender, StatType statType, int x, int teamSize, String headerMsg, String bodyMsg);
public void addStatRecord(Stat team1, Stat team2, WLT wlt);
public void addPlayerRecord(String p1, String p2, WLT wlt);
public void addPlayerRecord(OfflinePlayer p1, OfflinePlayer p2, WLT wlt);
public void addTeamRecord(String t1, String t2, WLT wlt);
public void addTeamRecord(Set<String> team1, Set<String> team2, WLT wlt);
public void addTeamRecord(Collection<Player> team1, Collection<Player> team2, WLT wlt);
public void changePlayerElo(String p1, String p2, WLT wlt);
public PlayerStat getPlayerRecord(String player) ;
public PlayerStat getPlayerRecord(OfflinePlayer player) ;
public TeamStat getTeamRecord(String teamName);
public TeamStat getTeamRecord(Set<String> players);
public boolean hidePlayer(String player, boolean hide);
/**
* Stop tracking stats
* @param playerName
*/
public void stopTracking(String playerName);
/**
* Resume tracking stats
* @param playerName
*/
public void resumeTracking(String playerName);
/**
* Stop displaying kill messages for this player
* @param playerName
*/
public void stopMessages(String playerName);
/**
* Resume displaying kill messages for this player
* @param playerName
*/
public void resumeMessages(String playerName);
/**
* Stop tracking stats
* @param player
*/
public void stopTracking(OfflinePlayer player);
/**
* Resume tracking stats
* @param player
*/
public void resumeTracking(OfflinePlayer player);
/**
* Stop displaying kill messages for this player
* @param player
*/
public void stopMessages(OfflinePlayer player);
/**
* Resume displaying kill messages for this player
* @param player
*/
public void resumeMessages(OfflinePlayer player);
public void resumeMessages(Collection<Player> players);
public void resumeTracking(Collection<Player> players);
public void stopMessages(Collection<Player> players);
public void stopTracking(Collection<Player> players);
public void addRecordGroup(Collection<Player> team1, Collection<Collection<Player>> teams, WLT wlt);
public Stat getRecord(String player);
public Stat getRecord(OfflinePlayer player);
public Stat loadRecord(OfflinePlayer op);
public Stat loadPlayerRecord(String name);
public Stat loadRecord(Set<Player> players);
public Stat getRecord(Collection<Player> players);
public void saveAll();
public List<Stat> getTopX(StatType statType, int x);
/**
* Returns the top x players in rating
*/
public List<Stat> getTopXRating(int x);
/**
* Returns the top x players in losses
*/
public List<Stat> getTopXLosses(int x);
/**
* Returns the top x players in wins
*/
public List<Stat> getTopXWins(int x);
/**
* Returns the top x players in k/d ratio
*/
public List<Stat> getTopXKDRatio(int x);
public List<Stat> getTopX(StatType statType, int x, Integer teamSize);
public List<Stat> getTopXRating(int x, Integer teamSize);
public List<Stat> getTopXLosses(int x, Integer teamSize);
public List<Stat> getTopXWins(int x, Integer teamSize);
public List<Stat> getTopXKDRatio(int x, Integer teamSize);
/**
* Get the rank of this player
* @param sender
* @return
*/
public Integer getRank(OfflinePlayer sender);
/**
* Get the rank of this player/team
* @param name
* @return
*/
public Integer getRank(String name);
/**
* reset the stats for this interface
*/
public void resetStats();
/**
* Dont Track Individual records or Versus Recrods, Just the overall tally
* @param b
*/
public void onlyTrackOverallStats(boolean b);
/**
* Set the rating of the given player
* @param player
* @param rating
* @return
*/
public boolean setRating(OfflinePlayer player, int rating);
/**
* Get the rating calculator being used(default EloCalculator)
* @return
*/
public RatingCalculator getRatingCalculator();
/**
* Get the ranking calculator being used(default EloCalculator)
* @return
*/
@Deprecated
public RatingCalculator getRankingCalculator();
/**
* Set the ranking of the given player
* @param player
* @param rating
* @return
*/
@Deprecated
public boolean setRanking(OfflinePlayer player, int rating);
/**
* Get the records of player1 vs player2
* @param playerName
* @param playerName2
* @param x : the number of records to return, -1 for all
* @return
*/
public List<WLTRecord> getVersusRecords(String playerName, String playerName2, int x);
/**
* What is the name of this interface
* @return
*/
public String getInterfaceName();
/**
* Immediately save these stats (they usually get saved at an appropriate time)
* @param stats
*/
public void save(Stat... stats);
/**
* Immediately save all records (they usually get saved at an appropriate time)
* and empty the cache
*/
public void flush();
/**
* Get how many records are in this interface
* @return
*/
public int getRecordCount();
/**
* Returns the Version of BattleTracker
* @return
*/
public Version getVersion();
public List<WLTRecord> getWinsSince(Stat stat, Long time);
/**
* Return whethe the database has changed
* This is not guaranteed to be 100% correct if data is being
* changed from outside the TrackerInterface
* @return
*/
boolean isModified();
}
@@ -0,0 +1,34 @@
package mc.alk.tracker;
import mc.alk.tracker.controllers.ConfigController;
import mc.alk.tracker.ranking.EloCalculator;
import mc.alk.tracker.ranking.RatingCalculator;
public class TrackerOptions {
boolean saveIndividualRecords = false;
RatingCalculator ratingCalculator;
public TrackerOptions(){
EloCalculator ec = new EloCalculator();
ec.setDefaultRating((float) ConfigController.getDouble("elo.default",1250));
ec.setEloSpread((float) ConfigController.getDouble("elo.spread",400));
ratingCalculator = ec;
}
public RatingCalculator getRatingCalculator() {
return ratingCalculator;
}
public void setRatingCalculator(RatingCalculator ratingCalculator) {
this.ratingCalculator = ratingCalculator;
}
public boolean savesIndividualRecords() {
return saveIndividualRecords;
}
public void setSaveIndividualRecords(boolean saveIndividualRecords) {
this.saveIndividualRecords = saveIndividualRecords;
}
}
@@ -0,0 +1,60 @@
package mc.alk.tracker.controllers;
import mc.alk.battlepluginupdater.PluginUpdater.AnnounceUpdateOption;
import mc.alk.battlepluginupdater.PluginUpdater.UpdateOption;
import mc.alk.tracker.Defaults;
import mc.alk.tracker.listeners.BTEntityListener;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
/**
*
* @author alkarin
*
*/
public class ConfigController {
static YamlConfiguration config = new YamlConfiguration();
static File f = null;
public static boolean getBoolean(String node) {return config.getBoolean(node, false);}
public static boolean getBoolean(String node,boolean b) {return config.getBoolean(node, b);}
public static String getString(String node) {return config.getString(node,null);}
public static String getString(String node,String def) {return config.getString(node,def);}
public static int getInt(String node,int i) {return config.getInt(node, i);}
public static double getDouble(String node, double d) {return config.getDouble(node, d);}
public static void setConfig(File f){
ConfigController.f = f;
loadAll();
}
public static void loadAll(){
try {config.load(f);} catch (Exception e){e.printStackTrace();}
Defaults.RAMPAGE_TIME = config.getInt("rampageTime", 7);
Defaults.STREAK_EVERY = config.getInt("streakMessagesEvery", 15);
Defaults.PVE_MESSAGES = config.getBoolean("sendPVEDeathMessages",Defaults.PVE_MESSAGES);
Defaults.PVP_MESSAGES = config.getBoolean("sendPVPDeathMessages",Defaults.PVP_MESSAGES);
Defaults.BUKKIT_PVE_MESSAGES = config.getBoolean("showBukkitPVEMessages",Defaults.BUKKIT_PVE_MESSAGES);
Defaults.BUKKIT_PVP_MESSAGES = config.getBoolean("showBukkitPVPMessages",Defaults.BUKKIT_PVP_MESSAGES);
Defaults.INVOLVED_PVE_MESSAGES = config.getBoolean("sendInvolvedPvEMessages", Defaults.INVOLVED_PVE_MESSAGES);
Defaults.INVOLVED_PVP_MESSAGES= config.getBoolean("sendInvolvedPvPMessages",Defaults.INVOLVED_PVP_MESSAGES);
Defaults.RADIUS = config.getInt("msgRadius", 0);
Defaults.MSG_TOP_HEADER = config.getString("topHeaderMsg",Defaults.MSG_TOP_HEADER);
Defaults.MSG_TOP_BODY = config.getString("topBodyMsg",Defaults.MSG_TOP_BODY);
Defaults.USE_SIGNS = config.getBoolean("useSigns",Defaults.USE_SIGNS);
UpdateOption o = UpdateOption.fromString(config.getString("autoUpdate", "none"));
Defaults.AUTO_UPDATE = o != null ? o : UpdateOption.NONE;
AnnounceUpdateOption ao = AnnounceUpdateOption.fromString(config.getString("announceUpdate", "none"));
Defaults.ANNOUNCE_UPDATE = ao != null ? ao : AnnounceUpdateOption.NONE;
BTEntityListener.setIgnoreEntities(config.getStringList("ignoreEntities"));
BTEntityListener.setIgnoreWorlds(config.getStringList("ignoreWorlds"));
}
public static File getFile() {
return f;
}
public static YamlConfiguration getConfig() {
return config;
}
}
@@ -0,0 +1,6 @@
package mc.alk.tracker.controllers;
public enum DBInterface {
INSTANCE;
}
@@ -0,0 +1,234 @@
package mc.alk.tracker.controllers;
import com.dthielke.herochat.Herochat;
import java.io.File;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.Random;
import mc.alk.tracker.objects.SpecialType;
import mc.alk.battlebukkitlib.InventoryUtil;
import mc.alk.v1r7.controllers.MC;
import org.apache.commons.lang.StringUtils;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
/**
*
* @author alkarin
*
*/
public class MessageController {
private static YamlConfiguration config = new YamlConfiguration();
static File f;
static Herochat hc;
static MessageController mc;
static List<String> meleeMsgs = null;
static List<String> rangeMsgs = null;
static final Random r = new Random();
static String TRACKER_PREFIX = "&5[Tracker]&f ";
static String PVP_PREFIX = "&4[PvP]&f ";
static String PVE_PREFIX = "&2[PvE]&f ";
public static Herochat getHeroChat() {
return hc;
}
public static void setHeroChat(Herochat hc) {
MessageController.hc = hc;
}
public static String colorChat(String msg) {
return msg.replace('&', (char) 167);
}
public static boolean setConfig(File f){
MessageController.f = f;
return load();
}
public static YamlConfiguration getConfig(){
return config;
}
public static File getFile(){
return f;
}
public static String getMsg(String node, Object... varArgs) {
return getMsg(TRACKER_PREFIX,"messages."+node,varArgs);
}
public static String getMsgNP(String node, Object... varArgs) {
return getMsg(null, "messages."+node,varArgs);
}
private static String getMsg(String prefix, String node, Object... varArgs) {
if (!config.contains(node)){
System.err.println("Error getting message " + prefix + "." + node);
return null;
}
String msg = config.getString(node);
StringBuilder buf = new StringBuilder();
if (prefix != null)
buf.append(prefix);
Formatter form = new Formatter(buf);
try{
form.format(msg, varArgs);
form.close();
} catch(Exception e){
System.err.println("Error getting message " + prefix + "." + node);
for (Object o: varArgs){ System.err.println("argument=" + o);}
e.printStackTrace();
}
return colorChat(buf.toString());
}
public static boolean sendMessage(Player p, String message){
if (message ==null) return true;
String[] msgs = message.split("\n");
for (String msg: msgs){
if (p == null){
System.out.println(MC.colorChat(msg));
} else {
p.sendMessage(MC.colorChat(msg));
}
}
return true;
}
public static boolean sendMessage(CommandSender p, String message){
if (message ==null) return true;
if (p instanceof Player){
if (((Player) p).isOnline())
p.sendMessage(MC.colorChat(message));
} else {
p.sendMessage(MC.colorChat(message));
}
return true;
}
public static String getPvEMessage(boolean melee, String killer, String target, String weapon){
String node=null;
List<String> messages = null;
if (killer != null){
node = "pve." + killer.toLowerCase();
messages = config.getStringList(node);
}
if (messages == null || messages.isEmpty()){
node = melee ? "pve.meleeDeaths" : "pve.rangeDeaths";
messages = config.getStringList(node);
}
if (messages == null || messages.isEmpty()){
System.err.println("[Tracker] getPvEMessage, message node="+node +" args="+ killer +":" +target+":"+weapon);
return null;
}
String msg = messages.get(r.nextInt(messages.size()));
return formatMessage(PVE_PREFIX, msg,killer,target, weapon, null);
}
public static String getPvPMessage(boolean melee, String killer, String target, ItemStack weapon){
String node=null;
List<String> messages = null;
String wpnName = null;
List<String> wpnLore = null;
if (weapon != null){
node = "pvp."+ weapon.getType().name().toLowerCase();
wpnName = InventoryUtil.getCustomName(weapon);
if (weapon.hasItemMeta() && weapon.getItemMeta().hasLore()) {
wpnLore = weapon.getItemMeta().getLore();
}
messages = config.getStringList(node);
}
if (messages == null || messages.isEmpty()){
node = melee ? "pvp.meleeDeaths" : "pvp.rangeDeaths";
messages = config.getStringList(node);
}
if (messages == null || messages.isEmpty()){
System.err.println("[Tracker] getPvPMessage, message node="+node +" args="+ killer +":" +target+":"+weapon);
return null;
}
String msg = messages.get(r.nextInt(messages.size()));
return formatMessage(PVP_PREFIX, msg,killer,target, wpnName, null, wpnLore);
}
public static String getSpecialMessage(SpecialType type, int nKills, String killer, String target, ItemStack weapon){
String node = null;
switch (type){
case STREAK: node = "special.streak." + nKills; break;
case RAMPAGE: node = "special.rampage." + nKills; break;
}
String message = config.getString(node);
if (message == null || message.isEmpty()){
switch (type){
case STREAK: node = "special.streak.default"; break;
case RAMPAGE: node = "special.rampage.default"; break;
}
message = config.getString(node);
}
return formatMessage(PVP_PREFIX, message,killer,target, null, nKills+"");
}
private static String formatMessage(String prefix, String msg, String killer, String target, String item, String times) {
return formatMessage(prefix, msg, killer, target, item, times, new ArrayList<String>());
}
private static String formatMessage(String prefix, String msg, String killer, String target,
String item, String times, List<String> lore) {
try {
if (killer != null) {
msg = StringUtils.replace(msg, "%k", killer);
} else {
msg = StringUtils.replace(msg, "%k", "Unknown killer");
}
if (target != null) {
msg = StringUtils.replace(msg, "%d", target);
} else {
msg = StringUtils.replace(msg, "%d", "unknown target");
}
if (item != null) {
msg = StringUtils.replace(msg, "%i", item.replace('_', ' '));
} else {
msg = StringUtils.replace(msg, "%i", "unknown item");
}
if (times != null) {
msg = StringUtils.replace(msg, "%n", times);
} else {
msg = StringUtils.replace(msg, "%n", "?");
}
if (lore != null && !lore.isEmpty()) {
String slore = StringUtils.join(lore, ", ");
msg = StringUtils.replace(msg, "%l", slore);
} else {
msg = StringUtils.replace(msg, "%l", "???");
}
} catch (Exception e) {
System.err.println("Error getting message " + msg);
// for (Object o: varArgs){ System.err.println("argument=" + o);}
e.printStackTrace();
}
return colorChat(prefix + msg);
}
public static boolean load() {
try {
config.load(f);
} catch (Exception e) {
e.printStackTrace();
}
TRACKER_PREFIX = config.getString("messages.prefix", PVP_PREFIX);
PVP_PREFIX = config.getString("pvp.prefix", PVP_PREFIX);
PVE_PREFIX = config.getString("pve.prefix", PVE_PREFIX);
return true;
}
public static boolean contains(String string) {
return config.contains(string);
}
}
@@ -0,0 +1,398 @@
package mc.alk.tracker.controllers;
import mc.alk.tracker.Tracker;
import mc.alk.tracker.TrackerInterface;
import mc.alk.tracker.objects.Stat;
import mc.alk.tracker.objects.StatSign;
import mc.alk.tracker.objects.StatType;
import mc.alk.battlebukkitlib.SignUtil;
import mc.alk.v1r7.util.SerializerUtil;
import org.apache.commons.lang.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SignController {
final Map<String,StatSign> personalSigns = new ConcurrentHashMap<String,StatSign>();
final Map<String,StatSign> topSigns = new ConcurrentHashMap<String,StatSign>();
final Map<String, Map<String,StatSign>> allSigns = new ConcurrentHashMap<String,Map<String,StatSign>>();
final Map<String,Integer> prevSignCount = new ConcurrentHashMap<String,Integer>();
boolean updating = false;
public SignController(){
// allSigns.put(key, value)
}
public void addSign(StatSign sign){
switch(sign.getSignType()){
case TOP:
topSigns.put(sign.getLocationString(), sign);
break;
case PERSONAL:
personalSigns.put(sign.getLocationString(), sign);
}
}
public Map<String,StatSign> getPersonalSigns(){
return personalSigns;
}
public Map<String,StatSign> getTopSigns(){
return topSigns;
}
public void addSigns(Collection<StatSign> signs) {
for (StatSign sign: signs){
addSign(sign);}
}
public synchronized void updateSigns(){
if (updating || topSigns.isEmpty())
return;
updating = true;
final Map<String,StatSign> map;
synchronized(topSigns){
map = new HashMap<String,StatSign>(topSigns);
}
Collection<String> badSigns = new HashSet<String>();
Collection<TrackerInterface> interfaces = Tracker.getAllInterfaces();
List<StatSign> tops = new ArrayList<StatSign>();
for (TrackerInterface ti : interfaces){
final String tiName = ti.getInterfaceName().toUpperCase();
for (String loc : map.keySet()){
StatSign ss = map.get(loc);
if (!ss.getDBName().toUpperCase().equals(tiName)){
continue;}
Sign s = getSign(loc);
if (s == null){
badSigns.add(loc);
continue;
}
switch(ss.getSignType()){
case TOP:
tops.add(ss);
break;
default:
break;
}
}
doTopSigns(ti,tops);
tops = new ArrayList<StatSign>();
}
synchronized(topSigns){
for (String s: badSigns){
topSigns.remove(s);
}
}
updating = false;
}
/**
* For all of the signs of this type
* @param ti TrackerInterface
* @param statsigns List<StatSign>
*/
private void doTopSigns(TrackerInterface ti, List<StatSign> statsigns){
if (statsigns == null || statsigns.isEmpty())
return;
/// Sort based on stattype
Collections.sort(statsigns, new Comparator<StatSign>(){
@Override
public int compare(StatSign arg0, StatSign arg1) {
if (arg0.getStatType() == null && arg1.getStatType() == null) return 0;
else if (arg1.getStatType() == null ) return -1;
else if (arg0.getStatType() == null ) return 1;
return arg0.getStatType().compareTo(arg1.getStatType());
}
});
/// Get the max length we need to query in the database for each stattype
/// Once we have found the max, do an async update for the statsigns
StatType st = statsigns.get(0).getStatType();
List<Stat> stats = new ArrayList<Stat>();
List<StatSign> update = new ArrayList<StatSign>();
int max = 0;
int offset = 0;
for (StatSign ss: statsigns){
if (ss.getStatType() != st){
/// Update signs
if (st != null)
updateSigns(ti,update,max, st,offset++);
/// Reset variables
st = ss.getStatType();
stats.clear();
update = new ArrayList<StatSign>();
max =0;
}
update.add(ss);
/// If size != prevsize, they have added or removed signs, coutn as a change
int size = this.getUpDownCount(ss);
Integer prevSize = prevSignCount.get(ss.getLocationString());
if (prevSize == null || prevSize != size){
prevSignCount.put(ss.getLocationString(), size);
}
if (max < size){
max = size;}
}
if (!update.isEmpty() && st != null){
updateSigns(ti,update,max,st,offset++);}
}
class SignResult{
final List<Sign> signs;
final int statSignIndex;
public SignResult(List<Sign> signs, int statSignIndex){
this.signs = signs;
this.statSignIndex = statSignIndex;
}
}
private SignResult getUpDown(StatSign ss) {
Sign s = getSign(ss.getLocation());
if (s == null)
return null;
Sign sign = null;
World w = s.getLocation().getWorld();
List<Sign> signList = new ArrayList<Sign>();
boolean foundUpStatSign = false;
/// Search up
int x = s.getLocation().getBlockX();
int y = s.getLocation().getBlockY();
int z = s.getLocation().getBlockZ();
LinkedList<Sign> upsignList = new LinkedList<Sign>();
while ((sign = getSign(w,x,++y,z)) != null){
/// another command sign, don't continue
if (breakLine(sign.getLine(0))){
foundUpStatSign = true;
break;
}
upsignList.addFirst(sign);
}
/// If there isnt a conflicting sign above, then add all those signs as well
if (!foundUpStatSign){
signList.addAll(upsignList);}
/// Add self
int originalSignIndex = signList.size();
signList.add(s);
sign = null;
/// Search down
x = s.getLocation().getBlockX();
y = s.getLocation().getBlockY();
z = s.getLocation().getBlockZ();
while ((sign = getSign(w,x,--y,z)) != null){
String line = sign.getLine(0);
/// another command sign, don't continue
if (breakLine(line))
break;
signList.add(sign);
}
return new SignResult(signList,originalSignIndex);
}
private int getUpDownCount(StatSign ss) {
Sign s = getSign(ss.getLocation());
if (s == null)
return 0;
int count = 1;
World w = s.getLocation().getWorld();
/// Search up
int x = s.getLocation().getBlockX();
int y = s.getLocation().getBlockY();
int z = s.getLocation().getBlockZ();
while (getSign(w,x,++y,z) != null){
count++;}
/// Search down
x = s.getLocation().getBlockX();
y = s.getLocation().getBlockY();
z = s.getLocation().getBlockZ();
while (getSign(w,x,--y,z) != null){
count++;}
return count;
}
private void updateSigns(final TrackerInterface ti,
final List<StatSign> update, final int max, final StatType type, final int offset) {
Bukkit.getScheduler().scheduleAsyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run() {
List<Stat> toplist= ti.getTopX(type, max * 4);
if (toplist != null && !toplist.isEmpty() && Tracker.getSelf().isEnabled()){
Bukkit.getScheduler().scheduleSyncDelayedTask(Tracker.getSelf(),
new UpdateSigns(ti.getInterfaceName(), update,toplist));
}
}
},2L*offset);
}
class UpdateSigns implements Runnable{
final String dbName;
final List<StatSign> statSignList;
final List<Stat> statList;
public UpdateSigns(String dbName, List<StatSign> update, List<Stat> toplist) {
this.dbName = StringUtils.capitalize(dbName);
this.statSignList = update;
this.statList = toplist;
}
@Override
public void run() {
for (StatSign ss: statSignList){
Sign s = getSign(ss.getLocation());
if (s == null)
continue;
SignResult sr = getUpDown(ss);
if (sr == null || sr.signs.isEmpty())
continue;
List<Sign> signList = sr.signs;
boolean quit = false;
int curTop = 0;
for (int i =0;i< signList.size() && !quit;i++){
int startIndex = 0;
s = signList.get(i);
if (i == sr.statSignIndex){
s.setLine(0, MessageController.colorChat("[&e"+dbName+"&0]"));
s.setLine(1, MessageController.colorChat("["+ss.getStatType().color()+ss.getStatType()+"&0]"));
s.setLine(2, MessageController.colorChat("&cNo Records"));
startIndex = 2;
}
for (int j=startIndex;j< 4;j++){
if (curTop >= statList.size()){
quit = true;
break;
}
int val = (int) statList.get(curTop).getStat(ss.getStatType());
String statLine = formatStatLine(statList.get(curTop).getName(), val, curTop);
if (!s.getLine(j).equals(statLine))
s.setLine(j, statLine+" ");
curTop++;
}
s.update(true);
}
}
}
private String formatStatLine(String name, int val, int curTop) {
StringBuilder sb = new StringBuilder(15);
sb.append(curTop + 1).append(".");
int length = sb.length() + intStringLength(val) + 1;
if (name.length() + length > 15){
sb.append(name.substring(0, 15-length )).
append("~").append(String.valueOf(val));
} else {
sb.append(name).append(StringUtils.repeat(" ", 16 - length - name.length())).
append(String.valueOf(val));
}
return sb.toString();
}
}
public static int intStringLength(int i) {
return i==0 ? (1) : (i<0) ? (int)Math.log10(Math.abs(i))+2 : (int)Math.log10(i)+1;
}
private boolean breakLine(String line) {
return line != null && (!line.isEmpty() && line.startsWith("["));
}
private Sign getSign(World w, int x, int y, int z) {
Block b = w.getBlockAt(x, y, z);
Material t = b.getType();
return t.name().contains("SIGN") ? (Sign)b.getState(): null;
}
private Sign getSign(Location l) {
if (l == null)
return null;
Material t = l.getBlock().getType();
return t.name().contains("SIGN") ? (Sign)l.getBlock().getState(): null;
}
private Sign getSign(String loc) {
Location l = SerializerUtil.getLocation(loc);
if (l == null)
return null;
Material t = l.getBlock().getType();
return t.name().contains("SIGN") ? (Sign)l.getBlock().getState(): null;
}
public StatSign getStatSign(Location location) {
String key = StatSign.getLocationString(location);
if (personalSigns.containsKey(key))
return personalSigns.get(key);
else
return topSigns.get(key);
}
public void clickedSign(Player player, Sign s, StatSign ss) {
switch(ss.getSignType()){
case TOP:
updateTopSign(player, s, ss);
break;
case PERSONAL:
updatePersonalSign(player, s, ss);
break;
}
}
private void updatePersonalSign(Player player, Sign s, StatSign ss) {
updateSign(player,s,ss);
}
private void updateTopSign(Player player, Sign s, StatSign ss) {
updateSign(player,s,ss);
}
private void updateSign(Player player, Sign s, StatSign ss){
String[] lines = s.getLines();
TrackerInterface ti = Tracker.getInterface(ss.getDBName());
Stat stat = ti.getRecord(player);
if (stat == null)
return;
lines[0] = "&0Your Stats";
lines[1] = "&0[&9"+stat.getRating() +"&0]";
int len = (stat.getWins() +"/" + stat.getLosses()).length();
lines[2] = (len <= 10) ? "&2"+stat.getWins() +"&0/&4" + stat.getLosses() :
stat.getWins() +"/" + stat.getLosses();
if (lines[2].length() <= 12)
lines[2] = "&0W/L " + lines[2];
lines[3] = "&0Streak: &9" + stat.getStreak() +"";
for (int i=0;i<lines.length;i++){
lines[i] = MessageController.colorChat(lines[i]);
}
SignUtil.sendLines(player, s, lines);
}
public void clearSigns() {
topSigns.clear();
personalSigns.clear();
}
public void removeSignAt(Location location) {
String l = StatSign.getLocationString(location);
topSigns.remove(l);
personalSigns.remove(l);
}
}
@@ -0,0 +1,85 @@
package mc.alk.tracker.controllers;
import mc.alk.tracker.Tracker;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public class TrackerController {
static Set<String> dontTrack = new HashSet<String>();
static Set<String> dontAnnounce = new HashSet<String>();
public static boolean dontTrack(Player target) {
return dontTrack.contains(target.getName()) ;
}
public static boolean dontTrack(String target) {
return dontTrack.contains(target);
}
public static void stopTracking(final String playername,boolean now){
if (now) {
dontTrack.add(playername);
} else {
Bukkit.getScheduler().scheduleSyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run() {
dontTrack.add(playername);
}
});
}
}
public static void resumeTracking(final String playername,boolean now){
if (now) {
dontTrack.remove(playername);
} else {
Bukkit.getScheduler().scheduleSyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run() {
dontTrack.remove(playername);
}
});
}
}
public static void stopAnnouncing(final String playername,boolean now){
if (now) {
dontAnnounce.add(playername);
} else {
Bukkit.getScheduler().scheduleSyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run() {
dontAnnounce.add(playername);
}
});
}
}
public static void resumeAnnouncing(final String playername,boolean now){
if (now) {
dontAnnounce.remove(playername);
} else {
Bukkit.getScheduler().scheduleSyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run() {
dontAnnounce.remove(playername);
}
});
}
}
public static void stopTracking(String playername){stopTracking(playername,false);}
public static void resumeTracking(String playername){resumeTracking(playername,false);}
public static void stopAnnouncing(String playername){stopAnnouncing(playername,false);}
public static void resumeAnnouncing(String playername){resumeAnnouncing(playername,false);}
public static void stopAnnouncing(Collection<Player> players) {for (Player p:players)dontAnnounce.add(p.getName());}
public static void resumeAnnouncing(Collection<Player> players) {for (Player p:players)dontAnnounce.remove(p.getName());}
public static void stopTracking(Collection<Player> players) {for (Player p:players){dontTrack.add(p.getName());}}
public static void resumeTracking(Collection<Player> players) {for (Player p:players)dontTrack.remove(p.getName());}
}
@@ -0,0 +1,538 @@
package mc.alk.tracker.controllers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import mc.alk.tracker.Defaults;
import mc.alk.tracker.Tracker;
import mc.alk.tracker.TrackerInterface;
import mc.alk.tracker.TrackerOptions;
import mc.alk.tracker.objects.PlayerStat;
import mc.alk.tracker.objects.Stat;
import mc.alk.tracker.objects.StatChange;
import mc.alk.tracker.objects.StatType;
import mc.alk.tracker.objects.TeamStat;
import mc.alk.tracker.objects.WLT;
import mc.alk.tracker.objects.WLTRecord;
import mc.alk.tracker.ranking.EloCalculator;
import mc.alk.tracker.ranking.RatingCalculator;
import mc.alk.tracker.serializers.SQLInstance;
import mc.alk.tracker.util.Cache;
import mc.alk.tracker.util.Cache.CacheSerializer;
import mc.alk.v1r7.core.Version;
import mc.alk.v1r7.serializers.SQLSerializerConfig;
import org.apache.commons.lang.mutable.MutableBoolean;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class TrackerImpl implements TrackerInterface, CacheSerializer<String,Stat>{
Cache<String, Stat> cache = new Cache<String, Stat>(this);
boolean trackIndividual = false;
RatingCalculator rc;
SQLInstance sql = null;
String tableName;
public static class DBConnectionException extends Exception{
private static final long serialVersionUID = 1L;
public DBConnectionException(String e){super(e);}
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder("[TI=");
sb.append( sql != null ? sql.getTable(): "null" );
sb.append("]");
return sb.toString();
}
public TrackerImpl(String tableName, TrackerOptions options) throws DBConnectionException{
initDB(tableName);
rc = options.getRatingCalculator();
this.trackIndividual = options.savesIndividualRecords();
}
public TrackerImpl(String type, String db, String urlOrPath, String table, String port, String user, String password) {
sql = new SQLInstance();
this.tableName = table;
sql.setTable(table);
SQLSerializerConfig.configureSQL(sql,type,urlOrPath,db,port,user,password);
cache.setSaveEvery(Defaults.SAVE_EVERY_X_SECONDS *1000);
EloCalculator ec = new EloCalculator();
ec.setDefaultRating(1250);
ec.setEloSpread(400);
rc = ec;
}
private void initDB(String tableName) throws DBConnectionException {
sql = new SQLInstance();
sql.setTable(tableName);
this.tableName = tableName;
SQLSerializerConfig.configureSQL(Tracker.getSelf(), sql,
ConfigController.config.getConfigurationSection("SQLOptions"));
cache.setSaveEvery(Defaults.SAVE_EVERY_X_SECONDS *1000);
}
public Stat load(String id, MutableBoolean dirty, Object... varArgs) {
Stat stat = sql.getRecord(id);
if (Cache.DEBUG) System.out.println(" sql returning " + stat);
if (stat != null){
stat.setCache(cache);
dirty.setValue(false);
} else if (varArgs.length != 0){
dirty.setValue(true);
stat = (Stat) varArgs[0];
if (Cache.DEBUG) System.out.println(" returning premade " + stat);
stat.setCache(cache);
stat.setRating(rc.getDefaultRating());
}
if (stat != null)
stat.setParent(this);
return stat;
}
public void save(List<Stat> stats) {
sql.saveAll(stats.toArray(new Stat[stats.size()]));
}
public void save(Stat... stats) {
sql.saveAll(stats);
}
private Stat getRecord(Stat pStat){
Stat stat = cache.get(pStat.getStrID(), pStat);
if (stat == null){
stat = pStat;
stat.setCache(cache);
stat.setRating(rc.getDefaultRating());
cache.put(pStat);
}
stat.setParent(this);
return stat;
}
public void addStatRecord(Stat team1, Stat team2, WLT wlt){
addStatRecord(team1,team2,wlt,true);
}
private void addStatRecord(final Stat team1, final Stat team2,
final WLT wlt, final boolean changeWinLossRecords){
if (cache.contains(team1) && cache.contains(team2)){
_addStatRecord(new StatChange(team1,team2,wlt,changeWinLossRecords));
} else {
Bukkit.getScheduler().scheduleAsyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run() {
_addStatRecord(new StatChange(team1,team2,wlt,changeWinLossRecords));
}
});
}
}
class RecordHandler implements Runnable {
final StatChange sc;
RecordHandler(StatChange sc) {this.sc = sc; }
public void run() {
_addStatRecord(sc);
}
}
private void _addStatRecord(StatChange sc){
Stat team1 = sc.getTeam1();
Stat team2 = sc.getTeam2();
WLT wlt = sc.getWlt();
boolean changeWinLossRecords = sc.isChangeWinLossRecords();
/// Get our records
Stat ts1 = getRecord(team1);
Stat ts2 = getRecord(team2);
if (Defaults.DEBUG_ADD_RECORDS) System.out.println("BT Debug: addStatRecord:sql="+sql + " ts1 = " + ts1 +" " + ts2);
/// Change win loss record
if (changeWinLossRecords){
ts1.setSaveIndividual(trackIndividual);
ts2.setSaveIndividual(trackIndividual);
switch(wlt){
case WIN:
ts1.win(ts2); ts2.loss(ts1);
break;
case LOSS:
ts1.loss(ts2); ts2.win(ts1);
break;
case TIE:
ts1.tie(ts2); ts2.tie(ts1);
break;
}
}
/// Change the elo
switch(wlt){
case WIN:
rc.changeRatings(ts1,ts2,false);
break;
case LOSS:
rc.changeRatings(ts2,ts1,false);
break;
case TIE:
rc.changeRatings(ts1,ts2,true);
break;
}
}
public void addPlayerRecord(String p1, String p2, WLT wlt) {
Stat ts1 = new PlayerStat(p1);
Stat ts2 = new PlayerStat(p2);
addStatRecord(ts1, ts2, wlt);
}
public void changePlayerElo(String p1, String p2, WLT wlt) {
Stat ts1 = new PlayerStat(p1);
Stat ts2 = new PlayerStat(p2);
addStatRecord(ts1, ts2, wlt,false);
}
public void addPlayerRecord(OfflinePlayer p1, OfflinePlayer p2, WLT wlt) {
addPlayerRecord(p1.getName(), p2.getName(),wlt);
}
public void addTeamRecord(String t1, String t2, WLT wlt) {
TeamStat ts1 = new TeamStat(t1,false);
TeamStat ts2 = new TeamStat(t2,false);
addStatRecord(ts1, ts2,wlt);
}
public void addTeamRecord(Set<String> team1, Set<String> team2, WLT wlt) {
TeamStat ts1 = new TeamStat(team1);
TeamStat ts2 = new TeamStat(team2);
addStatRecord(ts1, ts2,wlt);
}
public void addTeamRecord(Collection<Player> team1, Collection<Player> team2, WLT wlt) {
HashSet<String> names = new HashSet<String>();
for (OfflinePlayer p : team1){
names.add(p.getName());}
TeamStat ts1 = new TeamStat(names);
names = new HashSet<String>();
for (OfflinePlayer p : team2){
names.add(p.getName());}
TeamStat ts2 = new TeamStat(names);
addStatRecord(ts1, ts2,wlt);
}
public TeamStat getTeamRecord(String teamName) {
TeamStat ts = new TeamStat(teamName, false);
return (TeamStat) cache.get(ts.getKey());
}
public TeamStat getTeamRecord(Set<String> players) {
TeamStat ts = new TeamStat(players);
return (TeamStat) cache.get(ts.getKey());
}
public PlayerStat getPlayerRecord(String player) {
Stat ts = cache.get(player);
if (ts instanceof PlayerStat)
return (PlayerStat) ts;
return null;
}
public PlayerStat getPlayerRecord(OfflinePlayer player) {
return (PlayerStat) cache.get(player.getName());
}
public void stopTracking(String player) {TrackerController.stopTracking(player);}
public void resumeTracking(String player) {TrackerController.resumeTracking(player);}
public void stopMessages(String player) {TrackerController.stopAnnouncing(player);}
public void resumeMessages(String player) {TrackerController.resumeAnnouncing(player);}
public void stopTracking(OfflinePlayer player) {TrackerController.stopTracking(player.getName());}
public void resumeTracking(OfflinePlayer player) {TrackerController.resumeTracking(player.getName());}
public void stopMessages(OfflinePlayer player) {TrackerController.stopAnnouncing(player.getName());}
public void resumeMessages(OfflinePlayer player) {TrackerController.resumeAnnouncing(player.getName());}
public void resumeMessages(Collection<Player> players) {TrackerController.resumeAnnouncing(players);}
public void resumeTracking(Collection<Player> players) {TrackerController.resumeTracking(players);}
public void stopMessages(Collection<Player> players) {TrackerController.stopAnnouncing(players);}
public void stopTracking(Collection<Player> players) {TrackerController.stopTracking(players);}
public void addRecordGroup(Collection<Player> team1, Collection<Collection<Player>> teams, WLT wlt) {
TeamStat ts = new TeamStat(toStringCollection(team1));
Stat ts1 = cache.get(ts,ts);
ts1.incWins();
Collection<Stat> lstats = new ArrayList<Stat>();
for (Collection<Player> t : teams){
TeamStat loser = new TeamStat(toStringCollection(t));
Stat lstat = cache.get(loser,loser);
if (lstat == null){
cache.put(loser);
lstat = loser;
}
lstat.incLosses();
lstats.add(lstat);
}
rc.changeRatings(ts1, lstats, false);
}
private Set<String> toStringCollection(Collection<Player> players) {
Set<String> col = new HashSet<String>();
for (Player p: players){
col.add(p.getName());
}
return col;
}
public Stat getRecord(String player) {
return cache.get(player);
}
public Stat getRecord(OfflinePlayer player) {
return cache.get(player.getName());
}
public Stat loadRecord(OfflinePlayer op) {
PlayerStat stat = new PlayerStat(op);
return loadStat(stat);
}
public Stat loadPlayerRecord(String name) {
PlayerStat stat = new PlayerStat(name);
return loadStat(stat);
}
public Stat loadRecord(Set<Player> players) {
HashSet<String> names = new HashSet<String>();
for (OfflinePlayer p : players){
names.add(p.getName());}
Stat stat = new TeamStat(names);
return loadStat(stat);
}
private Stat loadStat(Stat stat){
Stat s = cache.get(stat, stat);
if (s==null){
cache.put(stat);
return stat;
}
return s;
}
public Stat getRecord(Collection<Player> players) {
HashSet<String> names = new HashSet<String>();
for (OfflinePlayer p : players){
names.add(p.getName());}
return cache.get(new TeamStat(names));
}
public void saveAll() {cache.save();}
public List<Stat> getTopXRanking(int x) { return getTopX(StatType.RANKING,x,null);}
public List<Stat> getTopXMaxRanking(int x) {return getTopX(StatType.MAXRANKING,x,null);}
public List<Stat> getTopXRating(int x) { return getTopX(StatType.RATING,x,null);}
public List<Stat> getTopXMaxRating(int x) {return getTopX(StatType.MAXRATING,x,null);}
public List<Stat> getTopXLosses(int x) { return getTopX(StatType.LOSSES,x,null);}
public List<Stat> getTopXWins(int x) {return getTopX(StatType.WINS,x,null);}
public List<Stat> getTopXKDRatio(int x) { return getTopX(StatType.KDRATIO,x,null);}
public List<Stat> getTopX(StatType statType, int x) {
return getTopX(statType,x,1);
}
public List<Stat> getTopXRanking(int x, Integer teamsize) {return getTopX(StatType.RANKING,x,teamsize);}
public List<Stat> getTopXMaxRanking(int x, Integer teamsize) {return getTopX(StatType.MAXRANKING,x,teamsize);}
public List<Stat> getTopXRating(int x, Integer teamsize) {return getTopX(StatType.RATING,x,teamsize);}
public List<Stat> getTopXMaxRating(int x, Integer teamsize) {return getTopX(StatType.MAXRATING,x,teamsize);}
public List<Stat> getTopXStreak(int x, Integer teamsize) {return getTopX(StatType.STREAK,x,teamsize);}
public List<Stat> getTopXMaxStreak(int x, Integer teamsize) {return getTopX(StatType.MAXSTREAK,x,teamsize);}
public List<Stat> getTopXWins(int x, Integer teamsize) {return getTopX(StatType.WINS,x,teamsize);}
public List<Stat> getTopXLosses(int x, Integer teamsize) {return getTopX(StatType.LOSSES,x,teamsize);}
public List<Stat> getTopXKDRatio(int x, Integer teamsize) {return getTopX(StatType.KDRATIO,x,teamsize);}
public List<Stat> getTopX(StatType statType, int x, Integer teamsize) {
cache.save();
return sql.getTopX(statType, x, teamsize);
}
public void resetStats() {
cache.clear();
sql.deleteTables();
}
/**
* write out all dirty records. and empty the cache
*/
public void flush() {
cache.flush();
}
public void onlyTrackOverallStats(boolean b) {
trackIndividual = !b;
}
public RatingCalculator getRatingCalculator() {
return rc;
}
public RatingCalculator getRankingCalculator() {
return rc;
}
public SQLInstance getSQL() {
return sql;
}
public boolean setRating(OfflinePlayer player, int rating){
Stat stat = cache.get(new PlayerStat(player));
if (stat == null)
return false;
stat.setRating(rating);
return true;
}
public boolean setRanking(OfflinePlayer player, int ranking) {
return setRating(player,ranking);
}
public String getInterfaceName() {
return tableName;
}
public List<WLTRecord> getVersusRecords(String name, String name2) {
return getVersusRecords(name,name2,10);
}
public List<WLTRecord> getVersusRecords(String name, String name2, int x) {
cache.save();
return sql.getVersusRecords(name,name2,x);
}
@Override
public List<WLTRecord> getWinsSince(Stat stat, Long time) {
cache.save();
return sql.getWinsSince(stat.getName(),time);
}
public void printTopX(CommandSender sender, StatType statType, int x){
printTopX(sender,statType,x,null, Defaults.MSG_TOP_HEADER, Defaults.MSG_TOP_BODY);
}
public void printTopX(CommandSender sender, StatType statType, int x, String headerMsg, String bodyMsg){
printTopX(sender,statType,x,null, headerMsg, bodyMsg);
}
public void printTopX(CommandSender sender, StatType statType, int x, int teamSize){
printTopX(sender,statType,x,teamSize, Defaults.MSG_TOP_HEADER, Defaults.MSG_TOP_BODY);
}
public void printTopX(CommandSender sender, StatType statType, int x, int teamSize, String headerMsg, String bodyMsg){
printTopX(sender,statType,x,new Integer(teamSize), headerMsg, bodyMsg);
}
private void printTopX(CommandSender sender, StatType statType, int x, Integer teamSize, String headerMsg, String bodyMsg){
if (x <= 0 ){
x = Integer.MAX_VALUE;}
cache.save();
List<Stat> teamstats = getTopXRanking(x, teamSize);
if (teamstats == null){
MessageController.sendMessage(sender,ChatColor.YELLOW + "The top " + statType.getName() + " can not be found");
return;
}
/// Header Message
String msg = headerMsg;
msg = msg.replaceAll("\\{interfaceName\\}", getInterfaceName());
msg = msg.replaceAll("\\{teamSize\\}", teamSize +"");
msg = msg.replaceAll("\\{stat\\}", statType.getName());
msg = msg.replaceAll("\\{x\\}", x +"");
MessageController.sendMessage(sender,msg);
/// Send the Body Messages
Map<StatType,Pattern> patterns = new HashMap<StatType,Pattern>(StatType.values().length);
for (StatType st: StatType.values()){
patterns.put(st,Pattern.compile("\\{"+st.name().toLowerCase()+"\\}"));
}
final int max = Math.min(x,teamstats.size());
for (int i=0;i<max;i++){
msg = bodyMsg;
Stat stat = teamstats.get(i);
for (StatType st : patterns.keySet()){
Matcher m = patterns.get(st).matcher(msg);
if (!m.find())
continue;
switch (st){
case WINS: case KILLS: msg = m.replaceAll(stat.getWins()+""); break;
case LOSSES: case DEATHS: msg = m.replaceAll(stat.getLosses()+""); break;
case TIES: msg = m.replaceAll(stat.getTies()+""); break;
case RANKING:
case RATING: msg = m.replaceAll(stat.getRating()+""); break;
case MAXRANKING:
case MAXRATING: msg = m.replaceAll(stat.getMaxRating()+""); break;
case STREAK: msg = m.replaceAll(stat.getStreak()+""); break;
case MAXSTREAK: msg = m.replaceAll(stat.getMaxStreak()+""); break;
case WLRATIO: msg = m.replaceAll(stat.getKDRatio()+""); break;
default:
break;
}
}
msg = msg.replaceAll("\\{rank\\}", i+1 +"");
msg = msg.replaceAll("\\{name\\}", stat.getName());
MessageController.sendMessage(sender,msg);
}
}
@Override
public int getRecordCount() {
return sql.getRecordCount();
}
@Override
public Version getVersion() {
return Tracker.getVersionObject();
}
@Override
public Integer getRank(OfflinePlayer sender) {
cache.save();
Stat s = getPlayerRecord(sender);
if (s == null)
return null;
return sql.getRanking(s.getRating(),s.getCount());
}
@Override
public Integer getRank(String team) {
cache.save();
Stat s = getRecord(team);
if (s == null)
return null;
return sql.getRanking(s.getRating(),s.getCount());
}
@Override
public boolean hidePlayer(String player, boolean hide) {
Stat s = getPlayerRecord(player);
if (s==null)
return false;
s.hide(hide);
cache.flush();
return true;
}
@Override
public boolean isModified(){
return cache.isModified();
}
}
@@ -0,0 +1,22 @@
package mc.alk.tracker.events;
import mc.alk.tracker.TrackerInterface;
import mc.alk.tracker.objects.Stat;
public class MaxRatingChangeEvent extends TrackerEvent{
final Stat stat;
final double oldMaxRating;
public MaxRatingChangeEvent(TrackerInterface trackerInterface, Stat stat, double oldMaxRating){
super(trackerInterface);
this.stat = stat;
this.oldMaxRating = oldMaxRating;
}
public Stat getStat(){
return stat;
}
public double getOldMaxRating(){
return oldMaxRating;
}
}
@@ -0,0 +1,56 @@
package mc.alk.tracker.events;
import mc.alk.tracker.Tracker;
import mc.alk.tracker.TrackerInterface;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
public class TrackerEvent extends Event{
private static final HandlerList handlers = new HandlerList();
final TrackerInterface trackerInterface;
public TrackerEvent(TrackerInterface trackerInterface){
this.trackerInterface = trackerInterface;
}
/**
* Alias for getInterfaceName
* @return
*/
public String getDBName(){
return trackerInterface.getInterfaceName();
}
/**
* Returns the name of the interface this event was called from
* @return
*/
public String getInterfaceName(){
return trackerInterface.getInterfaceName();
}
public void callEvent(){
Bukkit.getServer().getPluginManager().callEvent(this);
}
public void callSyncEvent(){
final Event event = this;
Bukkit.getScheduler().scheduleSyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run() {
Bukkit.getServer().getPluginManager().callEvent(event);
}
});
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}
@@ -0,0 +1,29 @@
package mc.alk.tracker.events;
import mc.alk.tracker.TrackerInterface;
import mc.alk.tracker.objects.Stat;
public class WinStatChangeEvent extends TrackerEvent{
final Stat winner;
final Stat loser;
public WinStatChangeEvent(TrackerInterface trackerInterface, Stat winner, Stat loser){
super(trackerInterface);
this.winner = winner;
this.loser = loser;
}
/**
* The winner
* @return winner, will never be null
*/
public Stat getWinner(){
return winner;
}
/**
* The loser, can be null
* @return Whoever lost
*/
public Stat getLoser(){
return loser;
}
}
@@ -0,0 +1,121 @@
package mc.alk.tracker.executors;
import mc.alk.tracker.Defaults;
import mc.alk.tracker.Tracker;
import mc.alk.tracker.TrackerInterface;
import mc.alk.tracker.objects.StatType;
import mc.alk.v1r7.executors.CustomCommandExecutor;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
public class BattleTrackerExecutor extends CustomCommandExecutor {
public BattleTrackerExecutor() {
super();
}
@MCCommand( cmds = {"enableDebugging"}, op=true, usage="enableDebugging <code section> <true | false>")
public void enableDebugging(CommandSender sender, String section, Boolean on){
if (section.equalsIgnoreCase("records")){
Defaults.DEBUG_ADD_RECORDS = on;
} else {
sendMessage(sender, "&cDebugging couldnt find code section &6"+ section);
return;
}
sendMessage(sender, "&a[BattleTracker]&2 debugging for &6" + section +"&2 now &6" + on);
}
@MCCommand( cmds = {"set"}, perm=Defaults.ADMIN_PERM, usage="set <pvp | pve> <section> <true | false>")
public void pvpToggle(CommandSender sender, String pvp, String section, Boolean on){
boolean ispvp = pvp.equalsIgnoreCase("pvp");
String type = ispvp ? "PvP" : "PvE";
if (section.equalsIgnoreCase("msg") || section.equalsIgnoreCase("message")){
if (ispvp){
Defaults.PVP_MESSAGES = on;
} else {
Defaults.PVE_MESSAGES = on;
}
sendMessage(sender, "&a[BattleTracker]&2 "+type+" messages now &6" + on);
} else {
sendMessage(sender, "&cDebugging couldnt find section &6"+ section);
sendMessage(sender, "&cValid sections: &6msg");
}
}
@MCCommand(cmds={"spawn"},op=true, usage="addkill <player1> <player2>: this is a debugging method")
public boolean spawn(Player sender, Integer n){
World w = Bukkit.getWorld("world");
for (int i=0;i<n;i++){
w.spawnEntity(sender.getLocation(), EntityType.ZOMBIE);
}
return true;
}
@MCCommand(cmds={"setRating"},op=true)
public boolean setRating(CommandSender sender, String db, OfflinePlayer player, Integer rating){
if (!Tracker.hasInterface(db))
return sendMessage(sender,"&c"+db +" does not exist");
TrackerInterface ti = Tracker.getInterface(db);
ti.setRating(player, rating);
return sendMessage(sender, player.getName() +" rating now " + rating);
}
@MCCommand(cmds={"resetRatings"},op=true)
public boolean resetRatings(CommandSender sender, String db){
if (!Tracker.hasInterface(db))
return sendMessage(sender,"&c"+db +" does not exist");
TrackerInterface ti = Tracker.getInterface(db);
ti.resetStats();
return sendMessage(sender,"&2All stats reset for &6" + ti.getInterfaceName() );
}
@MCCommand(cmds={"reload"},perm=Defaults.ADMIN_PERM)
public boolean reload(CommandSender sender){
Tracker.getSelf().loadConfigs();
return sendMessage(sender, "&2Configs reloaded for BattleTracker");
}
@MCCommand(cmds={"hide"},perm=Defaults.ADMIN_PERM)
public boolean hide(CommandSender sender, String db, OfflinePlayer player, Boolean hide){
if (!Tracker.hasInterface(db))
return sendMessage(sender,"&cDatabase "+db +" does not exist");
TrackerInterface ti = Tracker.getInterface(db);
ti.hidePlayer(player.getName(), hide);
return sendMessage(sender, "&2Player &6" + player.getName() +"&2 hiding="+hide);
}
@MCCommand(cmds={"top"})
public boolean top(CommandSender sender, String db){
if (!Tracker.hasInterface(db))
return sendMessage(sender,"&cDatabase "+db +" does not exist");
TrackerInterface ti = Tracker.getInterface(db);
ti.printTopX(sender, StatType.RATING, 10);
return true;
}
@MCCommand(cmds={"top"}, order=2)
public boolean top(CommandSender sender, String db, int x){
if (!Tracker.hasInterface(db))
return sendMessage(sender,"&c"+db +" does not exist");
TrackerInterface ti = Tracker.getInterface(db);
ti.printTopX(sender, StatType.RATING, 10, x);
return true;
}
@MCCommand(cmds={"showConfigOptions"}, op=true)
public boolean showConfigVars(CommandSender sender){
ReflectionToStringBuilder rtsb = new ReflectionToStringBuilder(
Defaults.class, ToStringStyle.MULTI_LINE_STYLE);
return sendMessage(sender, rtsb.toString());
}
}
@@ -0,0 +1,177 @@
package mc.alk.tracker.executors;
import java.util.List;
import mc.alk.tracker.TrackerInterface;
import mc.alk.tracker.controllers.MessageController;
import mc.alk.tracker.objects.Stat;
import mc.alk.tracker.objects.StatType;
import mc.alk.tracker.objects.VersusRecords.VersusRecord;
import mc.alk.tracker.objects.WLT;
import mc.alk.tracker.objects.WLTRecord;
import mc.alk.tracker.util.TimeUtil;
import mc.alk.v1r7.executors.CustomCommandExecutor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class TrackerExecutor extends CustomCommandExecutor {
final TrackerInterface ti;
public static final int MAX_RECORDS = 100;
public TrackerExecutor(TrackerInterface ti) {
super();
this.ti =ti;
}
@MCCommand(cmds={"top"}, usage="top [x] [team size]")
public boolean showTopXOther(CommandSender sender, String[] args){
int x = 5;
StatType st = null;
if (args.length > 1){
int xIndex = 1;
st = StatType.fromName(args[1]);
if (st != null){
xIndex = 2;
}
if (args.length > xIndex)
try {x = Integer.valueOf(args[xIndex]);}catch (Exception e){}
}
if (x<=0 || x > 100){
return sendMessage(sender,MessageController.getMsg("xBetween", MAX_RECORDS));}
List<Stat> stats = st == null ? ti.getTopXRating(x) : ti.getTopX(st, x);
String stname = st == null ? "Rating" : st.getName();
int min = Math.min(x, stats.size());
if (min==0){
return sendMessage(sender,MessageController.getMsg("noRecordsInTable", ti.getInterfaceName()));}
sendMessage(sender,"&4=============== &6"+ti.getInterfaceName()+" "+stname+"&4===============");
Stat stat;
for (int i=0;i<min;i++){
stat = stats.get(i);
sendMessage(sender,"&6"+(i+1)+"&e: &c" + stat.getName()+"&6["+stat.getRating()+"] &eWins(&6"+stat.getWins()+
"&e),Losses(&8"+stat.getLosses()+"&e),Streak(&b"+stat.getStreak()+"&e) W/L(&c"+String.format("%.2f", stat.getKDRatio())+"&e)");
}
return true;
}
@MCCommand(cmds={"versus","vs"}, usage="vs <player>")
public boolean versus(Player player1, String player2){
return versus(player1, player1.getName(), player2, 5);
}
@MCCommand(cmds={"versus","vs"}, usage="vs <player> <# records>")
public boolean versus(Player player1, String player2, Integer nRecords){
return versus(player1, player1.getName(), player2, nRecords);
}
private boolean versus(CommandSender sender, String player1, String player2, Integer x) {
if (x<=0 || x > 100){
return sendMessage(sender,MessageController.getMsg("xBetween", MAX_RECORDS));}
Stat stat1 = findStat(player1);
if (stat1 == null){
return sendMessage(sender,MessageController.getMsg("recordNotFound", player1));}
Stat stat2 = findStat(player2);
if (stat2 == null){
return sendMessage(sender,MessageController.getMsg("recordNotFound", player2));}
ti.save(stat1,stat2);
VersusRecord or = stat1.getRecordVersus(stat2);
sendMessage(sender,"&4======================== &6"+ti.getInterfaceName()+" &4========================");
sendMessage(sender,"&4================ &6"+stat1.getName()+" ("+stat1.getRating()+")&e vs &6" +
stat2.getName()+"("+stat2.getRating()+") &4================");
sendMessage(sender,"&eOverall Record (&2"+or.wins +" &e:&8 "+or.losses+" &e)");
List<WLTRecord> records = ti.getVersusRecords(stat1.getName(), stat2.getName(),x);
int min = Math.min(x, records.size());
for (int i=0;i< min;i++){
WLTRecord wlt = records.get(i);
final String color = wlt.wlt == WLT.WIN ? "&2" : "&8";
sendMessage(sender,color+wlt.wlt +"&e : &6" + TimeUtil.convertLongToDate(wlt.date));
}
return true;
}
@MCCommand(cmds={"addKill"},op=true,usage="addkill <player1> <player2>: this is a debugging method")
public boolean addKill(CommandSender sender, String p1, String p2){
Stat stat = ti.loadPlayerRecord(p1);
Stat stat2 = ti.loadPlayerRecord(p2);
if (stat == null || stat2 == null){
sender.sendMessage("Player not found");
return true;}
ti.addStatRecord(stat, stat2, WLT.WIN);
try {
VersusRecord or = stat.getRecordVersus(stat2);
sendMessage(sender, stat.getName()+ " versus " + stat2.getName()+" (&4"+or.wins +"&e:&8"+or.losses+"&e)");
} catch(Exception e){
}
return true;
}
protected boolean addKill(CommandSender sender, Player p1, Player p2) {
ti.addPlayerRecord(p1.getName(), p2.getName(), WLT.WIN);
return sendMessage(sender, "Added kill " + p1.getDisplayName() + " wins over " + p2.getDisplayName());
}
protected String getFullStatMsg(Stat stat) {
StringBuilder sb = new StringBuilder();
sb.append("&5"+stat.getName() +"&6["+stat.getRating()+"] &eWins(&6"+stat.getWins()+"&e),Losses(&8"+stat.getLosses()+
"&e),Streak(&b"+stat.getStreak()+"&e),MaxStreak(&7"+stat.getMaxStreak()+"&e) W/L(&c"+String.format("%.2f", stat.getKDRatio())+"&e)");
return sb.toString();
}
protected String getStatMsg(Stat stat1, Stat stat2) {
StringBuilder sb = new StringBuilder();
sb.append(getFullStatMsg(stat2));
VersusRecord or = stat1.getRecordVersus(stat2);
sb.append("record versus (&4"+or.wins +"&e:&8"+or.losses+"&e)");
return sb.toString();
}
@MCCommand()
public boolean showStatsSelf(Player p) {
Stat stat = ti.loadRecord(p);
String msg = getFullStatMsg(stat);
return sendMessage(p, msg);
}
@MCCommand
public boolean showStatsOther(CommandSender sender, OfflinePlayer p) {
/// Try to find the stat from what they typed
Stat stat = findStat(p.getName());
if (stat == null){ /// Find a player matching that name, hopefully
return sendMessage(sender,MessageController.getMsg("recordNotFound", p.getName()));}
String msg=null;
if (sender instanceof Player){
Stat selfStat = ti.loadRecord((Player)sender);
if (selfStat == null){ ///
sendMessage(sender, "&cYou have no records, Showing record for &6"+stat.getName());
msg = getFullStatMsg(stat);
} else {
msg = getStatMsg(selfStat,stat);
}
} else {
sendMessage(sender, "&2Showing record for &6" + stat.getName());
msg = getFullStatMsg(stat);
}
return sendMessage(sender, msg);
}
public Stat findStat(String name){
Stat stat = ti.getRecord(name);
if (stat == null){ /// Find a player matching that name, hopefully
OfflinePlayer op = findOfflinePlayer(name);
if (op == null){
return null;}
stat = ti.loadRecord(op);
}
return stat;
}
}
@@ -0,0 +1,313 @@
package mc.alk.tracker.listeners;
import mc.alk.tracker.Defaults;
import mc.alk.tracker.Tracker;
import mc.alk.tracker.TrackerInterface;
import mc.alk.tracker.TrackerOptions;
import mc.alk.tracker.controllers.ConfigController;
import mc.alk.tracker.controllers.MessageController;
import mc.alk.tracker.controllers.TrackerController;
import mc.alk.tracker.objects.SpecialType;
import mc.alk.tracker.objects.Stat;
import mc.alk.tracker.objects.WLT;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.AnimalTamer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Tameable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class BTEntityListener implements Listener{
static JavaPlugin plugin;
static final String UNKNOWN = "unknown";
ConcurrentHashMap<String,Long> lastDamageTime = new ConcurrentHashMap<String,Long>();
ConcurrentHashMap<String,RampageStreak> lastKillTime = new ConcurrentHashMap<String,RampageStreak>();
static HashSet<String> ignoreEntities = new HashSet<String>();
static HashSet<UUID> ignoreWorlds = new HashSet<UUID>();
Random r = new Random();
TrackerInterface playerTi;
TrackerInterface worldTi;
int count = 0;
class RampageStreak{
Long time; int nkills;
public RampageStreak(Long t, int nk){this.time = t; this.nkills = nk;}
}
public BTEntityListener(){
TrackerOptions to = new TrackerOptions();
to.setSaveIndividualRecords(true);
playerTi = Tracker.getInterface(Defaults.PVP_INTERFACE,to);
worldTi = Tracker.getInterface(Defaults.PVE_INTERFACE);
plugin = Tracker.getSelf();
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerDeath(PlayerDeathEvent event) {
ede(event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onEntityDeath(EntityDeathEvent event) {
if (event instanceof PlayerDeathEvent)
return;
/// we have a player killing a mob, if we are not tracking pve
/// we don't need to enter, no messages are usually sent for this
if (!ConfigController.getBoolean("trackPvE",false))
return;
ede(event);
}
private void ede(EntityDeathEvent event) {
if (ignoreWorlds.contains(event.getEntity().getWorld().getUID()))
return;
String target, killer;
boolean targetPlayer = false, killerPlayer = false;
boolean isMelee = true;
ItemStack killingWeapon = null;
/// Get our hapless target
Entity targetEntity = event.getEntity();
if (targetEntity instanceof Player){
target = ((Player)targetEntity).getName();
targetPlayer = true;
} else if (targetEntity instanceof Tameable){
target = "Tamed" + targetEntity.getType().getName();
} else {
target = targetEntity.getType().getName();
}
/// Should we be tracking this person
if (targetPlayer && (TrackerController.dontTrack(target))){
if (event instanceof PlayerDeathEvent)
((PlayerDeathEvent) event).setDeathMessage(""); /// Set to none, will cancel all non pvp messages
return;
}
if (!targetPlayer && !ConfigController.getBoolean("trackPvP") && !ConfigController.getBoolean("sendPVPDeathMessages")){
return;
}
/// Determine our killer
EntityDamageEvent lastDamageCause = targetEntity.getLastDamageCause();
Player killerEntity = null;
if (lastDamageCause instanceof EntityDamageByEntityEvent){
Entity damager = ((EntityDamageByEntityEvent) lastDamageCause).getDamager();
if (damager instanceof Player) { /// killer is player
killerEntity = (Player) damager;
killer = killerEntity.getName();
killerPlayer = true;
killingWeapon = killerEntity.getItemInHand();
} else if (damager instanceof Projectile) { /// we have some sort of projectile
isMelee = false;
Projectile proj = (Projectile) damager;
if (proj.getShooter() instanceof Player){ /// projectile was shot by a player
killerPlayer = true;
killerEntity = (Player) proj.getShooter();
killer = killerEntity.getName();
killingWeapon = killerEntity.getItemInHand();
} else if (proj.getShooter() != null){ /// projectile shot by some mob, or other source
killer = proj.getShooter().getClass().getSimpleName();
} else {
killer = UNKNOWN; /// projectile was null?
}
} else if (damager instanceof Tameable && ((Tameable) damager).isTamed()) {
AnimalTamer at = ((Tameable) damager).getOwner();
if (at != null){
if (at instanceof Player){
killerPlayer = true;
killerEntity = (Player) at;
}
killer = at.getName();
} else {
killer = damager.getType().getName();
}
} else { /// Killer is not a player
killer = damager.getType().getName();
}
} else {
if (lastDamageCause == null || lastDamageCause.getCause() == null)
killer = UNKNOWN;
else
killer = lastDamageCause.getCause().name();
}
if (killer == null)
killer = UNKNOWN;
if (killerPlayer && TrackerController.dontTrack(killer))
return;
if (ignoreEntities.contains(killer) || ignoreEntities.contains(targetEntity.toString()) ||
ignoreEntities.contains(target))
return;
/// Decide what to do
if (targetPlayer && killerPlayer){
/// Check to see if we add the records
if (ConfigController.getBoolean("trackPvP",true))
addRecord(playerTi,killer,target,WLT.WIN);
PlayerDeathEvent pde = (PlayerDeathEvent) event;
/// Check to see if an we have disabled death messages
if (!Defaults.PVP_MESSAGES && !Defaults.BUKKIT_PVP_MESSAGES){
/// Even with disabled messages, we can still send messages to involved players
if (Defaults.INVOLVED_PVP_MESSAGES){
String msg = getPvPDeathMessage(killer,target,isMelee,playerTi,killingWeapon);
sendMessage(killerEntity, (Player)targetEntity,msg);
} else {
sendMessage(pde,null);
}
return;
}
/// Check sending custom PvP messages
if (Defaults.PVP_MESSAGES){
// final String wpn = killingWeapon != null ? killingWeapon.getType().name().toLowerCase() : null;
String msg = getPvPDeathMessage(killer,target,isMelee,playerTi,killingWeapon);
sendMessage(pde,msg);
}
/// else we just let bukkit handle it like normal
} else if (!targetPlayer && !killerPlayer){ /// mobs killing each other, or dying by traps
/// Do nothing
} else { /// One player, One other
/// Get rid of Craft before mobs.. CraftSpider -> Spider
if (!killerPlayer && killer.contains("Craft")){
killer = killer.substring(5);}
if (!targetPlayer && target.contains("Craft")){
target = target.substring(5);}
/// Should we track the kills?
if (ConfigController.getBoolean("trackPvE",true)){
addRecord(worldTi, killer,target,WLT.WIN);}
/// Check message sending
if (targetPlayer && event instanceof PlayerDeathEvent){
/// Check to see if an admin has disabled death messages
PlayerDeathEvent pde = (PlayerDeathEvent) event;
if (!Defaults.PVE_MESSAGES && !Defaults.BUKKIT_PVE_MESSAGES){
if (Defaults.INVOLVED_PVE_MESSAGES){
final String wpn = killingWeapon != null ? killingWeapon.getType().name().toLowerCase() : null;
String msg = getPvEDeathMessage(killer,target,isMelee,wpn);
sendMessage(null,(Player) targetEntity,msg);
} else {
pde.setDeathMessage(null);
}
return;
}
if (Defaults.PVE_MESSAGES){
final String wpn = killingWeapon != null ? killingWeapon.getType().name().toLowerCase() : null;
String msg = getPvEDeathMessage(killer,target,isMelee,wpn);
sendMessage(pde,msg);
}
/// Else let bukkit handle
}
}
}
private void sendMessage(Player killerEntity, Player targetEntity, String msg) {
if (killerEntity != null){
MessageController.sendMessage(killerEntity, msg);}
if (targetEntity != null){
MessageController.sendMessage(targetEntity, msg);}
}
private void sendMessage(PlayerDeathEvent event, String msg){
if (msg == null){
event.setDeathMessage(null);
return;
}
if (Defaults.RADIUS <= 0){
event.setDeathMessage(msg);
} else {
Player player = event.getEntity();
if (player == null){
event.setDeathMessage(msg);
return;
}
Location l = player.getLocation();
if (l==null){
event.setDeathMessage(msg);
return;
}
event.setDeathMessage(null);
UUID wid = l.getWorld().getUID();
Collection<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers();
Player players[] = onlinePlayers.toArray(new Player[onlinePlayers.size()]);
for (Player p: players){
if (wid != p.getLocation().getWorld().getUID() || p.getLocation().distanceSquared(l) >= Defaults.RADIUS){
continue;}
p.sendMessage(msg);
}
}
}
public String getPvPDeathMessage(String killer, String target, boolean isMeleeDeath,
TrackerInterface ti, ItemStack killingWeapon){
/// Check for a rampage
try {
RampageStreak lastKill = lastKillTime.get(killer);
long now = System.currentTimeMillis() ;
if (lastKill != null && now - lastKill.time < Defaults.RAMPAGE_TIME){
lastKill.nkills++;
lastKill.time = now;
return MessageController.getSpecialMessage(SpecialType.RAMPAGE, lastKill.nkills, killer,target, killingWeapon);
} else {
lastKillTime.put(killer, new RampageStreak(now, 1));
}
} catch (Exception e){
/// in the unlikely case of a Concurrent modification exception, lets not crash
/// but instead send a normal message
}
/// If a normal streak
/// Player has a multiple of x, send they are on a streak message
Stat stat = ti.loadPlayerRecord(killer);
final int streak = stat.getStreak();
boolean hasStreak = MessageController.contains("special.streak."+streak);
/// they are on a streak
if (hasStreak || streak != 0 && Defaults.STREAK_EVERY != 0 && streak % Defaults.STREAK_EVERY== 0){
return MessageController.getSpecialMessage(SpecialType.STREAK, streak, killer,target, killingWeapon);
} else { /// Display a normal kill message
return MessageController.getPvPMessage(isMeleeDeath,killer, target, killingWeapon);
}
}
public String getPvEDeathMessage(String p1, String p2, boolean isMeleeDeath, String killingWeapon){
return MessageController.getPvEMessage(isMeleeDeath, p1, p2,killingWeapon);
}
public static void addRecord(final TrackerInterface ti,final String e1, final String e2, final WLT record){
ti.addPlayerRecord(e1, e2, record);
}
public static void setIgnoreEntities(List<String> list) {
ignoreEntities.clear();
if (list != null)
ignoreEntities.addAll(list);
}
public static void setIgnoreWorlds(List<String> list) {
ignoreWorlds.clear();
if (list != null){
for (String s: list){
World w = Bukkit.getWorld(s);
if (w != null){
ignoreWorlds.add(w.getUID());}
}
}
}
}
@@ -0,0 +1,47 @@
package mc.alk.tracker.listeners;
import mc.alk.tracker.Tracker;
import mc.alk.tracker.controllers.MessageController;
import mc.alk.tracker.plugins.BTPlaceholderExtension;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import com.dthielke.herochat.Herochat;
/**
*
* @author alkarin
*
*/
public class BTPluginListener implements Listener {
@EventHandler
public void onPluginEnable(PluginEnableEvent event) {
loadPlugins();
}
public static void loadPlugins(){
///HeroChat
Herochat hc = MessageController.getHeroChat();
if (hc == null) {
Plugin plugin = Bukkit.getServer().getPluginManager().getPlugin("HeroChat");
hc = ((Herochat) plugin);
if (hc != null){
MessageController.setHeroChat(hc);
PluginDescriptionFile pDesc = plugin.getDescription();
System.out.println("[" + Tracker.getSelf().getVersion() + "] loaded " + pDesc.getName() +
" version " + pDesc.getVersion());
}
}
// PlaceholderAPI
if (Bukkit.getServer().getPluginManager().getPlugin("PlaceholderAPI") != null) {
new BTPlaceholderExtension().register();
}
}
}
@@ -0,0 +1,146 @@
package mc.alk.tracker.listeners;
import mc.alk.tracker.Defaults;
import mc.alk.tracker.Tracker;
import mc.alk.tracker.controllers.MessageController;
import mc.alk.tracker.controllers.SignController;
import mc.alk.tracker.objects.SignType;
import mc.alk.tracker.objects.StatSign;
import mc.alk.tracker.objects.StatType;
import mc.alk.tracker.objects.exceptions.InvalidSignException;
import mc.alk.v1r7.util.AutoClearingTimer;
import mc.euro.bukkitadapter.material.BattleMaterial;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
public class SignListener implements Listener{
SignController signController;
AutoClearingTimer<String> timer = new AutoClearingTimer<String>();
public static final int SECONDS = 5;
public SignListener(SignController signController){
this.signController = signController;
timer.setSaveEvery(61050); /// will auto clear records every minute, and 1sec
}
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if (event.isCancelled() || event.getClickedBlock() == null)
return;
final Block block = event.getClickedBlock();
if (!(block.getState() instanceof Sign)) {
return ;
}
StatSign ss = signController.getStatSign(event.getClickedBlock().getLocation());
if (ss == null)
return;
if (timer.withinTime(event.getPlayer().getName(),SECONDS*1000L)){
event.getPlayer().sendMessage(ChatColor.RED+"wait");
return;
} else {
timer.put(event.getPlayer().getName());
final Location l = block.getLocation();
Bukkit.getScheduler().scheduleSyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run(){
l.getWorld().getBlockAt(l).getState().update(true);
}
},SECONDS*20);
}
signController.clickedSign(event.getPlayer(), (Sign) block.getState(), ss);
}
@EventHandler
public void onBlockBreak(BlockBreakEvent event){
final Block block = event.getBlock();
if (!(block.getState() instanceof Sign)) {
return;
}
Sign s = (Sign)block.getState();
final String l = s.getLine(0);
if (l == null || l.isEmpty() || l.charAt(0) != '[')
return;
signController.removeSignAt(s.getLocation());
}
@EventHandler
public void onSignChange(SignChangeEvent event){
final Block block = event.getBlock();
if (!(block.getState() instanceof Sign)) {
return;
}
StatSign ss;
try {
ss = getStatSign(block.getLocation(), event.getLines());
} catch (InvalidSignException e) {
MessageController.sendMessage(event.getPlayer(), e.getMessage());
return;
}
if (ss == null){
return;}
if (!event.getPlayer().hasPermission(Defaults.ADMIN_PERM) && !event.getPlayer().isOp()){
MessageController.sendMessage(event.getPlayer(), "&cYou don't have perms to create top signs");
cancelSignPlace(event, block);
return;
}
String lines[] = event.getLines();
lines[0] = "[&e"+ss.getDBName()+"&0]";
lines[1] = "[&e"+ss.getStatType()+"&0]";
lines[2] = "&2Updating";
for (int i=0;i<lines.length;i++){
lines[i] = MessageController.colorChat(lines[i]);
}
signController.addSign(ss);
Bukkit.getScheduler().scheduleSyncDelayedTask(Tracker.getSelf(), new Runnable(){
@Override
public void run() {
signController.updateSigns();
Tracker.getSelf().saveConfig();
}
},40);
}
private StatSign getStatSign(Location l, String lines[]) throws InvalidSignException {
/// Quick check to make sure this is even a stat sign
/// make sure first two lines are not null or empty.. line 1 starts with '['
if (lines.length < 2 ||
lines[0] == null || lines[0].isEmpty() || lines[0].charAt(0) != '[' ||
lines[1] == null || lines[1].isEmpty()){
return null;}
/// find the Sign Type, like top, personal
String strType = lines[1].replace('[', ' ').replace(']', ' ').trim();
SignType signType = SignType.fromName(strType);
if (signType == null)
return null;
/// find the database
String db = lines[0].replace('[', ' ').replace(']', ' ').trim();
if (!Tracker.hasInterface(db)){
throw new InvalidSignException("Tracker Database " + db +" not found");}
StatType st = StatType.fromName(strType);
if (st == null){
return null;}
StatSign ss = new StatSign(db, l, SignType.TOP); /// TODO change when we have more than 1 type
ss.setStatType(st);
return ss;
}
// TODO: Implement way to drop the proper sign for 1.14+
public void cancelSignPlace(SignChangeEvent event, Block block){
event.setCancelled(true);
block.setType(Material.AIR);
block.getWorld().dropItemNaturally(block.getLocation(), new ItemStack(BattleMaterial.OAK_SIGN.parseMaterial(), 1));
}
}
@@ -0,0 +1,94 @@
package mc.alk.tracker.objects;
import mc.alk.tracker.TrackerInterface;
import mc.alk.battlebukkitlib.objects.Hologram;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import java.util.List;
public class LeaderboardHologram extends Hologram
{
private String leaderboardName;
private int topAmount;
private TrackerInterface trackerInterface;
private StatType statType;
public LeaderboardHologram(TrackerInterface trackerInterface,
StatType statType, String leaderboardName, int topAmount,
double distanceBetweenLines, Location location)
{
super(distanceBetweenLines, location, ChatColor.GREEN+ "Updating...");
setLeaderboardName(leaderboardName);
setTrackerInterface(trackerInterface);
setStatType(statType);
setTopAmount(topAmount);
update();
}
public LeaderboardHologram(TrackerInterface trackerInterface,
StatType statType, String leaderboardName, int topAmount,
VerticalTextSpacing spacingType, Location location)
{
super(spacingType.spacing(), location, ChatColor.GREEN+ "Updating...");
setLeaderboardName(leaderboardName);
setTrackerInterface(trackerInterface);
setStatType(statType);
setTopAmount(topAmount);
update();
}
public String getLeaderboardName()
{
return leaderboardName;
}
public TrackerInterface getTrackerInterface()
{
return trackerInterface;
}
public StatType getStatType()
{
return statType;
}
public int getTopAmount()
{
return topAmount;
}
public void setLeaderboardName(String name)
{
getLines().remove(getLeaderboardName());
getLines().add(0, leaderboardName);
this.leaderboardName = name;
}
public void setTrackerInterface(TrackerInterface trackerInterface)
{
this.trackerInterface = trackerInterface;
}
public void setTopAmount(int amount)
{
this.topAmount = amount;
}
public void setStatType(StatType type)
{
this.statType = type;
}
public void update()
{
getLines().clear();
setLeaderboardName(getLeaderboardName());
List<Stat> stats = getTrackerInterface().getTopX(getStatType(),
getTopAmount());
for (Stat stat : stats)
{
getLines().add(stat.getName() + " - " + stat.getWins());
}
}
}
@@ -0,0 +1,20 @@
package mc.alk.tracker.objects;
import java.util.ArrayList;
import org.bukkit.OfflinePlayer;
public class PlayerStat extends TeamStat{
public PlayerStat(OfflinePlayer player) {
this(player.getName());
}
public PlayerStat(String player) {
this.name = this.strid = player;
members = new ArrayList<String>();
members.add(player);
count = 1;
}
}
@@ -0,0 +1,22 @@
package mc.alk.tracker.objects;
public enum SignType {
TOP, PERSONAL;
public static SignType fromName(String name) {
if (name ==null)
return null;
name = name.toUpperCase();
SignType gt = null;
try{
gt = SignType.valueOf(name);
} catch (Exception e){}
if (gt == null){
StatType st = StatType.fromName(name);
if (st != null)
gt = SignType.PERSONAL;
}
return gt;
}
}
@@ -0,0 +1,5 @@
package mc.alk.tracker.objects;
public enum SpecialType {
STREAK, RAMPAGE
}
+269
View File
@@ -0,0 +1,269 @@
package mc.alk.tracker.objects;
import mc.alk.tracker.Defaults;
import mc.alk.tracker.controllers.TrackerImpl;
import mc.alk.tracker.events.MaxRatingChangeEvent;
import mc.alk.tracker.events.WinStatChangeEvent;
import mc.alk.tracker.objects.VersusRecords.VersusRecord;
import mc.alk.tracker.ranking.EloCalculator;
import mc.alk.tracker.util.Cache.CacheObject;
import mc.alk.tracker.util.Util;
import mc.alk.v1r7.util.Log;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public abstract class Stat extends CacheObject<String,Stat>{
protected String strid = null;
protected String name;
protected float rating = EloCalculator.DEFAULT_ELO;
protected float maxRating = rating;
protected int wins = 0, losses= 0, ties = 0;
protected int streak = 0, maxStreak =0;
protected int count = 1; /// How many members are in the team
boolean hide = false;
List<String> members ;
VersusRecords vRecord = null;
private TrackerImpl parent;
@Override
public String getKey() {
if (strid.length() > 32 )
Util.printStackTrace();
return getStrID();
}
public List<String> getMembers() {
return members;
}
public String getStrID(){return strid;}
public void setName(String name) {
this.name = name; setDirty();
if (strid != null && strid.length() > 32 ){
Log.err("NAME = " + name +" strid=" + strid);
Util.printStackTrace();
}
}
public String getName(){return name;}
public void setWins(int wins) {this.wins = wins;setDirty();}
public int getWins() {return wins;}
public void setStreak(int i){streak = i;setDirty();}
public int getStreak() { return streak;}
public void setLosses(int i){losses = i;setDirty();}
public int getLosses() {return losses;}
public void setTies(int i){ties = i;setDirty();}
public int getTies() {return ties;}
public int getCount() { return count;}
public void setCount(int i){count = i;setDirty();}
public float getKDRatio() { return ((float) wins) / losses;}
public void incLosses() {
streak = 0;
losses++;
setDirty();
}
public void incTies(){
streak = 0;
ties++;
setDirty();
}
public void incWins() {
wins++;
incStreak();
setDirty();
}
public void incStreak() {
streak++;
if (streak > maxStreak)
maxStreak = streak;
setDirty();
}
public void endStreak() {streak=0;setDirty();}
public int getMaxStreak() {return maxStreak;}
public void setMaxStreak(int maxStreak) {this.maxStreak = maxStreak;setDirty();}
public void setMaxRating(int maxRating) {this.maxRating = maxRating;setDirty();}
public int getRating() {
return (int) rating;
}
public int getMaxRating() {return (int) maxRating;}
public void setRating(float rating){
this.rating = rating;
if (this.rating > maxRating){
int threshold = ( ((int)maxRating) /100) *100 + 100;
double oldRating = maxRating;
maxRating = this.rating;
if (maxRating < threshold && this.rating >= threshold){
maxRating = this.rating;
new MaxRatingChangeEvent(parent,this,oldRating).callSyncEvent();
}
}
setDirty();
}
@Override
public boolean equals( Object obj ) {
if(this == obj) return true;
if((obj == null) || (obj.getClass() != this.getClass())) return false;
TeamStat test = (TeamStat)obj;
return this.compareTo(test) == 0;
}
/**
* Teams are ordered list of strings
*/
public int compareTo(TeamStat o) {
return this.strid.compareTo(o.strid);
}
public VersusRecords getRecord(){
if (vRecord == null)
vRecord = new VersusRecords(getKey(),parent.getSQL()) ;
return vRecord;
}
public void win(Stat ts) {
if (Defaults.DEBUG_ADD_RECORDS) System.out.println("BT Debug: win: tsID="+ts.getStrID() +
" parent=" + parent +" " + (parent !=null? parent.getSQL() : "null") + " key=" + getKey());
wins++;
streak++;
if (streak > maxStreak){
maxStreak=streak;}
getRecord().addWin(ts.getStrID());
new WinStatChangeEvent(parent,this,ts).callSyncEvent();
setDirty();
}
public void loss(Stat ts) {
losses++;
streak =0;
getRecord().addLoss(ts.getStrID());
setDirty();
}
public void tie(Stat ts) {
ties++;
getRecord().addTie(ts.getStrID());
setDirty();
}
public static String getKey(Player players){
return players.getName();
}
public static String getKey(String player){
return player;
}
protected static String getKey(List<String> playernames){
Collections.sort(playernames);
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String s : playernames){
if (!first) sb.append(",");
sb.append(s);
first = false;
}
if (sb.length() > 32){
return sb.toString().hashCode() + "";
}
return sb.toString();
}
public VersusRecord getRecordVersus(Stat stat) {
/// We cant get a record if we have no way of loading
if (vRecord == null){
vRecord = getRecord();}
return vRecord.getRecordVersus(stat.getStrID());
}
protected void createName(){
if (name != null && !name.isEmpty()) /// We have a specified name, dont use the naive append all players together
return;
// System.out.println("name="+name);
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String n : members){
if (!first) sb.append(",");
sb.append(n);
first = false;
}
name = sb.toString();
// System.out.println("afterwards name="+name);
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder();
sb.append("[Team=" + getName() + " ["+getRating()+":"+getKDRatio()+"](" + getWins() + ":" + getLosses() + ":" + getStreak() +") id="+strid +
",count="+count+",p.size="+ (members==null?"null" : members.size()) );
if (vRecord != null){
sb.append(" [Kills]= ");
HashMap<String,List<WLTRecord>> records = vRecord.getIndividualRecords();
if (records != null){
for (String tk : records.keySet()){
sb.append(tk +":" + vRecord.getIndividualRecords().get(tk) +" ," );}
}
}
sb.append("]");
return sb.toString();
}
public VersusRecords getRecordSet() {
return vRecord;
}
public void setParent(TrackerImpl parent) {
this.parent = parent;
}
public void setSaveIndividual(boolean saveIndividualRecord) {
if (vRecord != null)
vRecord.setSaveIndividual(saveIndividualRecord);
}
public int getFlags() {
return hide ? 1 : 0;
}
public void setFlags(int flags) {
hide = (flags == 0 ? false : true);
}
public boolean isHidden(){
return hide;
}
public void hide(boolean hide) {
if (this.hide != hide){
setDirty();
this.hide = hide;
}
}
public float getStat(StatType statType) {
switch(statType){
case WINS: case KILLS: return getWins();
case LOSSES: case DEATHS: return getLosses();
case RANKING: case RATING: return getRating();
case KDRATIO : case WLRATIO : return getKDRatio();
case MAXRANKING : case MAXRATING : return getMaxRating();
case MAXSTREAK: return getMaxStreak();
case STREAK: return getStreak();
case TIES: return getTies();
default:
break;
}
return 0;
}
}
@@ -0,0 +1,30 @@
package mc.alk.tracker.objects;
public class StatChange {
final Stat team1;
final Stat team2;
final WLT wlt;
final boolean changeWinLossRecords;
public StatChange(final Stat team1, final Stat team2,
final WLT wlt, final boolean changeWinLossRecords){
this.team1 = team1;
this.team2 = team2;
this.wlt = wlt;
this.changeWinLossRecords = changeWinLossRecords;
}
public Stat getTeam1() {
return team1;
}
public Stat getTeam2() {
return team2;
}
public WLT getWlt() {
return wlt;
}
public boolean isChangeWinLossRecords() {
return changeWinLossRecords;
}
}
@@ -0,0 +1,92 @@
package mc.alk.tracker.objects;
import java.util.HashMap;
import java.util.Map;
import mc.alk.v1r7.util.Log;
import mc.alk.v1r7.util.SerializerUtil;
import org.bukkit.Location;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
public class StatSign implements ConfigurationSerializable{
final String dbName;
final Location location;
final SignType type;
StatType statType = StatType.RATING;
public StatSign(String dbName, Location location, SignType type){
this.dbName = dbName;
this.location = location;
this.type = type;
}
@Override
public Map<String, Object> serialize() {
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("location", SerializerUtil.getLocString(location));
map.put("signType", type.toString());
map.put("dbName", dbName);
if (statType != null)
map.put("statType", statType.toString());
return map;
}
public static StatSign valueOf(Map<String, Object> map) {
return deserialize(map);
}
public static StatSign deserialize(Map<String, Object> map) {
Object signTypeStr = map.get("signType");
Object locStr = map.get("location");
Object statStr = map.get("statType");
Object dbStr = map.get("dbName");
if (signTypeStr == null || locStr == null || dbStr == null)
return null;
SignType type = SignType.valueOf(signTypeStr.toString());
Location l = null;
try{
l = SerializerUtil.getLocation(locStr.toString());
} catch (IllegalArgumentException e){
Log.warn("BattleTracker error retrieving sign at " + locStr);
}
if (type == null || l == null)
return null;
StatSign ss = new StatSign(dbStr.toString(),l,type);
if (statStr != null){
StatType statType = StatType.fromName(statStr.toString());
ss.setStatType(statType);
}
return ss;
}
public void setStatType(StatType statType) {
this.statType = statType;
}
public StatType getStatType(){
return statType;
}
public SignType getSignType() {
return type;
}
public String getLocationString() {
return SerializerUtil.getLocString(location);
}
public static String getLocationString(Location location){
return SerializerUtil.getLocString(location);
}
public Location getLocation(){
return location;
}
@Override
public String toString(){
return "["+SerializerUtil.getLocString(location)+ " : "+type+"]";
}
public String getDBName() {
return dbName;
}
}
@@ -0,0 +1,66 @@
package mc.alk.tracker.objects;
import org.bukkit.ChatColor;
public enum StatType {
WINS("Wins"),KILLS("Kills"), LOSSES("Losses"),DEATHS("Deaths"),TIES("Ties"),
STREAK("Streak"),MAXSTREAK("MaxStreak"), RANKING("Ranking"),MAXRANKING("MaxRanking"),
RATING("Rating"),MAXRATING("MaxRating"),
KDRATIO("K/D Ratio"),WLRATIO("W/L Ratio");
final String name;
StatType(String name){
this.name = name;
}
public String getName(){
return name;
}
@Override
public String toString(){
return name;
}
public static StatType fromName(String name) {
if (name ==null)
return null;
name = name.toUpperCase();
StatType gt = null;
try{
gt = StatType.valueOf(name);
} catch (Exception e){}
if (name.equals("KDR")){
return KDRATIO;
} else if (name.equals("WLR")){
return WLRATIO;
}
return gt;
}
public ChatColor color(){
switch(this){
case WLRATIO:
case KDRATIO:
return ChatColor.DARK_GREEN;
case WINS:
case KILLS:
return ChatColor.GREEN;
case DEATHS:
case LOSSES:
return ChatColor.DARK_RED;
case MAXRANKING:
case MAXRATING:
return ChatColor.GOLD;
case RANKING:
case RATING:
return ChatColor.GOLD;
case STREAK:
case MAXSTREAK:
return ChatColor.DARK_PURPLE;
case TIES:
return ChatColor.YELLOW;
default:
return ChatColor.YELLOW;
}
}
}
@@ -0,0 +1,56 @@
package mc.alk.tracker.objects;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Set;
public class TeamStat extends Stat implements Comparable<TeamStat>{
public TeamStat(){
}
public TeamStat(String name, boolean id){
this(name,id,0);
}
public int charCount(final String s, final Character testc){
int count =0;
for (int i=0;i< s.length();i++)
if (testc.equals(s.charAt(i)))
count++;
return count;
}
public TeamStat(String name, boolean id, int teamSize) {
if (id){
this.strid = name;
count = teamSize;
} else {
this.name = name;
this.strid = name;
/// If we have a comma delimited list, then we know the # of players, otherwise its a mystery(0)
int c = charCount(name,',');
count = c == 0 ? 0: c+1;
}
}
public TeamStat(Set<String> p){
this.members = new ArrayList<String>(p);
createName();
strid = TeamStat.getKey(this.members);
count = p.size();
}
public void setMembers(Collection<String> players){
// System.out.println("Setting members = " + players);
if (players == null)
return;
this.members = new LinkedList<String>(players);
strid = TeamStat.getKey(this.members);
createName();
}
}
@@ -0,0 +1,122 @@
package mc.alk.tracker.objects;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import mc.alk.tracker.objects.VersusRecords.VersusRecord;
import mc.alk.tracker.serializers.SQLInstance;
import mc.alk.tracker.util.Cache;
import mc.alk.tracker.util.Cache.CacheObject;
import mc.alk.tracker.util.Cache.CacheSerializer;
import org.apache.commons.lang.mutable.MutableBoolean;
public class VersusRecords implements CacheSerializer<List<String>,VersusRecord>{
String id;
SQLInstance sql;
Cache<List<String>, VersusRecord> totals = new Cache<List<String>, VersusRecord>(this);
HashMap<String,List<WLTRecord>> ind_records;
boolean saveIndividualRecord = true;
public VersusRecords(String myid, SQLInstance sql){
this.id = myid;
this.sql = sql;
}
public static class VersusRecord extends CacheObject<List<String>,VersusRecord>{
public int wins,losses, ties;
final public List<String> ids = new ArrayList<String>(2);
public VersusRecord(String id1, String id2){
ids.add(id1); ids.add(id2);
}
@Override
public List<String> getKey() {return ids;}
static public List<String>getKey(String id, String oid){return Arrays.asList(new String[]{id,oid});}
public void incWin() {wins++;setDirty();}
public void incLosses() {losses++;setDirty();}
public void incTies() {ties++;setDirty();}
}
private List<WLTRecord> getIndRecord(String opponent){
if (ind_records == null)
ind_records = new HashMap<String,List<WLTRecord>>();
List<WLTRecord> record = ind_records.get(opponent);
if (record == null) {
record = new ArrayList<WLTRecord>();
ind_records.put(opponent, record);
}
return record;
}
public void addWin(String oid) {
totals.get(VersusRecord.getKey(id, oid)).incWin();
if (saveIndividualRecord)
getIndRecord(oid).add(new WLTRecord(WLT.WIN));
}
public void addLoss(String oid) {
totals.get(VersusRecord.getKey(id, oid)).incLosses();
if (saveIndividualRecord)
getIndRecord(oid).add(new WLTRecord(WLT.LOSS));
}
public void addTie(String oid) {
totals.get(VersusRecord.getKey(id, oid)).incTies();
if (saveIndividualRecord)
getIndRecord(oid).add(new WLTRecord(WLT.TIE));
}
public VersusRecord getRecordVersus(String opponentId) {
return totals.get(new ArrayList<String>(VersusRecord.getKey(id, opponentId)));
}
@Override
public VersusRecord load(List<String> key, MutableBoolean dirty, Object... varArgs) {
VersusRecord or = sql.getVersusRecord(key.get(0), key.get(1));
if (or != null){
or.setCache(totals);
dirty.setValue(false);
} else {
or = new VersusRecord(key.get(0), key.get(1));
dirty.setValue(true);
}
return or;
}
@Override
public void save(List<VersusRecord> types) {
sql.realsaveVersusRecords(types);
sql.saveIndividualRecords(id, ind_records);
ind_records = null;
}
public Collection<VersusRecord> getOverallRecords() {
if (totals == null)
return null;
return totals.values();
}
public HashMap<String,List<WLTRecord>> getIndividualRecords() {
return ind_records;
}
public void setIndividualRecords(HashMap<String,List<WLTRecord>> ind) {
this.ind_records = ind;
}
public void flushOverallRecords() {
totals.flush();
}
public void setSaveIndividual(boolean saveIndividualRecord) {
this.saveIndividualRecord = saveIndividualRecord;
}
}
+20
View File
@@ -0,0 +1,20 @@
package mc.alk.tracker.objects;
public enum WLT {
LOSS,WIN,TIE;
public static WLT valueOf(int value) {
if (value >= WLT.values().length || value < 0)
return null;
return WLT.values()[value];
}
public WLT reverse() {
switch(this){
case LOSS: return WIN;
case WIN : return LOSS;
case TIE: return TIE;
}
return null;
}
}
@@ -0,0 +1,32 @@
package mc.alk.tracker.objects;
public class WLTRecord {
public final Long date;
public WLT wlt;
public WLTRecord(WLT wlt, Long date) {
this.wlt = wlt;
this.date = date;
}
public WLTRecord(WLT wlt) {
this.wlt = wlt;
this.date = System.currentTimeMillis();
}
public int compareTo(WLTRecord o) {
return this.date.compareTo(o.date);
}
public String toString(){
return "[WLT " + wlt + " " + date+"]";
}
public void reverse() {
wlt = wlt.reverse();
}
public Long getDate() {
return date;
}
}
@@ -0,0 +1,9 @@
package mc.alk.tracker.objects.exceptions;
public class InvalidSignException extends Exception{
private static final long serialVersionUID = 1L;
public InvalidSignException(String message) {
super(message);
}
}
@@ -0,0 +1,78 @@
package mc.alk.tracker.plugins;
import mc.alk.tracker.Tracker;
import mc.alk.tracker.TrackerInterface;
import mc.alk.tracker.objects.Stat;
import mc.alk.tracker.objects.StatType;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
public class BTPlaceholderExtension extends PlaceholderExpansion {
@Override
public String getIdentifier() {
return "BT";
}
@Override
public String getAuthor() {
return "BattlePlugins";
}
@Override
public String getVersion() {
return Tracker.getVersionObject().getVersion();
}
@Override
public String onPlaceholderRequest(Player player, String params) {
if (player == null || !player.isOnline())
return "";
String[] split = params.split("_");
String interfaceName = split[0];
// The interface is not tracked or does not exist
if (!Tracker.hasInterface(interfaceName))
return "";
TrackerInterface tracker = Tracker.getInterface(interfaceName);
Stat record = tracker.getRecord(player);
// Gets leaderboard stats (ex: %bt_pvp_top_wins_1%)
if (split[1].equalsIgnoreCase("top")) {
try {
Integer.parseInt(split[3]);
} catch (NumberFormatException ex) {
return null; // not a number at the end of the placeholder
}
String stat = split[2];
for (StatType type : StatType.values()) {
if (!stat.equalsIgnoreCase(type.name()))
continue;
int i = Integer.parseInt(split[3]);
Stat top = tracker.getTopX(type, i).get(i-1);
if (split.length >= 5 && split[4].equalsIgnoreCase("name")) {
return top.getName();
}
return String.valueOf(top.getStat(type));
}
}
// Gets player stats (ex: %bt_pvp_wins%)
for (StatType type : StatType.values()) {
if (split[1].equalsIgnoreCase(type.name())) {
return String.valueOf(record.getStat(type));
}
if (!split[1].equalsIgnoreCase("top"))
continue;
}
return null;
}
}
@@ -0,0 +1,77 @@
package mc.alk.tracker.ranking;
import mc.alk.tracker.objects.Stat;
import java.util.Collection;
public class EloCalculator implements RatingCalculator {
public static final float MIN_ELO = 100;
public static float DEFAULT_ELO = 1250;
float defaultElo, spread;
@Override
public String getName(){
return "Elo";
}
private float eloChange(Stat p1, Stat p2, float result){
final float k = getK(p1.getRating());
final float Di = p2.getRating() - p1.getRating();
final float expected = (float) ( 1.0f / (1 + Math.pow(10, Di/spread)));
float eloChange = k * (result-expected);
return eloChange;
}
@Override
public void changeRatings(Stat p1, Stat p2, boolean tie){
float result = tie ? 0.5f : 1.0f;
final float eloChange = eloChange(p1,p2,result);
final float p1elo = p1.getRating()+eloChange;
final float p2elo = p2.getRating()-eloChange;
p1.setRating(p1elo > MIN_ELO? p1elo : MIN_ELO);
p2.setRating(p2elo > MIN_ELO? p2elo : MIN_ELO);
}
@Override
public void changeRatings(Stat ts1, Collection<Stat> teamstats, boolean tie) {
float result = tie ? 0.5f : 1.0f;
double eloWinner = 0;
double dampening = teamstats.size() == 1 ? 1 : teamstats.size() / 2.0D;
for (Stat ts : teamstats){
final double eloChange = eloChange(ts1,ts,result) / dampening;
eloWinner+= eloChange;
ts.setRating((int) (ts.getRating() - eloChange));
}
ts1.setRating((int) (ts1.getRating() + eloWinner));
}
protected int getK(float elo) {
if (elo < 1600){
return 50;
} else if (elo < 1800){
return 35;
} else if (elo < 2000){
return 20;
} else if (elo < 2500){
return 10;
}
return 6;
}
public void setEloSpread(float spread) {
this.spread = spread;
}
@Override
public void setDefaultRating(float elo) {
defaultElo = elo;
}
@Override
public float getDefaultRating() {
return defaultElo;
}
}
@@ -0,0 +1,41 @@
package mc.alk.tracker.ranking;
import mc.alk.tracker.objects.Stat;
import java.util.Collection;
public interface RatingCalculator {
/**
* What is this rating called
* @return name of rating (like elo)
*/
public String getName();
/**
* Set the default rating
* @param initialRating
*/
public void setDefaultRating(float initialRating);
/**
* Get the default rating
* @return
*/
public float getDefaultRating();
/**
* Change the ratings of stat1 and stat2
* @param stat1
* @param stat2
* @param tie
*/
public void changeRatings(Stat stat1, Stat stat2, boolean tie);
/**
* Change the ratings of a group of stats
* @param stat1
* @param stats
* @param tie
*/
public void changeRatings(Stat stat1, Collection<Stat> stats, boolean tie);
}
@@ -0,0 +1,62 @@
package mc.alk.tracker.serializers;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Formatter;
import java.util.GregorianCalendar;
import java.util.Vector;
import mc.alk.tracker.Tracker;
public class FileLogger {
static final String version ="1.0.1";
static Vector<String> msgs = new Vector<String>();
public FileLogger() {}
public static synchronized int log(String msg) {
try {
Calendar cal = new GregorianCalendar();
SimpleDateFormat sdf = new SimpleDateFormat("MM/dd,hh:mm:ss");
msgs.add(sdf.format(cal.getTime()).toString() + ","+msg+"\n");
} catch(Exception e){
e.printStackTrace();
}
return -1;
}
public static synchronized int log(String node, Object... varArgs) {
try {
Calendar cal = new GregorianCalendar();
SimpleDateFormat sdf = new SimpleDateFormat("MM/dd,hh:mm:ss");
StringBuilder buf = new StringBuilder();
Formatter form = new Formatter(buf);
form.format(node, varArgs);
form.close();
msgs.add(sdf.format(cal.getTime()).toString() + "," + buf.toString() +"\n");
return msgs.size();
} catch(Exception e){
e.printStackTrace();
}
return -1;
}
public static synchronized void saveAll() {
try {
FileWriter fstream = new FileWriter(new File(Tracker.getSelf().getDataFolder()+"/test.log"),true);
BufferedWriter out = new BufferedWriter(fstream);
for (String msg : msgs){
out.write(msg);
}
msgs.clear();
out.close();
} catch (Exception e){
e.printStackTrace();
}
}
}
@@ -0,0 +1,604 @@
package mc.alk.tracker.serializers;
import mc.alk.tracker.objects.PlayerStat;
import mc.alk.tracker.objects.Stat;
import mc.alk.tracker.objects.StatType;
import mc.alk.tracker.objects.TeamStat;
import mc.alk.tracker.objects.VersusRecords;
import mc.alk.tracker.objects.VersusRecords.VersusRecord;
import mc.alk.tracker.objects.WLT;
import mc.alk.tracker.objects.WLTRecord;
import mc.alk.v1r7.serializers.SQLSerializer;
import mc.alk.v1r7.util.Log;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
public class SQLInstance extends SQLSerializer{
public static final int TEAM_ID_LENGTH = 32;
public static final int TEAM_NAME_LENGTH = 48;
static public String URL = "localhost";
static public String PORT = "3306";
static public String USERNAME = "root";
static public String PASSWORD = "";
public String DB = "BattleTracker";
public static final String TABLE_PREFIX = "bt_";
public final String VERSUS_TABLE_SUFFIX = "_versus";
public final String OVERALL_TABLE_SUFFIX = "_overall";
public final String INDIVIDUAL_TABLE_SUFFIX = "_tally";
public String OVERALL_TABLE, VERSUS_TABLE, INDIVIDUAL_TABLE;
public static final String MEMBER_TABLE = TABLE_PREFIX+"members";
static final public String NAME = "Name";
static final public String TEAMID = "ID";
static final public String ID1 = "ID1";
static final public String ID2 = "ID2";
static final public String WINS= "Wins";
static final public String LOSSES = "Losses";
// static final public String KILLS= "Kills";
// static final public String DEATHS = "Deaths";
static final public String WLTIE = "WLTIE";
static final public String TIES = "Ties";
static final public String STREAK = "Streak";
static final public String MAXSTREAK = "maxStreak";
static final public String ELO = "Elo";
static final public String MAXELO = "maxElo";
static final public String COUNT = "Count";
static final public String DATE = "Date";
static final public String RANK = "Rank";
static final public String RANK_TYPE = "RankType";
static final public String TX_KILLS = "TK";
static final public String TX_STREAK = "Streak";
static final public String TX_KD = "KD";
static final public String TX_ELO = "ELO";
static final public String VALUE = "Value";
static final public String MEMBERS = "Members";
static final public String FLAGS = "Flags";
String drop_tables;
String create_individual_table, create_versus_table, create_member_table,create_overall_table;
String create_individual_table_idx, create_versus_table_idx, create_member_table_idx,create_overall_table_idx;
String get_overall_totals, insert_overall_totals;
String get_topx_wins, get_topx_losses, get_topx_ties;
String get_topx_streak,get_topx_maxstreak;
String get_topx_kd, get_topx_elo, get_topx_maxelo;
String get_topx_wins_tc, get_topx_losses_tc, get_topx_ties_tc;
String get_topx_streak_tc,get_topx_maxstreak_tc;
String get_topx_kd_tc, get_topx_elo_tc, get_topx_maxelo_tc;
String save_ind_record, get_ind_record;
String insert_versus_record, get_versus_record;
String get_versus_records, getx_versus_records, get_wins_since;
String truncate_all_tables;
String get_rank;
public static final String get_members = "select "+NAME+" from " + MEMBER_TABLE + " where " + TEAMID +" = ?";
String save_members;
String tableName;
public SQLInstance(){}
public void setTable(String tableName) {
this.tableName = tableName;
}
public String getTable(){
return tableName;
}
@Override
public boolean init(){
super.init();
VERSUS_TABLE = TABLE_PREFIX+tableName+VERSUS_TABLE_SUFFIX;
OVERALL_TABLE = TABLE_PREFIX+tableName+OVERALL_TABLE_SUFFIX;
INDIVIDUAL_TABLE = TABLE_PREFIX+tableName+INDIVIDUAL_TABLE_SUFFIX;
create_overall_table = "CREATE TABLE IF NOT EXISTS " + OVERALL_TABLE +" ("+
TEAMID + " VARCHAR(" + TEAM_ID_LENGTH +") NOT NULL ,"+
NAME + " VARCHAR(" + TEAM_NAME_LENGTH +") ,"+
WINS + " INTEGER UNSIGNED ," +
LOSSES + " INTEGER UNSIGNED," +
// KILLS + " INTEGER UNSIGNED ," +
// DEATHS + " INTEGER UNSIGNED," +
TIES + " INTEGER UNSIGNED," +
STREAK + " INTEGER UNSIGNED," +
MAXSTREAK + " INTEGER UNSIGNED," +
ELO + " INTEGER UNSIGNED DEFAULT " + 1250+"," +
MAXELO + " INTEGER UNSIGNED DEFAULT " + 1250+"," +
COUNT + " INTEGER UNSIGNED DEFAULT 1," +
FLAGS + " INTEGER UNSIGNED DEFAULT 0," +
"PRIMARY KEY (" + TEAMID +")) ";
create_versus_table = "CREATE TABLE IF NOT EXISTS " + VERSUS_TABLE +" ("+
ID1 + " VARCHAR(" + TEAM_ID_LENGTH +") NOT NULL ,"+
ID2 + " VARCHAR(" + TEAM_ID_LENGTH +") NOT NULL ,"+
WINS + " INTEGER UNSIGNED ," +
LOSSES + " INTEGER UNSIGNED," +
TIES + " INTEGER UNSIGNED," +
"PRIMARY KEY ("+ID1 +", "+ID2+"))";
create_member_table = "CREATE TABLE IF NOT EXISTS " + MEMBER_TABLE +" ("+
TEAMID + " VARCHAR(" + TEAM_ID_LENGTH +") NOT NULL ,"+
NAME + " VARCHAR(" + MAX_NAME_LENGTH +") NOT NULL ," +
"PRIMARY KEY (" + TEAMID +","+NAME+"))";
get_topx_wins = "select * from "+OVERALL_TABLE +" WHERE "+FLAGS+" & 1 <> 1 ORDER BY "+WINS+" DESC LIMIT ? ";
get_topx_losses = "select * from "+OVERALL_TABLE +" WHERE "+FLAGS+" & 1 <> 1 ORDER BY "+LOSSES+" DESC LIMIT ? ";
get_topx_ties = "select * from "+OVERALL_TABLE +" WHERE "+FLAGS+" & 1 <> 1 ORDER BY "+TIES+" DESC LIMIT ? ";
get_topx_streak = "select * from "+OVERALL_TABLE +" WHERE "+FLAGS+" & 1 <> 1 ORDER BY "+STREAK +" DESC LIMIT ? ";
get_topx_maxstreak = "select * from "+OVERALL_TABLE +" WHERE "+FLAGS+" & 1 <> 1 ORDER BY "+MAXSTREAK +" DESC LIMIT ? ";
get_topx_elo = "select * from "+OVERALL_TABLE +" WHERE "+FLAGS+" & 1 <> 1 ORDER BY "+ELO+" DESC LIMIT ? ";
get_topx_maxelo = "select * from "+OVERALL_TABLE +" WHERE "+FLAGS+" & 1 <> 1 ORDER BY "+MAXELO+" DESC LIMIT ? ";
get_topx_kd = "select *,(" + WINS + "/" + LOSSES+") as KD from "+OVERALL_TABLE +" WHERE "+FLAGS+" & 1 <> 1 ORDER BY KD DESC LIMIT ? ";
get_topx_wins_tc = "select * from "+OVERALL_TABLE +" WHERE "+COUNT+"=? AND "+FLAGS+" & 1 <> 1 ORDER BY "+WINS+" DESC LIMIT ? ";
get_topx_losses_tc = "select * from "+OVERALL_TABLE +" WHERE "+COUNT+"=? AND "+FLAGS+" & 1 <> 1 ORDER BY "+LOSSES+" DESC LIMIT ? ";
get_topx_ties_tc = "select * from "+OVERALL_TABLE +" WHERE "+COUNT+"=? AND "+FLAGS+" & 1 <> 1 ORDER BY "+TIES+" DESC LIMIT ? ";
get_topx_streak_tc = "select * from "+OVERALL_TABLE +" WHERE "+COUNT+"=? AND "+FLAGS+" & 1 <> 1 ORDER BY "+STREAK +" DESC LIMIT ? ";
get_topx_maxstreak_tc = "select * from "+OVERALL_TABLE +" WHERE "+COUNT+"=? AND "+FLAGS+" & 1 <> 1 ORDER BY "+MAXSTREAK +" DESC LIMIT ? ";
get_topx_elo_tc = "select * from "+OVERALL_TABLE +" WHERE "+COUNT+"=? AND "+FLAGS+" & 1 <> 1 ORDER BY "+ELO+" DESC LIMIT ? ";
get_topx_maxelo_tc = "select * from "+OVERALL_TABLE +" WHERE "+COUNT+"=? AND "+FLAGS+" & 1 <> 1 ORDER BY "+MAXELO+" DESC LIMIT ? ";
get_topx_kd_tc = "select *,(" + WINS + "/" + LOSSES+") as KD from "+OVERALL_TABLE +" WHERE "+COUNT+"=? AND "+FLAGS+" & 1 <> 1 ORDER BY KD DESC LIMIT ? ";
get_overall_totals = "select * from " + OVERALL_TABLE + " where " + TEAMID +" = ?";
get_versus_record = "select "+WINS+","+LOSSES+","+TIES+" from "+VERSUS_TABLE+" WHERE "+ID1+"=? AND "+ID2+"=?";
getx_versus_records = "select * from "+INDIVIDUAL_TABLE+" WHERE ("+ID1+"=? AND "+ID2+"=?) OR ("+ID1+"=? AND "+ID2+"=?) ORDER BY "+DATE+" DESC LIMIT ?";
get_rank = "select count(*) from "+OVERALL_TABLE+" where "+ELO+" > ? and "+COUNT+"=?";
get_wins_since = "select * from "+INDIVIDUAL_TABLE+" WHERE ("+ID1+"=? AND WLTIE=1) OR ("+ID2+"=? AND WLTIE=0) AND "+DATE+" >= ? ORDER BY "+DATE+" DESC ";
switch(TYPE){
case MYSQL:
create_individual_table = "CREATE TABLE IF NOT EXISTS " + INDIVIDUAL_TABLE +" ("+
ID1 + " VARCHAR(" + TEAM_ID_LENGTH +") NOT NULL ,"+
ID2 + " VARCHAR(" + TEAM_ID_LENGTH +") NOT NULL ,"+
DATE + " DATETIME," +
WLTIE + " INTEGER UNSIGNED," +
"PRIMARY KEY (" + ID1 +", " + ID2 + "," + DATE + "), "+
"INDEX USING HASH (" + ID1 +"),INDEX USING BTREE (" + DATE +")) ";
create_member_table_idx = "CREATE INDEX "+MEMBER_TABLE+"_idx ON " +MEMBER_TABLE+" ("+TEAMID+") USING HASH";
create_versus_table_idx = "CREATE INDEX "+VERSUS_TABLE+"_idx ON " +VERSUS_TABLE+" ("+ID1+") USING HASH";
insert_overall_totals = "INSERT INTO "+OVERALL_TABLE+" VALUES (?,?,?,?,?,?,?,?,?,?,?) " +
"ON DUPLICATE KEY UPDATE " +
WINS + " = VALUES(" + WINS +"), " + LOSSES +"=VALUES(" + LOSSES + "), " + TIES +"=VALUES(" + TIES + "), " +
STREAK +"= VALUES(" + STREAK+")," +MAXSTREAK +"= VALUES(" + MAXSTREAK+")," +
ELO +"= VALUES(" + ELO + ")," + MAXELO +"= VALUES(" + MAXELO+"),"+
FLAGS+"=VALUES("+FLAGS+")";
insert_versus_record = "insert into "+VERSUS_TABLE+" VALUES(?,?,?,?,?) " +
"ON DUPLICATE KEY UPDATE " +
WINS + " = VALUES(" + WINS +"), " + LOSSES +"=VALUES(" + LOSSES + "), " + TIES +"=VALUES(" + TIES + ")";
save_ind_record = "insert ignore into "+INDIVIDUAL_TABLE+" VALUES(?,?,?,?)";
save_members = "insert ignore into " + MEMBER_TABLE + " VALUES(?,?) ";
truncate_all_tables = "truncate table " +OVERALL_TABLE+"; truncate table " + VERSUS_TABLE+"; truncate table "+INDIVIDUAL_TABLE;
break;
case SQLITE:
create_individual_table = "CREATE TABLE IF NOT EXISTS " + INDIVIDUAL_TABLE +" ("+
ID1 + " VARCHAR(" + TEAM_ID_LENGTH +") NOT NULL ,"+
ID2 + " VARCHAR(" + TEAM_ID_LENGTH +") NOT NULL ,"+
DATE + " DATETIME," +
WLTIE + " INTEGER UNSIGNED," +
"PRIMARY KEY (" + ID1 +", " + ID2 + "," + DATE + ")) ";
create_member_table_idx = "CREATE UNIQUE INDEX IF NOT EXISTS "+MEMBER_TABLE+"_idx ON " +MEMBER_TABLE+" ("+TEAMID+")";
create_versus_table_idx = "CREATE UNIQUE INDEX IF NOT EXISTS "+VERSUS_TABLE+"_idx ON " +VERSUS_TABLE+" ("+ID1+")";
insert_versus_record = "insert or replace into "+VERSUS_TABLE+" VALUES(?,?,?,?,?)";
save_ind_record = "insert or ignore into "+INDIVIDUAL_TABLE+" VALUES(?,?,?,?)";
insert_overall_totals = "INSERT OR REPLACE INTO "+OVERALL_TABLE+" VALUES (?,?,?,?,?,?,?,?,?,?,?) ";
// insert_overall_totals = "INSERT OR REPLACE INTO "+OVERALL_TABLE+" VALUES (?," +
// "(select "+NAME+" from "+OVERALL_TABLE+" where "+TEAMID+"=?),"+
// "?,?,? ,?,? ,?,?,"+
// "(select "+COUNT+" from "+OVERALL_TABLE+" where "+TEAMID+"=?))";
save_members = "insert or ignore into " + MEMBER_TABLE + " VALUES(?,?) ";
truncate_all_tables = "drop table " +OVERALL_TABLE+"; drop table " + VERSUS_TABLE+"; drop table "+INDIVIDUAL_TABLE;
}
if (shouldUpdateTo1point0()){
updateTo1Point0();}
// if (shouldUpdateTo1point1()){
// updateTo1Point1();}
try {
createTable(VERSUS_TABLE, create_versus_table, create_versus_table_idx);
createTable(OVERALL_TABLE, create_overall_table );
createTable(INDIVIDUAL_TABLE,create_individual_table,create_individual_table_idx);
createTable(MEMBER_TABLE,create_member_table,create_member_table_idx);
} catch (Exception e){
e.printStackTrace();
return false;
}
return true;
}
public List<Stat> getTopX(StatType statType, int x, Integer teamcount) {
if (x <= 0){
x = Integer.MAX_VALUE;}
RSCon rscon = null;
if (teamcount == null){
switch(statType){
case WINS: case KILLS: rscon = executeQuery(get_topx_wins,x);break;
case LOSSES: case DEATHS: rscon = executeQuery(get_topx_losses,x); break;
case TIES: rscon = executeQuery(get_topx_ties,x); break;
case RATING: case RANKING: rscon = executeQuery(get_topx_elo,x); break;
case MAXRATING: case MAXRANKING: rscon = executeQuery(get_topx_maxelo,x); break;
case STREAK: rscon = executeQuery(get_topx_streak,x); break;
case MAXSTREAK: rscon = executeQuery(get_topx_maxstreak,x); break;
case WLRATIO: case KDRATIO: rscon = executeQuery(get_topx_kd,x); break;
default:
}
} else {
switch(statType){
case WINS: case KILLS: rscon = executeQuery(get_topx_wins_tc,teamcount,x);break;
case LOSSES: case DEATHS: rscon = executeQuery(get_topx_losses_tc,teamcount,x); break;
case TIES: rscon = executeQuery(get_topx_ties_tc,teamcount,x); break;
case RATING: case RANKING: rscon = executeQuery(get_topx_elo_tc,teamcount,x); break;
case MAXRATING: case MAXRANKING: rscon = executeQuery(get_topx_maxelo_tc,teamcount,x); break;
case STREAK: rscon = executeQuery(get_topx_streak_tc,teamcount,x); break;
case MAXSTREAK: rscon = executeQuery(get_topx_maxstreak_tc,teamcount,x); break;
case WLRATIO: case KDRATIO: rscon = executeQuery(get_topx_kd_tc,teamcount,x); break;
default:
}
}
return rscon == null ? null : createStatList(rscon);
}
public Integer getRanking(int rating, int teamSize) {
Integer rank = getInteger(get_rank, rating, teamSize);
return rank != null ? rank + 1: null;
}
private List<Stat> createStatList(RSCon rscon){
List<Stat> stats = new ArrayList<Stat>();
if (rscon == null)
return stats;
try {
ResultSet rs = rscon.rs;
while (rs.next()){
Stat s = createStat(rscon);
if (s != null)
stats.add(s);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeConnection(rscon);
}
return stats;
}
public Stat getRecord(String key) {
RSCon rscon = executeQuery(get_overall_totals, key);
try {
ResultSet rs = rscon.rs;
// System.out.println("rscon & rs " + rscon +" " + rs);
while (rs.next()){
return createStat(rscon);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
closeConnection(rscon);
}
return null;
}
private Stat createStat(RSCon rscon) throws SQLException{
ResultSet rs = rscon.rs;
Stat ts = null;
String id = rs.getString(TEAMID);
String name= rs.getString(NAME);
int kills = rs.getInt(WINS);
int deaths = rs.getInt(LOSSES);
int streak = rs.getInt(STREAK);
int maxStreak = rs.getInt(MAXSTREAK);
int ties = rs.getInt(TIES);
int elo = rs.getInt(ELO);
int maxElo = rs.getInt(MAXELO);
int count = rs.getInt(COUNT);
int flags = rs.getInt(FLAGS);
if (DEBUG) System.out.println("name =" + name + " id=" + id +" ranking=" + elo +" count="+count);
if (count == 1){
ts = new PlayerStat(id);
} else {
ts = new TeamStat(id,true);
ts.setName(name);
}
Integer nid= null;
try {nid = Integer.valueOf(id);} catch (NumberFormatException nfe){}
if (nid != null && ts instanceof TeamStat){
HashSet<String> players = new HashSet<String>();
RSCon rscon2 = null;
try{
rscon2 = executeQuery(rscon.con, true, TIMEOUT, get_members, id);
ResultSet rs2 = rscon2.rs;
while (rs2.next()){
players.add(rs2.getString(NAME));
}
((TeamStat)ts).setMembers(players);
} finally{
closeConnection(rscon2);
}
} else {
}
ts.setWins(kills);
ts.setLosses(deaths);
ts.setStreak(streak);
ts.setTies(ties);
ts.setRating(elo);
ts.setCount(count);
ts.setMaxStreak(maxStreak);
ts.setMaxRating(maxElo);
ts.setFlags(flags);
if (DEBUG) System.out.println("stat = " + ts);
return ts;
}
public void save(Stat stat) {
saveAll(stat);
}
public void saveAll(Stat... stats) {
saveTotals(stats);
/// Now Save members
for (Stat stat: stats){
try{
/// We only need to save the members if they exceed the id length, which turns the id into a hash
Integer nid = null;
try {nid = Integer.valueOf(stat.getStrID());} catch (NumberFormatException e){}
/// Save members
List<String> members = stat.getMembers();
if (nid != null && members != null && members.size() > 1)
saveMembers(stat.getStrID(), members);
VersusRecords rs = stat.getRecordSet();
if (DEBUG) System.out.println("SaveVersusRecords " + rs);
if (rs != null){
rs.flushOverallRecords();
// saveOverallRecords(rs.getOverallRecords());
// rs.setOverallRecords(null);
if (saveIndividualRecords(stat.getStrID(), rs.getIndividualRecords())){
rs.setIndividualRecords(null);/// lets keep the memory small where we can
}
}
} catch(Exception e){
e.printStackTrace();
}
}
}
public boolean saveIndividualRecords(String id, HashMap<String, List<WLTRecord>> indRecords) {
if (indRecords == null || indRecords.isEmpty())
return true;
if (DEBUG) System.out.println("SaveIndividual " + id);
List<List<Object>> batch = new ArrayList<List<Object>>();
for (String oid : indRecords.keySet()){
HashSet<Timestamp> times = new HashSet<Timestamp>();
for (WLTRecord wlt: indRecords.get(oid)){
switch(wlt.wlt){
case LOSS: /// do nothing, let the winner do the saving
continue;
case TIE: /// whoevers name is less stores the data
if (id.compareTo(oid) > 0)
continue;
default:
break;
}
Timestamp ts = new Timestamp((wlt.date /1000)*1000);
while (times.contains(ts)){ /// Since mysql can only handle seconds, increment to first free second
ts.setTime(ts.getTime()+1000);
}
times.add(ts);
batch.add(Arrays.asList(new Object[]{id,oid,ts, wlt.wlt.ordinal()}));
}
}
try {
executeBatch(save_ind_record, batch);
} catch (Exception e){
return false;
}
return true;
}
private void saveTotals(Stat... stats){
if (stats == null || stats.length==0)
return;
List<List<Object>> batch = new ArrayList<List<Object>>();
if (DEBUG) System.out.println("saveTotals ");
for (Stat stat: stats){
/// The "name" is just a comma delimited list of ids in the simple case, we can reconstruct it from the members
String name= stat.getName();
if (name!= null && name.length() > TEAM_NAME_LENGTH){
name = null;}
if (stat.getRating() < 0 || stat.getRating() > 200000){
Log.err("ELO OUT OF RANGE " + stat.getRating() +" stat=" + stat);
}
batch.add(Arrays.asList(new Object[]{stat.getStrID(),name, stat.getWins(), stat.getLosses(),stat.getTies(),
stat.getStreak(),stat.getMaxStreak(), stat.getRating(),
stat.getMaxRating(), stat.getCount(), stat.getFlags()}));
}
try{
executeBatch(insert_overall_totals, batch);
} catch (Exception e){
System.err.println("ERROR SAVING TOTALS");
e.printStackTrace();
for (Stat stat: stats){
Log.err(" Possible failed stat = " + stat);
}
}
}
public void saveMembers(String strid, List<String> players) {
if (players == null)
return ;
if (DEBUG) System.out.println("SaveMember " + strid +" players=" + players);
List<List<Object>> batch = new ArrayList<List<Object>>();
for (String player: players){
batch.add(Arrays.asList(new Object[]{strid,player}));
}
executeBatch(save_members,batch);
}
public VersusRecord getVersusRecord(String id, String opponentId) {
VersusRecord or = null;
List<Object> objs = getObjects(get_versus_record, id, opponentId);
if (objs != null && !objs.isEmpty()){
or = new VersusRecord(id,opponentId);
or.wins = Integer.valueOf(objs.get(0).toString());
or.losses = Integer.valueOf(objs.get(1).toString());
or.ties = Integer.valueOf(objs.get(2).toString());
}
return or;
}
private WLTRecord parseWLTRecord(ResultSet rs) {
try{
Timestamp ts = rs.getTimestamp(DATE);
WLTRecord wlt = new WLTRecord(WLT.valueOf(rs.getInt(WLTIE)), ts.getTime());
return wlt;
} catch (Exception e){
e.printStackTrace();
return null;
}
}
public List<WLTRecord> getVersusRecords(String id, String opponentId, int x) {
if (x <= 0){
x = Integer.MAX_VALUE;}
RSCon rscon = executeQuery(getx_versus_records,id,opponentId,opponentId, id, x);
List<WLTRecord> list = new ArrayList<WLTRecord>();
if (rscon != null){
try {
ResultSet rs = rscon.rs;
String winner;
while (rs.next()){
winner = rs.getString(ID1);
WLTRecord wlt = parseWLTRecord(rs);
if (wlt == null)
continue;
if (winner.equalsIgnoreCase(opponentId)){
wlt.reverse();
}
list.add(wlt);
}
} catch (SQLException e) {
e.printStackTrace();
} finally{
closeConnection(rscon);
}
}
return list;
}
public List<WLTRecord> getWinsSince(String id, Long time) {
RSCon rscon = null;
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(time);
String datestr = dateFormat.format(date);
rscon = executeQuery(get_wins_since,id,id, datestr);
List<WLTRecord> list = new ArrayList<WLTRecord>();
if (rscon != null){
try {
ResultSet rs = rscon.rs;
while (rs.next()){
WLTRecord wlt = parseWLTRecord(rs);
if (wlt == null)
continue;
list.add(wlt);
}
} catch (SQLException e) {
e.printStackTrace();
} finally{
closeConnection(rscon);
}
}
return list;
}
public void realsaveVersusRecords(Collection<VersusRecord> types) {
if (types==null)
return;
if (DEBUG) System.out.println("saveOverallRecords types=" + types +" size=" +(types != null ? types.size():0));
List<List<Object>> batch = new ArrayList<List<Object>>();
for (VersusRecord or: types){
/// Whichever id is less stores the information to avoid redundancy
/// Alkarin vs Yodeler. Alkarin stores the info
if (or.ids.get(0).compareTo(or.ids.get(1)) > 0)
continue;
batch.add(Arrays.asList(new Object[]{or.ids.get(0),or.ids.get(1),or.wins,or.losses,or.ties}));
}
executeBatch(insert_versus_record,batch);
}
public void deleteTables(){
switch (this.getType()){
case MYSQL:
this.executeUpdate("truncate table " +OVERALL_TABLE);
this.executeUpdate("truncate table " +VERSUS_TABLE);
this.executeUpdate("truncate table " +INDIVIDUAL_TABLE);
break;
case SQLITE:
this.executeUpdate(truncate_all_tables);
/// For SQLite, need to drop the tables and recreate them
init();
break;
default:
break;
}
}
public int getRecordCount() {
return getInteger("select count(*) from " + INDIVIDUAL_TABLE);
}
public boolean shouldUpdateTo1point0() {
return hasTable(OVERALL_TABLE) && !hasColumn(OVERALL_TABLE,FLAGS);
}
// public boolean shouldUpdateTo1point1() {
// return !hasColumn(OVERALL_TABLE,KILLS);
// }
private void updateTo1Point0() {
Log.warn("[BattleTracker] updating database to 1.0");
String alter = "ALTER TABLE "+OVERALL_TABLE+" ADD "+FLAGS+" INTEGER DEFAULT 0 ";
executeUpdate(alter);
}
//
// private void updateTo1Point1() {
// Log.warn("[BattleTracker] updating database to 1.1");
// String alter = "ALTER TABLE "+OVERALL_TABLE+" ADD "+KILLS+" INTEGER DEFAULT 0 ";
// executeUpdate(alter);
// alter = "ALTER TABLE "+OVERALL_TABLE+" ADD "+DEATHS+" INTEGER DEFAULT 0 ";
// executeUpdate(alter);
// }
}
@@ -0,0 +1,72 @@
package mc.alk.tracker.serializers;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import mc.alk.tracker.controllers.SignController;
import mc.alk.tracker.objects.StatSign;
import mc.alk.v1r7.serializers.BaseConfig;
import mc.alk.v1r7.util.SerializerUtil;
import org.bukkit.Location;
import org.bukkit.Material;
public class SignSerializer extends BaseConfig{
SignController sc;
public SignSerializer(SignController sc) {
this.sc = sc;
}
public void saveAll() {
Map<String,StatSign> map = sc.getTopSigns();
if (map != null){
List<StatSign> l = new ArrayList<StatSign>(map.values());
config.set("topSigns", l);
}
map = sc.getPersonalSigns();
if (map != null){
List<StatSign> l = new ArrayList<StatSign>(map.values());
config.set("personalSigns", l);
}
save();
}
public void loadAll(){
String[] types = new String[]{"topSigns","personalSigns"};
sc.clearSigns();
for (String type: types){
List<?> signs = config.getList(type);
if (signs == null)
continue;
for (Object o : signs){
if (o == null || !(o instanceof StatSign))
continue;
if (!stillSign((StatSign)o))
continue;
sc.addSign((StatSign) o);
}
}
}
public static boolean stillSign(StatSign o) {
String l = o.getLocationString();
if (l == null)
return false;
try{
Location loc = SerializerUtil.getLocation(l);
if (loc == null)
return false;
Material mat = loc.getWorld().getBlockAt(loc).getType();
if (!mat.name().contains("SIGN"))
return false;
} catch( Exception e){
return false;
}
return true;
}
}
@@ -0,0 +1,100 @@
package mc.alk.tracker.serializers;
import mc.alk.battlepluginupdater.FileUpdater;
import mc.euro.version.Version;
import mc.alk.tracker.Tracker;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
public class YamlConfigUpdater {
public void update(YamlConfiguration config, File configFile) {
File backupDir = new File(Tracker.getSelf().getDataFolder()+"/backups");
if (!backupDir.exists()) backupDir.mkdir();
Version curVersion = new Version(config.getString("version", "0"));
Version newVersion = new Version("1.0");
try{
if (curVersion.compareTo(newVersion) < 0){
curVersion = updateTo1Point0(configFile,backupDir, curVersion, newVersion);}
newVersion = new Version("1.0.1");
if (curVersion.compareTo(newVersion) < 0){
curVersion = updateTo1Point01(configFile,backupDir, curVersion, newVersion);}
newVersion = new Version("1.0.2");
if (curVersion.compareTo(newVersion) < 0){
curVersion = updateTo1Point02(configFile,backupDir, curVersion, newVersion);}
newVersion = new Version("1.0.3");
if (curVersion.compareTo(newVersion) < 0){
curVersion = updateTo1Point03(configFile,backupDir, curVersion, newVersion);}
newVersion = new Version("1.0.4");
if (curVersion.compareTo(newVersion) < 0){
curVersion = updateTo1Point04(configFile,backupDir, curVersion, newVersion);}
} catch (Exception e){
e.printStackTrace();
}
}
private Version updateTo1Point0(File oldFile, File backupDir, Version newVersion, Version oldVersion) throws IOException {
FileUpdater fu = new FileUpdater(oldFile,backupDir,newVersion,oldVersion);
fu.addBefore(".*showBukkitPVPMessages.*", "version: 1.0");
fu.addAfter(".*trackPvE:.*", "",
"### The default messages when top command is used",
"topHeaderMsg: '&4Top &6{interfaceName}&4 {stat} TeamSize:{teamSize}'",
"topBodyMsg: '&e#{rank}&4 {name} - {wins}:{losses}&6[{rating}]'",
"",
"### ignore the following entities when they kill or are killed",
"ignoreEntities: []");
return fu.update();
}
private Version updateTo1Point01(File oldFile, File backupDir, Version newVersion, Version oldVersion) throws IOException {
FileUpdater fu = new FileUpdater(oldFile,backupDir,newVersion,oldVersion);
fu.replace(".*version:.*", "version: 1.0.1");
fu.addAfter(".*sendPVEDeathMessages:.*", "",
"### If server wide PvP or Pve messages are disabled you can turn",
"### these to true to allow the killer and the dead person",
"### to still see the messages",
"sendInvolvedPvPMessages: false",
"sendInvolvedPvEMessages: false");
fu.addAfter(".*ignoreEntities:.*", "",
"### Should we even use leaderboard signs?",
"allowLeaderboardSigns: true");
return fu.update();
}
private Version updateTo1Point02(File oldFile, File backupDir, Version newVersion, Version oldVersion) throws IOException {
FileUpdater fu = new FileUpdater(oldFile,backupDir,newVersion,oldVersion);
fu.replace(".*version:.*", "version: 1.0.2");
fu.addAfter(".*ignoreEntities:.*", "",
"### ignore the following worlds",
"ignoreWorlds: []");
return fu.update();
}
private Version updateTo1Point03(File oldFile, File backupDir, Version newVersion, Version oldVersion) throws IOException {
FileUpdater fu = new FileUpdater(oldFile,backupDir,newVersion,oldVersion);
fu.replace(".*version:.*", "version: 1.0.3","");
fu.addBefore(".*showBukkitPVPMessages:.*", "",
"autoUpdate: true ## auto update to newer versions found on bukkit");
return fu.update();
}
private Version updateTo1Point04(File oldFile, File backupDir, Version newVersion, Version oldVersion) throws IOException {
FileUpdater fu = new FileUpdater(oldFile,backupDir,newVersion,oldVersion);
fu.replace(".*version:.*", "version: 1.0.4");
fu.replace("autoUpdate:.*",
"# Updates will be retrieved from the latest plugin on the bukkit site. Valid Options : none, release, beta, all",
"# none (don't auto update)",
"# release (only get release versions, ignore beta and alpha)",
"# beta (get release and beta versions, ignore alpha)",
"# all (get all new updates)",
"autoUpdate: release",
"",
"# show newer versions. Valid Options: none, console, ops",
"# none (don't show new versions)",
"# console (show only to console log on startup)",
"# ops (announce to ops on join, will only show this message once per server start)",
"announceUpdate: console");
return fu.update();
}
}
@@ -0,0 +1,60 @@
package mc.alk.tracker.serializers;
import mc.alk.battlepluginupdater.FileUpdater;
import mc.euro.version.Version;
import mc.alk.tracker.Tracker;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
public class YamlMessageUpdater {
public void update(YamlConfiguration config, File configFile) {
File backupDir = new File(Tracker.getSelf().getDataFolder()+"/backups");
if (!backupDir.exists()) backupDir.mkdir();
Version curVersion = new Version(config.getString("version", "0"));
Version newVersion = new Version("1.0");
try{
if (curVersion.compareTo(newVersion) < 0){
curVersion = updateTo1Point0(configFile,backupDir, newVersion, curVersion);}
} catch (Exception e){
e.printStackTrace();
return;
}
newVersion = new Version("1.0.1");
try{
if (curVersion.compareTo(newVersion) < 0){
curVersion = updateTo1Point01(configFile,backupDir, newVersion, curVersion);}
} catch (Exception e){
e.printStackTrace();
return;
}
}
private Version updateTo1Point0(File oldFile, File backupDir, Version newVersion, Version oldVersion) throws IOException {
FileUpdater fu = new FileUpdater(oldFile,backupDir,newVersion,oldVersion);
fu.addAfter(".*# &f White.*", "version: 1.0");
fu.addBefore(".*wolf:.*",
" snowman:",
" - '&8snowmen&f have fought back against &6%d&f'",
" ozelot:",
" - 'The cute ocelot showed its power to &6%d&f'",
" - '&6%d&f discovered that ocelots have fangs'",
" anvil:",
" - '&6%d&f was crushed by an anvil'",
" - '&6%d&f found to use anvils, not stand under them'");
return fu.update();
}
private Version updateTo1Point01(File oldFile, File backupDir, Version newVersion, Version oldVersion) throws IOException {
FileUpdater fu = new FileUpdater(oldFile,backupDir,newVersion,oldVersion);
fu.replace(".*version:.*", "version: "+newVersion.toString());
fu.addBefore(".*air:.*",
" bow:",
" - '&6%k&f feathered &6%d&f with arrows!'",
" - '&6%k&fs arrow pierced through &6%d&f!'");
return fu.update();
}
}
+350
View File
@@ -0,0 +1,350 @@
package mc.alk.tracker.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.mutable.MutableBoolean;
/**
* Author: alkarin
*
* A Cache class that allows quick access to stored objects
* This will also store if the value is NOT found.
* @param <Key>
* @param <Value>
*/
public class Cache <Key, Value> {
public static final String version = "1.2";
public static final boolean DEBUG = false;
/**
* ease of use cache to allows subclasses to define themselves dirty
* @param <K>
* @param <V>
*/
public static abstract class CacheObject<K,V> implements UniqueKey<K>{
protected Cache<K, V> cache;
public void setCache(Cache<K,V> cache){this.cache = cache;}
@SuppressWarnings("unchecked")
protected void setDirty(){if (cache != null) cache.setDirty(getKey());}
// protected abstract void setDirty();
}
/**
* All cache classes must be able to return a Unique Key, (not necessarily just an int ala hashCode() )
* @param <K>
*/
public interface UniqueKey<K> {
public K getKey();
}
/**
* Interface for the class that will Save and Load the objects in the cache
*
* @param <K> Key Object
* @param <T> Value Object
*/
public interface CacheSerializer<K,T>{
public T load(K key, MutableBoolean dirty, Object... varArgs);
public void save(List<T> types);
}
/**
* A cache element along with when it was used
*/
public class CacheElement{
public UniqueKey<Key> v;
public Long lastUsed;
public CacheElement(UniqueKey<Key> value){
this.v = value;
}
public void setUsed(){
this.lastUsed = System.currentTimeMillis();
}
}
CacheSerializer<Key,UniqueKey<Key>> serializer; /// Our serializer for the cache data
Map<Key,CacheElement> map = new HashMap<Key,CacheElement>(); /// a mapping of the key to the cache objects
boolean modified = false;
Set<Key> dirty = new HashSet<Key>(); /// which keys have been modified
Boolean autoFlush = false;
Long autoFlushTime = null;
Long lastCheckedTime = System.currentTimeMillis();
/**
* Create a new cache with the object that will save/load
* @param serializer
*/
public Cache(CacheSerializer<Key,Value> serializer){
this.setSerializer(serializer);
}
/**
* Specify that this cache should try to save and remove old records every specified time
* This is a "loose" time, and will only be checked on a get or setDirty call
* @param time
*/
public void setSaveEvery(long time){
autoFlush = true;
autoFlushTime = time;
}
@SuppressWarnings("unchecked")
/**
* get a key. if varArgs is not null these values will be passed to the serializer in the case that the
* cache object does not exist for loading.
* @param key cache key
* @param varArgs arguments that will be passed to serializer when a key is not found
* @return
*/
public Value get(Key key, Object... varArgs) {
if (DEBUG) System.out.println(" - getting key = " + key + " contains=" + map.containsKey(key));
CacheElement o=null;
if (map.containsKey(key)){
o = map.get(key);
} else{
MutableBoolean isdirty = new MutableBoolean(false);
UniqueKey<Key> t = serializer.load(key, isdirty, varArgs);
if (DEBUG) System.out.println(" - loaded element = " + t + " ");
o = new CacheElement(t);
synchronized(map){
map.put(key, o);
}
if (DEBUG) System.out.println(" - adding key = " + key + " contains=" + map.containsKey(key) +" dirty="+isdirty);
if (isdirty.booleanValue()){ /// If its dirty, add to our dirty set
synchronized(dirty){
dirty.add(key);
}
}
}
o.setUsed();
if (autoFlush && autoFlushTime != null){
flushOld(autoFlushTime);}
return (Value) o.v;
}
public boolean contains(UniqueKey<Key> obj){
return map.containsKey(obj.getKey());
}
public UniqueKey<Key> put(UniqueKey<Key> obj) {
CacheElement o = new CacheElement(obj);
final Key key = obj.getKey();
synchronized(map){
map.put(key, o);
}
synchronized(dirty){
dirty.add(key);
}
if (DEBUG) System.out.println(" - adding key = " + key + " contains=" + map.containsKey(key) +" dirty="+dirty);
o.setUsed();
if (autoFlush && autoFlushTime != null){
flushOld(autoFlushTime);}
return obj;
}
/**
* get a cache object using the key from the given param
* @param type
* @return
*/
public Value get(UniqueKey<Key> obj){
return get(obj.getKey());
}
/**
* get a cache object using the key from the given param.
* @param type
* @param varArgs
* @return
*/
public Value get(UniqueKey<Key> type,Object... varArgs) {
return get(type.getKey(),varArgs);
}
/**
* remove a cache object using the type given
* @param type
* @return
*/
public Value remove(UniqueKey<Key> type){
return remove(type.getKey());
}
@SuppressWarnings("unchecked")
/**
* remove a cache object using the key from the given param
* @param key
* @return
*/
public Value remove(Key key){
if (DEBUG) System.out.println(" - remove key = " + key + " contains=" + map.containsKey(key));
CacheElement o = map.remove(key);
if (o != null){
if (dirty.contains(key)){
List<UniqueKey<Key>> types = new ArrayList<UniqueKey<Key>>(1);
types.add(o.v);
serializer.save(types);
}
return (Value) o.v;
} else {
dirty.remove(key);
}
return null;
}
public boolean isModified(){
return modified;
}
@SuppressWarnings("unchecked")
public Collection<Value> values() {
return (Collection<Value>) map.values();
}
/**
* Specify that a cache object is 'dirty' and needs to be saved to db
* @param key
*/
public void setDirty(Key... keys) {
modified = true;
synchronized(dirty){
for (Key key : keys){
if (DEBUG) System.out.println(" - setting dirty key = " + key + " v=" + map.get(key));
dirty.add(key);
}
}
if (autoFlush && autoFlushTime != null){
flushOld(autoFlushTime);}
}
/**
* Specify that a list of cache objects are 'dirty' and need to be saved to db
* @param types
*/
public void setDirty(UniqueKey<Key> ... types) {
synchronized(dirty){
for (UniqueKey<Key> t: types){
if (map.containsKey(t.getKey()))
dirty.add(t.getKey());
}
}
}
public void setClean(UniqueKey<Key> ... types) {
synchronized(dirty){
for (UniqueKey<Key> t: types){
dirty.remove(t.getKey());
}
}
}
/**
* Explicitly save
* @param element
*/
public void save(UniqueKey<Key> element) {
List<UniqueKey<Key>> types = new ArrayList<UniqueKey<Key>>(1);
types.add(element);
synchronized(dirty){
dirty.remove(element.getKey());
}
serializer.save(types);
}
/**
* Explicitly save
* @param element
*/
public void save(Key key) {
List<UniqueKey<Key>> types = new ArrayList<UniqueKey<Key>>(1);
if (map.containsKey(key)){
types.add(map.get(key).v);
}
synchronized(dirty){
dirty.remove(key);
}
serializer.save(types);
}
/**
* Save all dirty cache records
*/
public void save() {
List<UniqueKey<Key>> types = new ArrayList<UniqueKey<Key>>(dirty.size());
synchronized(dirty){ synchronized(map){
dirty.remove(null);
for (Key key: dirty){
if (DEBUG) System.out.println(" - saving key = " + key + " v=" + map.get(key));
CacheElement e = map.get(key);
if (e != null && e.v != null)
types.add(e.v);
}
dirty.clear();
modified = false;
}}
serializer.save(types);
}
@SuppressWarnings("unchecked")
/**
* Specify the class to be used to serialize the data
* @param cachable
*/
public void setSerializer(CacheSerializer<Key, Value> serializer){
this.serializer = (CacheSerializer<Key, UniqueKey<Key>>) serializer;
}
/**
* write out all dirty records. and empty the cache
*/
public void flush() {
save();
synchronized(dirty){ synchronized(map){
map.clear();
dirty.clear();
modified = false;
}}
}
/**
* write out all records ands clear any records older than the specified time (in milliseconds)
*/
public void flushOld(Long time) {
final long now = System.currentTimeMillis();
if (now - lastCheckedTime < time)
return;
lastCheckedTime = now;
save();
List<Key> old = new ArrayList<Key>();
synchronized(dirty){ synchronized(map){
for (CacheElement e: map.values()){
if (now - e.lastUsed > time && e.v != null){
if (DEBUG) System.out.println(" - flushing old cache element =" + e.v +" lastUsed=" + (now - e.lastUsed));
old.add(e.v.getKey());
}
}
dirty.clear();
modified = false;
for (Key k: old){
map.remove(k);
}
}}
}
/**
* Clear the cache and all records.
*/
public void clear(){
synchronized(dirty){ synchronized(map){
map.clear();
dirty.clear();
modified = false;
}}
}
}
@@ -0,0 +1,73 @@
package mc.alk.tracker.util;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
public class TimeUtil {
static final String version = "1.0";
static long lastCheck = 0;
public static String convertMillisToString(long t){
return convertSecondsToString(t/1000);
}
public static String convertSecondsToString(long t){
long s = t % 60;
t /= 60;
long m = t %60;
t /=60;
long h = t % 24;
t /=24;
long d = t;
boolean has = false;
StringBuilder sb = new StringBuilder();
if (d > 0) {
has=true;
sb.append("&6"+d + "&e " + dayOrDays(d) +" ");}
if (h > 0) {
has =true;
sb.append("&6"+h + "&e " + hourOrHours(h)+" ");}
if (m > 0) {
has=true;
sb.append("&6"+m + "&e " + minOrMins(m)+" ");}
if (s > 0) {
has = true;
sb.append("&6"+s + "&e " + secOrSecs(s)+" ");}
if (!has){
sb.append("&60 sec");
}
return sb.toString();
}
public static String convertToString(long t){
t = t / 1000;
return convertSecondsToString(t);
}
public static String dayOrDays(long t){
return t > 1 || t == 0? "days" : "day";
}
public static String hourOrHours(long t){
return t > 1 || t ==0 ? "hours" : "hour";
}
public static String minOrMins(long t){
return t > 1 || t == 0? "minutes" : "minute";
}
public static String secOrSecs(long t){
return t > 1 || t == 0? "sec" : "secs";
}
public static String convertLongToDate(long time) {
SimpleDateFormat sdf = new SimpleDateFormat("E hh:mm z");
sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
return sdf.format(time);
}
public static String PorP(int size) {
return size == 1 ? "person" : "people";
}
}
+10
View File
@@ -0,0 +1,10 @@
package mc.alk.tracker.util;
public class Util {
static public void printStackTrace(){
for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
System.out.println(ste);
}
}
}
@@ -1,515 +0,0 @@
package org.battleplugins.tracker;
import org.battleplugins.tracker.event.BattleTrackerPreInitializeEvent;
import org.battleplugins.tracker.feature.Feature;
import org.battleplugins.tracker.feature.combatlog.CombatLog;
import org.battleplugins.tracker.feature.damageindicators.DamageIndicators;
import org.battleplugins.tracker.feature.placeholderapi.PlaceholderApiFeature;
import org.battleplugins.tracker.message.Messages;
import org.battleplugins.tracker.sql.SqlInstance;
import org.battleplugins.tracker.stat.calculator.EloCalculator;
import org.battleplugins.tracker.stat.calculator.RatingCalculator;
import org.battleplugins.tracker.util.CommandInjector;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
* The main class for BattleTracker.
*/
public class BattleTracker extends JavaPlugin {
private static final int PLUGIN_ID = 4598;
private static BattleTracker instance;
final Map<String, Supplier<Tracker>> trackerSuppliers = new HashMap<>();
private final Map<String, TrackerLoader> trackerLoaders = new HashMap<>();
private final Map<String, Tracker> trackers = new ConcurrentHashMap<>();
private final Map<String, RatingCalculator> ratingCalculators = new HashMap<>();
private final Map<Tracker, List<Listener>> trackerListeners = new HashMap<>();
private PlaceholderApiFeature placeholderApiFeature;
private CombatLog combatLog;
private DamageIndicators damageIndicators;
private BattleTrackerConfig config;
private Path trackersPath;
private Path featuresPath;
private BukkitTask autoSaveTask;
private boolean debugMode;
private volatile boolean shuttingDown;
@Override
public void onLoad() {
instance = this;
this.loadConfig(false);
Path dataFolder = this.getDataFolder().toPath();
this.trackersPath = dataFolder.resolve("trackers");
this.featuresPath = dataFolder.resolve("features");
new BattleTrackerPreInitializeEvent(this).callEvent();
}
@Override
public void onEnable() {
this.shuttingDown = false;
Bukkit.getPluginManager().registerEvents(new BattleTrackerListener(this), this);
// Captures duel match membership before Duels tears the match down on death.
Bukkit.getPluginManager().registerEvents(new org.battleplugins.tracker.util.DuelsHook.DeathGuard(), this);
// Captures Spleef membership before Spleef processes an elimination.
Bukkit.getPluginManager().registerEvents(new org.battleplugins.tracker.util.SpleefHook.DeathGuard(), this);
// Register default calculators
this.registerCalculator(new EloCalculator(this.config.getRating().elo()));
this.enable();
// Loads all tracker loaders
this.loadTrackerLoaders(this.trackersPath);
this.startAutoSaveTask();
new Metrics(this, PLUGIN_ID);
}
private void enable() {
if (Files.notExists(this.trackersPath)) {
try {
Files.createDirectories(this.trackersPath);
} catch (IOException e) {
throw new RuntimeException("Error creating trackers directory!", e);
}
this.saveResource("trackers/pve.yml", false);
this.saveResource("trackers/pvp.yml", false);
}
if (Files.notExists(this.featuresPath)) {
try {
Files.createDirectories(this.featuresPath);
} catch (IOException e) {
throw new RuntimeException("Error creating features directory!", e);
}
this.saveResource("features/combat-log.yml", false);
this.saveResource("features/damage-indicators.yml", false);
}
Path dataFolder = this.getDataFolder().toPath();
if (Files.notExists(dataFolder.resolve("messages.yml"))) {
this.saveResource("messages.yml", false);
}
this.placeholderApiFeature = new PlaceholderApiFeature();
this.placeholderApiFeature.onEnable(this);
}
@Override
public void onDisable() {
this.shuttingDown = true;
try {
this.disable(true).join();
} catch (CompletionException e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
this.error("Error disabling BattleTracker!", cause);
} finally {
SqlInstance sqlInstance = SqlInstance.getInstance();
if (sqlInstance != null) {
sqlInstance.close();
}
}
}
private CompletableFuture<Void> disable(boolean block) {
this.shuttingDown = true;
this.cancelAutoSaveTask();
if (this.placeholderApiFeature != null) {
this.unloadFeature(this.placeholderApiFeature);
}
if (this.combatLog != null) {
this.unloadFeature(this.combatLog);
}
if (this.damageIndicators != null) {
this.unloadFeature(this.damageIndicators);
}
// Save all trackers
List<CompletableFuture<Void>> saveFutures = new ArrayList<>();
for (Tracker tracker : this.trackers.values()) {
List<Listener> listeners = this.trackerListeners.remove(tracker);
if (listeners != null && !listeners.isEmpty()) {
listeners.forEach(HandlerList::unregisterAll);
}
saveFutures.add(tracker.saveAll(!block).thenRun(() -> tracker.destroy(block)));
}
CompletableFuture<Void> future = CompletableFuture.allOf(saveFutures.toArray(CompletableFuture[]::new))
.whenComplete((aVoid, throwable) -> this.trackers.clear());
if (block) {
future.join();
}
return future;
}
void postInitialize() {
Messages.load(this.getDataFolder().toPath().resolve("messages.yml"));
this.loadTrackers();
this.combatLog = this.loadFeature(this.featuresPath.resolve("combat-log.yml"), CombatLog::load);
this.damageIndicators = this.loadFeature(this.featuresPath.resolve("damage-indicators.yml"), DamageIndicators::load);
}
/**
* Reloads the plugin.
*/
public void reload() {
this.disable(false).whenCompleteAsync((aVoid, e) -> {
if (e != null) {
this.error("Error disabling plugin!", e);
}
// Reload the config
this.loadConfig(true);
this.shuttingDown = false;
this.enable();
this.postInitialize();
this.startAutoSaveTask();
}, Bukkit.getScheduler().getMainThreadExecutor(this));
}
private void cancelAutoSaveTask() {
if (this.autoSaveTask == null) {
return;
}
this.autoSaveTask.cancel();
this.autoSaveTask = null;
}
private void startAutoSaveTask() {
this.cancelAutoSaveTask();
if (this.config.getAdvanced().saveInterval() == -1) {
return;
}
this.autoSaveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> {
if (this.shuttingDown) {
return;
}
this.debug("Auto save: Saving all trackers.");
for (Tracker tracker : this.trackers.values()) {
long startTIme = System.currentTimeMillis();
tracker.saveAll().whenComplete((aVoid, e) -> {
if (e != null) {
this.error("Error saving tracker {}!", tracker.getName(), e);
} else {
this.debug("Auto save: Saved tracker {} in {}ms.", tracker.getName(), System.currentTimeMillis() - startTIme);
}
tracker.flush(false);
this.debug("Auto save: Flushed tracker {}.", tracker.getName());
});
}
this.debug("Auto save: Finished saving all trackers.");
}, this.config.getAdvanced().saveInterval() * 20L, this.config.getAdvanced().saveInterval() * 20L);
}
<T extends Feature> T loadFeature(Path path, Function<ConfigurationSection, T> featureLoader) {
Configuration configuration = YamlConfiguration.loadConfiguration(path.toFile());
T feature = featureLoader.apply(configuration);
if (feature.enabled()) {
feature.onEnable(this);
}
return feature;
}
void unloadFeature(Feature feature) {
if (feature.enabled()) {
feature.onDisable(this);
}
}
/**
* Returns an in-memory representation of the configuration.
*
* @return the BattleTracker configuration
*/
public BattleTrackerConfig getMainConfig() {
return config;
}
/**
* Returns the rating calculator with the given name.
*
* @param name the name of the rating calculator
* @return the rating calculator with the given name
*/
@Nullable
public RatingCalculator getCalculator(String name) {
return this.ratingCalculators.get(name);
}
/**
* Registers a {@link RatingCalculator}.
*
* @param calculator the rating calculator to register
*/
public void registerCalculator(RatingCalculator calculator) {
this.ratingCalculators.put(calculator.getName(), calculator);
}
/**
* Registers a {@link Tracker}.
*
* @param tracker the tracker to register
*/
public void registerTracker(Tracker tracker) {
this.trackers.put(tracker.getName().toLowerCase(Locale.ROOT), tracker);
}
/**
* Unregisters a {@link Tracker}.
*
* @param tracker the tracker to unregister
*/
public void unregisterTracker(Tracker tracker) {
this.trackers.remove(tracker.getName());
}
/**
* Registers a {@link Listener} to a {@link Tracker}.
*
* @param tracker the tracker to register the listener to
* @param listener the listener to register
*/
public void registerListener(Tracker tracker, Listener listener) {
this.trackerListeners.computeIfAbsent(tracker, key -> new ArrayList<>()).add(listener);
Bukkit.getPluginManager().registerEvents(listener, this);
}
/**
* Registers a {@link Tracker} supplier.
*
* @param name the name of the tracker supplier to register
* @param trackerSupplier the tracker supplier to register
*/
public void registerTracker(String name, Supplier<Tracker> trackerSupplier) {
this.trackerSuppliers.put(name, trackerSupplier);
}
/**
* Returns the {@link Tracker} with the given name.
*
* @param name the name of the tracker
* @return the tracker with the given name
*/
public Optional<Tracker> tracker(String name) {
return Optional.ofNullable(this.getTracker(name));
}
/**
* Returns the {@link Tracker} with the given name,
* or null if the tracker does not exist.
*
* @param name the name of the tracker
* @return the tracker with the given name
*/
@Nullable
public Tracker getTracker(String name) {
return this.trackers.get(name.toLowerCase(Locale.ROOT));
}
/**
* Returns all the {@link Tracker}s associated with the plugin.
*
* @return all the trackers associated with the plugin
*/
public List<Tracker> getTrackers() {
return List.copyOf(this.trackers.values());
}
/**
* Returns the {@link CombatLog} feature.
*
* @return the CombatLog feature
*/
public CombatLog getCombatLog() {
return this.combatLog;
}
/**
* Returns whether the plugin is in debug mode.
*
* @return whether the plugin is in debug mode
*/
public boolean isDebugMode() {
return this.debugMode;
}
/**
* Sets whether the plugin is in debug mode.
*
* @param debugMode whether the plugin is in debug mode
*/
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
}
public void info(String message) {
this.getSLF4JLogger().info(message);
}
public void info(String message, Object... args) {
this.getSLF4JLogger().info(message, args);
}
public void error(String message) {
this.getSLF4JLogger().error(message);
}
public void error(String message, Object... args) {
this.getSLF4JLogger().error(message, args);
}
public void warn(String message) {
this.getSLF4JLogger().warn(message);
}
public void warn(String message, Object... args) {
this.getSLF4JLogger().warn(message, args);
}
public void debug(String message, Object... args) {
if (this.isDebugMode()) {
this.getSLF4JLogger().info("[DEBUG] " + message, args);
}
}
private void loadTrackerLoaders(Path path) {
if (Files.notExists(path)) {
return;
}
// Create tracker loaders
try (Stream<Path> trackerPaths = Files.walk(path)) {
trackerPaths.forEach(trackerPath -> {
try {
if (Files.isDirectory(trackerPath)) {
return;
}
Configuration configuration = YamlConfiguration.loadConfiguration(Files.newBufferedReader(trackerPath));
String name = configuration.getString("name");
if (name == null) {
this.warn("Tracker {} does not have a name!", trackerPath.getFileName());
return;
}
TrackerLoader loader = new TrackerLoader(this, configuration, trackerPath);
this.trackerLoaders.put(name, loader);
CommandInjector.inject(name, name.toLowerCase(Locale.ROOT));
} catch (IOException e) {
throw new RuntimeException("Error reading tracker config", e);
}
});
} catch (IOException e) {
throw new RuntimeException("Error walking trackers path!", e);
}
}
private void loadTrackers() {
// Register our trackers once ALL the plugins have loaded. This ensures that
// all custom plugins adding their own trackers have been loaded.
for (TrackerLoader value : this.trackerLoaders.values()) {
try {
value.load();
} catch (Exception e) {
this.error("An error occurred when loading tracker {}: {}", value.trackerPath().getFileName(), e.getMessage(), e);
}
}
// Load custom trackers
for (Map.Entry<String, Supplier<Tracker>> entry : this.trackerSuppliers.entrySet()) {
Tracker tracker = entry.getValue().get();
this.registerTracker(tracker);
}
}
private void loadConfig(boolean reload) {
this.saveDefaultConfig();
File configFile = new File(this.getDataFolder(), "config.yml");
Configuration config = YamlConfiguration.loadConfiguration(configFile);
try {
this.config = BattleTrackerConfig.load(config);
if (config.getBoolean("debug-mode", false)) {
this.debugMode = true;
}
} catch (Exception e) {
this.error("Failed to load BattleTracker configuration!", e);
if (!reload) {
this.getServer().getPluginManager().disablePlugin(this);
}
}
BattleTrackerConfig.DatabaseOptions databaseOptions = this.config.getDatabaseOptions();
// Database can only be initialized once; need a full restart to change it
if (!reload) {
SqlInstance.init(databaseOptions);
}
}
/**
* Returns the instance of the plugin.
*
* @return the instance of the plugin
*/
public static BattleTracker getInstance() {
return instance;
}
}
@@ -1,105 +0,0 @@
package org.battleplugins.tracker;
import org.battleplugins.tracker.sql.SqlSerializer;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import java.util.Locale;
public class BattleTrackerConfig {
private final DatabaseOptions databaseOptions;
private final Rating rating;
private final Advanced advanced;
BattleTrackerConfig(DatabaseOptions databaseOptions, Rating rating, Advanced advanced) {
this.databaseOptions = databaseOptions;
this.rating = rating;
this.advanced = advanced;
}
public DatabaseOptions getDatabaseOptions() {
return this.databaseOptions;
}
public Rating getRating() {
return this.rating;
}
public Advanced getAdvanced() {
return this.advanced;
}
static BattleTrackerConfig load(Configuration configuration) {
ConfigurationSection dbSection = configuration.getConfigurationSection("database");
if (dbSection == null) {
throw new IllegalArgumentException("Database section not found in configuration!");
}
ConfigurationSection ratingSection = configuration.getConfigurationSection("rating");
if (ratingSection == null) {
throw new IllegalArgumentException("Rating section not found in configuration!");
}
ConfigurationSection advancedSection = configuration.getConfigurationSection("advanced");
if (advancedSection == null) {
throw new IllegalArgumentException("Advanced section not found in configuration!");
}
return new BattleTrackerConfig(DatabaseOptions.load(dbSection), Rating.load(ratingSection), Advanced.load(advancedSection));
}
public record DatabaseOptions(
SqlSerializer.SqlType type,
String prefix,
String db,
String url,
String port,
String user,
String password
) {
public static DatabaseOptions load(ConfigurationSection section) {
String typeName = section.getString("type", "sqlite").trim().toUpperCase(Locale.ROOT);
SqlSerializer.SqlType type;
try {
type = SqlSerializer.SqlType.valueOf(typeName);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Unknown database type '" + section.getString("type") + "'. Supported types: sqlite, mysql, mariadb.", e);
}
String prefix = section.getString("prefix");
String db = section.getString("db");
String url = section.getString("url");
String port = section.getString("port");
String user = section.getString("username");
String password = section.getString("password");
return new DatabaseOptions(type, prefix, db, url, port, user, password);
}
}
public record Rating(Elo elo) {
public static Rating load(ConfigurationSection section) {
ConfigurationSection eloSection = section.getConfigurationSection("elo");
Elo elo = new Elo(
(float) eloSection.getDouble("default"),
(float) eloSection.getDouble("spread")
);
return new Rating(elo);
}
}
public record Elo(float defaultElo, float spread) {
}
public record Advanced(boolean flushOnLeave, int saveInterval, int staleEntryTime) {
public static Advanced load(ConfigurationSection section) {
boolean flushOnLeave = section.getBoolean("flush-on-leave");
int saveInterval = section.getInt("save-interval");
int staleEntryTime = section.getInt("stale-entry-time");
return new Advanced(flushOnLeave, saveInterval, staleEntryTime);
}
}
}
@@ -1,94 +0,0 @@
package org.battleplugins.tracker;
import org.battleplugins.tracker.event.BattleTrackerPostInitializeEvent;
import org.battleplugins.tracker.util.TrackerInventoryHolder;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.ServerLoadEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.concurrent.CompletableFuture;
class BattleTrackerListener implements Listener {
private final BattleTracker plugin;
public BattleTrackerListener(BattleTracker plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onServerLoad(ServerLoadEvent event) {
// There is logic called later, however by this point all plugins
// using the BattleTracker API should have been loaded. As modules will
// listen for this event to register their behavior, we need to ensure
// they are fully initialized so any references to said modules in
// tracker config files will be valid.
new BattleTrackerPostInitializeEvent(this.plugin).callEvent();
this.plugin.postInitialize();
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
for (Tracker tracker : this.plugin.getTrackers()) {
tracker.getOrCreateRecord((OfflinePlayer) player).whenComplete((record, e) -> {
if (tracker instanceof SqlTracker sqlTracker) {
sqlTracker.getRecords().lock(player.getUniqueId());
}
});
}
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
for (Tracker tracker : this.plugin.getTrackers()) {
CompletableFuture<Void> saveFuture = tracker.save(player);
if (this.plugin.isDebugMode()) {
long startTimestamp = System.currentTimeMillis();
saveFuture.thenRun(() -> this.plugin.info("Saved {} data for {} in {}ms", tracker.getName(), player.getName(), System.currentTimeMillis() - startTimestamp));
}
if (Bukkit.isStopping()) {
saveFuture.join();
}
if (tracker instanceof SqlTracker sqlTracker) {
sqlTracker.getRecords().unlock(player.getUniqueId());
}
}
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
Inventory topInventory = event.getView().getTopInventory();
if (!(topInventory.getHolder() instanceof TrackerInventoryHolder keyedHolder)) {
return;
}
if (keyedHolder.getKey().equals(TrackerInventoryHolder.RECAP_KEY)) {
event.setCancelled(true);
}
if (!topInventory.equals(event.getClickedInventory())) {
return;
}
ItemStack itemInSlot = event.getCurrentItem();
if (itemInSlot != null && itemInSlot.hasItemMeta() && itemInSlot.getItemMeta().getPersistentDataContainer().has(TrackerInventoryHolder.RECAP_KEY)) {
event.getWhoClicked().closeInventory();
keyedHolder.handleClick(TrackerInventoryHolder.RECAP_KEY);
}
}
}
@@ -1,346 +0,0 @@
package org.battleplugins.tracker;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.MutableClassToInstanceMap;
import org.battleplugins.tracker.event.TallyRecordEvent;
import org.battleplugins.tracker.feature.TrackerFeature;
import org.battleplugins.tracker.sql.DbCache;
import org.battleplugins.tracker.sql.TrackerSqlSerializer;
import org.battleplugins.tracker.stat.Record;
import org.battleplugins.tracker.stat.StatType;
import org.battleplugins.tracker.stat.TallyContext;
import org.battleplugins.tracker.stat.TallyEntry;
import org.battleplugins.tracker.stat.VersusTally;
import org.battleplugins.tracker.stat.calculator.RatingCalculator;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class SqlTracker implements Tracker {
protected final BattleTracker battleTracker;
protected final String name;
private final RatingCalculator calculator;
private final Set<TrackedDataType> trackedData;
private final List<String> disabledWorlds;
private final TrackerSqlSerializer sqlSerializer;
private final DbCache.MapCache<UUID, Record> records = DbCache.createMap();
private final DbCache.SetCache<VersusTally> tallies = DbCache.createSet();
private final DbCache.MultimapCache<UUID, TallyEntry> tallyEntries = DbCache.createMultimap();
private final ClassToInstanceMap<TrackerFeature> features = MutableClassToInstanceMap.create();
private long lastTopLoad = 0;
public SqlTracker(BattleTracker battleTracker, String name, RatingCalculator calculator, Set<TrackedDataType> trackedData, List<String> disabledWorlds) {
this(battleTracker, name, calculator, trackedData, disabledWorlds, null);
}
public SqlTracker(BattleTracker battleTracker, String name, RatingCalculator calculator, Set<TrackedDataType> trackedData, List<String> disabledWorlds, TrackerSqlSerializer sqlSerializer) {
this.battleTracker = battleTracker;
this.name = name;
this.calculator = calculator;
this.trackedData = trackedData;
this.disabledWorlds = disabledWorlds;
if (sqlSerializer == null) {
sqlSerializer = this.createSerializer();
}
this.sqlSerializer = sqlSerializer;
}
protected TrackerSqlSerializer createSerializer() {
return new TrackerSqlSerializer(this);
}
@Override
public String getName() {
return this.name;
}
public DbCache.MapCache<UUID, Record> getRecords() {
return this.records;
}
@Override
public CompletableFuture<@Nullable Record> getRecord(OfflinePlayer player) {
return this.records.getOrLoad(player.getUniqueId(), this.sqlSerializer.loadRecord(player.getUniqueId())).exceptionally(e -> {
this.battleTracker.error("Failed to load record for {}", player.getUniqueId(), e);
return null;
});
}
@Override
public CompletableFuture<List<Record>> getTopRecords(int limit, StatType orderBy) {
// Recompute this every minute
if (System.currentTimeMillis() - this.lastTopLoad > 60000) {
this.lastTopLoad = System.currentTimeMillis();
return this.records.loadBulk(this.sqlSerializer.getTopRecords(limit, orderBy), Record::getId)
.thenApply(records -> records.stream().toList());
}
// Load from cache
return CompletableFuture.completedFuture(this.records.keySet()
.stream()
.map(this.records::getCached)
.sorted((r1, r2) -> Float.compare(r2.getStat(orderBy), r1.getStat(orderBy)))
.limit(limit)
.toList()
);
}
@Override
public List<StatType> getAdditionalStats() {
return List.of();
}
@Override
public Record getRecord(Player player) {
// Online players will always have a record
Record record = this.records.getCached(player.getUniqueId());
if (record == null) {
// Uh oh, there's a bug in BattleTracker!
throw new RuntimeException("Failed to load record for " + player.getUniqueId() + ". This is a bug in the plugin and should be reported!");
}
return record;
}
@Override
public void addRecord(UUID uuid, Record record) {
this.records.put(uuid, record);
}
public DbCache.SetCache<VersusTally> getTallies() {
return this.tallies;
}
@Override
public CompletableFuture<VersusTally> getVersusTally(OfflinePlayer player1, OfflinePlayer player2) {
return this.tallies.getOrLoad(tally -> tally.isTallyFor(player1.getUniqueId(), player2.getUniqueId()), this.sqlSerializer.loadVersusTally(player1.getUniqueId(), player2.getUniqueId())
.exceptionally(e -> {
this.battleTracker.error("Failed to load tally entries for {} and {}", player1.getUniqueId(), player2.getUniqueId(), e);
return null;
}));
}
@Override
public VersusTally createNewVersusTally(OfflinePlayer player1, OfflinePlayer player2) {
VersusTally versusTally = new VersusTally(this, player1, player2, new HashMap<>());
this.tallies.add(versusTally);
return versusTally;
}
@Override
public CompletableFuture<VersusTally> getOrCreateVersusTally(OfflinePlayer player1, OfflinePlayer player2) {
return this.getVersusTally(player1, player2).thenApply(tally -> {
if (tally == null) {
return this.createNewVersusTally(player1, player2);
}
return tally;
});
}
@Override
public void modifyTally(VersusTally tally, Consumer<TallyContext> context) {
context.accept((statType, value) -> {
tally.statistics().put(statType, value);
SqlTracker.this.tallies.modify(tally);
});
}
public DbCache.MultimapCache<UUID, TallyEntry> getTallyEntries() {
return this.tallyEntries;
}
@Override
public void recordTallyEntry(TallyEntry entry) {
this.tallyEntries.put(entry.id1(), entry);
this.tallyEntries.put(entry.id2(), entry);
Record record1 = this.records.getCached(entry.id1());
Record record2 = this.records.getCached(entry.id2());
if (record1 == null || record2 == null) {
this.battleTracker.warn("Failed to call tally entry event for {} and {} as one of the records was not found!", entry.id1(), entry.id2());
return;
}
new TallyRecordEvent(this, record1, record2, entry.tie()).callEvent();
}
@Override
public CompletableFuture<List<TallyEntry>> getTallyEntries(UUID uuid, boolean includeLosses) {
return this.tallyEntries.getOrLoad(uuid, this.sqlSerializer.loadTallyEntries(uuid)).thenApply(entries -> {
if (includeLosses) {
return entries;
}
return entries.stream().filter(entry -> entry.id1().equals(uuid)).toList();
}).exceptionally(e -> {
this.battleTracker.error("Failed to load tally entries for {}", uuid, e);
return List.of();
});
}
@Override
public void setValue(StatType statType, float value, OfflinePlayer player) {
this.getOrCreateRecord(player).whenComplete((record, e) -> {
if (record == null) {
return;
}
record.setValue(statType, value);
});
}
@Override
public void updateRating(Player killer, Player loser, boolean tie) {
Record killerRecord = this.getOrCreateRecord(killer);
Record killedRecord = this.getOrCreateRecord(loser);
this.calculator.updateRating(killerRecord, killedRecord, tie);
float killerRating = killerRecord.getRating();
float killerMaxRating = killerRecord.getStat(StatType.MAX_RATING);
this.setValue(StatType.RATING, killerRecord.getRating(), killer);
this.setValue(StatType.RATING, killedRecord.getRating(), loser);
if (killerRating > killerMaxRating) {
this.setValue(StatType.MAX_RATING, killerRating, killer);
}
if (tie) {
this.incrementValue(StatType.TIES, killer);
this.incrementValue(StatType.TIES, loser);
}
this.setValue(StatType.KD_RATIO, killerRecord.getStat(StatType.KILLS) / Math.max(1, killerRecord.getStat(StatType.DEATHS)), killer);
this.setValue(StatType.KD_RATIO, killedRecord.getStat(StatType.KILLS) / Math.max(1, killedRecord.getStat(StatType.DEATHS)), loser);
float killerKdr = killerRecord.getStat(StatType.KD_RATIO);
float killerMaxKdr = killerRecord.getStat(StatType.MAX_KD_RATIO);
if (killerKdr > killerMaxKdr) {
this.setValue(StatType.MAX_KD_RATIO, killerKdr, killer);
}
this.setValue(StatType.STREAK, 0, loser);
this.incrementValue(StatType.STREAK, killer);
float killerStreak = killerRecord.getStat(StatType.STREAK);
float killerMaxStreak = killerRecord.getStat(StatType.MAX_STREAK);
if (killerStreak > killerMaxStreak) {
this.setValue(StatType.MAX_STREAK, killerStreak, killer);
}
}
@Override
public Record createNewRecord(OfflinePlayer player) {
Map<StatType, Float> columns = new HashMap<>();
for (String column : this.sqlSerializer.getOverallColumns()) {
columns.put(StatType.get(column), 0f);
}
Record record = new Record(this, player.getUniqueId(), player.getName(), columns);
return this.createNewRecord(player, record);
}
@Override
public Record createNewRecord(OfflinePlayer player, Record record) {
record.setRating(this.calculator.getDefaultRating());
this.records.put(player.getUniqueId(), record);
return record;
}
@Override
public void removeRecord(OfflinePlayer player) {
this.records.remove(player.getUniqueId());
this.sqlSerializer.removeRecord(player.getUniqueId());
}
@Override
public <T extends TrackerFeature> Optional<T> feature(Class<T> feature) {
return Optional.ofNullable(this.getFeature(feature));
}
@Override
public boolean hasFeature(Class<? extends TrackerFeature> feature) {
return this.features.containsKey(feature);
}
@Override
public <T extends TrackerFeature> @Nullable T getFeature(Class<T> feature) {
return this.features.getInstance(feature);
}
@Override
public void registerFeature(TrackerFeature feature) {
this.features.put(feature.getClass(), feature);
if (feature.enabled()) {
feature.onEnable(this.battleTracker, this);
}
}
@Override
public Set<TrackedDataType> getTrackedData() {
return Set.copyOf(this.trackedData);
}
@Override
public List<String> getDisabledWorlds() {
return List.copyOf(this.disabledWorlds);
}
@Override
public RatingCalculator getRatingCalculator() {
return this.calculator;
}
@Override
public CompletableFuture<Void> save(OfflinePlayer player) {
return this.sqlSerializer.save(player.getUniqueId());
}
@Override
public CompletableFuture<Void> saveAll() {
return this.sqlSerializer.saveAll();
}
@Override
public CompletableFuture<Void> saveAll(boolean async) {
return this.sqlSerializer.saveAll(async);
}
@Override
public void flush(boolean aggressive) {
for (UUID uuid : this.records.keySet()) {
this.records.flush(uuid, aggressive);
}
this.tallies.flush(aggressive);
for (UUID uuid : this.tallyEntries.keySet()) {
this.tallyEntries.flush(uuid, aggressive);
}
}
@Override
public void destroy(boolean block) {
this.flush(true);
}
}
@@ -1,19 +0,0 @@
package org.battleplugins.tracker;
/**
* Represents the type of data being tracked.
*/
public enum TrackedDataType {
/**
* Represents player versus player data.
*/
PVP,
/**
* Represents player versus enemy data.
*/
PVE,
/**
* Represents player versus world data.
*/
WORLD
}
@@ -1,365 +0,0 @@
package org.battleplugins.tracker;
import org.battleplugins.tracker.feature.TrackerFeature;
import org.battleplugins.tracker.stat.Record;
import org.battleplugins.tracker.stat.StatType;
import org.battleplugins.tracker.stat.TallyContext;
import org.battleplugins.tracker.stat.TallyEntry;
import org.battleplugins.tracker.stat.VersusTally;
import org.battleplugins.tracker.stat.calculator.RatingCalculator;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* Main interface used for tracking stats.
*/
public interface Tracker {
/**
* Returns the name of the tracker.
*
* @return the name of the tracker
*/
String getName();
/**
* Returns the record for the given OfflinePlayer.
*
* @param player the OfflinePlayer to get the record from
* @return the record for the given OfflinePlayer
*/
CompletableFuture<@Nullable Record> getRecord(OfflinePlayer player);
/**
* Returns the top records for the specified stat type, ordered by
* the specified stat type.
*
* @param limit the limit of records to get
* @param orderBy the stat type to order by
* @return the top records for the specified stat type
*/
CompletableFuture<List<Record>> getTopRecords(int limit, StatType orderBy);
/**
* Returns additional stats that are tracked by this tracker. These are
* typically populated by third party trackers that wish to track stats
* that are not included in BattleTracker by default.
*
* @return additional stats that are tracked by this tracker
*/
List<StatType> getAdditionalStats();
/**
* Returns the record for the given Player.
*
* @param player the Player to get the record from
* @return the record for the given Player
*/
Record getRecord(Player player);
/**
* Adds a record to the tracker.
*
* @param uuid the uuid to add the record for
* @param record the record to add
*/
void addRecord(UUID uuid, Record record);
/**
* Returns the versus tally for the given OfflinePlayers.
*
* @param player1 the first OfflinePlayer to get the versus tally from
* @param player2 the second OfflinePlayer to get the versus tally from
* @return the versus tally for the given OfflinePlayers
*/
CompletableFuture<VersusTally> getVersusTally(OfflinePlayer player1, OfflinePlayer player2);
/**
* Creates a new versus tally for the given OfflinePlayers.
*
* @param player1 the first OfflinePlayer in the versus tally
* @param player2 the second OfflinePlayer in the versus tally
* @return the new versus tally for the given OfflinePlayers
*/
VersusTally createNewVersusTally(OfflinePlayer player1, OfflinePlayer player2);
/**
* Returns or creates a versus tally for the given OfflinePlayers.
*
* @param player1 the first OfflinePlayer to get the versus tally from
* @param player2 the second OfflinePlayer to get the versus tally from
* @return the versus tally for the given OfflinePlayers or create a new one
*/
CompletableFuture<VersusTally> getOrCreateVersusTally(OfflinePlayer player1, OfflinePlayer player2);
/**
* Modifies the tally with the given context.
*
* @param tally the tally to modify
* @param context the context to modify the tally with
*/
void modifyTally(VersusTally tally, Consumer<TallyContext> context);
/**
* Records a tally entry.
*
* @param entry the tally entry to record
*/
void recordTallyEntry(TallyEntry entry);
/**
* Returns a list of tally entries for the given player.
*
* @param uuid the UUID of the player to get the tally entries for
* @param includeLosses if losses should be included in the returned list
* @return a list of tally entries for the given player
*/
CompletableFuture<List<TallyEntry>> getTallyEntries(UUID uuid, boolean includeLosses);
/**
* Increments a value with the given stat type.
*
* @param statType the stat type to increment the value for
* @param player the player to increment the value for
*/
default void incrementValue(StatType statType, Player player) {
Record record = this.getRecord(player);
if (record == null) {
return;
}
record.setValue(statType, record.getStat(statType) + 1);
}
/**
* Decrements a value with the given stat type.
*
* @param statType the stat type to decrement the value for
* @param player the player to decrement the value for
*/
default void decrementValue(StatType statType, Player player) {
Record record = this.getRecord(player);
if (record == null) {
return;
}
record.setValue(statType, record.getStat(statType) - 1);
}
/**
* Sets a value with the given stat type.
*
* @param statType the stat type to set the value for
* @param value the value to set
* @param player the player to set the value for
*/
void setValue(StatType statType, float value, OfflinePlayer player);
/**
* Sets the rating for the specified players.
*
* @param killer the player to increment the rating for
* @param loser the player to decrement the rating for
* @param tie if the end result was a tie
*/
void updateRating(Player killer, Player loser, boolean tie);
/**
* Enables tracking for the specified player.
*
* @param player the player to enable tracking for
*/
default void enableTracking(Player player) {
Record record = this.getRecord(player);
if (record == null) {
return;
}
record.setTracking(true);
}
/**
* Disables tracking for the specified player.
*
* @param player the player to disable tracking for
*/
default void disableTracking(Player player) {
Record record = this.getRecord(player);
if (record == null) {
return;
}
record.setTracking(false);
}
/**
* Adds a record for the specified player to the tracker from the
* default SQL columns.
*
* @param player the player to create the record for
* @return the new record created
*/
Record createNewRecord(OfflinePlayer player);
/**
* Adds a record for the specified player to the tracker.
*
* @param player the player to create the record for
* @param record the record to add
* @return the new record created
*/
Record createNewRecord(OfflinePlayer player, Record record);
/**
* Returns the record for the given player or creates
* a new one of one was unable to be found.
*
* @param player the player to get/create the record for
* @return the record for the given player or create a new one
*/
default CompletableFuture<Record> getOrCreateRecord(OfflinePlayer player) {
return this.getRecord(player).thenApply(record -> {
if (record == null) {
return this.createNewRecord(player);
}
return record;
});
}
/**
* Returns the record for the given player or creates
* a new one of one was unable to be found.
*
* @param player the player to get/create the record for
* @return the record for the given player or create a new one
*/
default Record getOrCreateRecord(Player player) {
Record record = this.getRecord(player);
if (record == null) {
return this.createNewRecord(player);
}
return record;
}
/**
* Removes the record for the specified player.
*
* @param player the player to remove the record for
*/
void removeRecord(OfflinePlayer player);
/**
* Returns the {@link TrackerFeature} from the given class.
*
* @param feature the class of the feature
* @return the feature from the given class
*/
<T extends TrackerFeature> Optional<T> feature(Class<T> feature);
/**
* Returns if the tracker has the specified feature.
*
* @param feature the class of the feature
* @return if the tracker has the specified feature
*/
boolean hasFeature(Class<? extends TrackerFeature> feature);
/**
* Returns the {@link TrackerFeature} from the given class.
*
* @param feature the class of the feature
* @return the feature from the given class
*/
@Nullable
<T extends TrackerFeature> T getFeature(Class<T> feature);
/**
* Registers a feature to the tracker.
*
* @param feature the feature to register
*/
<T extends TrackerFeature> void registerFeature(T feature);
/**
* Returns if the tracker tracks the specified data type.
*
* @param type the data type to check
* @return if the tracker tracks the specified data type
*/
default boolean tracksData(TrackedDataType type) {
return this.getTrackedData().contains(type);
}
/**
* Returns the {@link TrackedDataType} tracked by this
* tracker.
*
* @return the data tracked by this tracker
*/
Set<TrackedDataType> getTrackedData();
/**
* Returns a list of disabled worlds for this tracker.
*
* @return a list of disabled worlds for this tracker
*/
List<String> getDisabledWorlds();
/**
* Returns the rating calculator for this tracker.
*
* @return the rating calculator for this tracker
*/
RatingCalculator getRatingCalculator();
/**
* Saves the records to the database for the specified player.
*
* @param player the player to save records for
*/
CompletableFuture<Void> save(OfflinePlayer player);
/**
* Saves the records for all the players in the cache.
*/
CompletableFuture<Void> saveAll();
/**
* Saves the records for all the players in the cache.
*
* @param async whether the save should run asynchronously
* @return a future that completes when the save finishes
*/
default CompletableFuture<Void> saveAll(boolean async) {
return this.saveAll();
}
/**
* Flushes cached data in the tracker.
* <p>
* This method provides the option to aggressively
* flush the cache, which will remove all cached objects
* that do not have a lock set on them.
* <p>
* NOTE: Ensure you have run the {@link #saveAll()} method
* as this method will not clear unsaved entries.
*
* @param aggressive if the flush should be aggressive.
*/
void flush(boolean aggressive);
/**
* Destroys the tracker.
*/
void destroy(boolean block);
}
@@ -1,474 +0,0 @@
package org.battleplugins.tracker;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.battleplugins.tracker.feature.recap.BattleRecap;
import org.battleplugins.tracker.feature.recap.Recap;
import org.battleplugins.tracker.feature.recap.RecapRoundup;
import org.battleplugins.tracker.message.Messages;
import org.battleplugins.tracker.stat.Record;
import org.battleplugins.tracker.stat.StatType;
import org.battleplugins.tracker.stat.TallyEntry;
import org.battleplugins.tracker.stat.VersusTally;
import org.battleplugins.tracker.util.Util;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
public class TrackerExecutor implements TabExecutor {
private final Tracker tracker;
private final Map<String, SimpleExecutor> commands;
public TrackerExecutor(Tracker tracker) {
this.tracker = tracker;
this.commands = new HashMap<>(
Map.of(
"top", new SimpleExecutor("View the top players of this tracker.", Arguments.ofOptional("max"), this::top),
"rank", new SimpleExecutor("View the rank of a player.", Arguments.ofOptional("player"), this::rank),
"versus", new SimpleExecutor("Compare the stats of players in relation to each other.", Arguments.of("player").optional("target"), this::versus)
)
);
if (this.tracker.hasFeature(Recap.class)) {
this.commands.put("recap", new SimpleExecutor("View the recap of a player.", Arguments.ofOptional("player"), this::recap));
}
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length == 0) {
this.sendHelp(sender, label);
return true;
}
SimpleExecutor simpleCommand = this.commands.get(args[0]);
if (simpleCommand != null) {
if (!hasPermission(sender, args[0])) {
Messages.send(sender, "command-no-permission");
return true;
}
simpleCommand.consumer().accept(sender, args.length == 1 ? "" : String.join(" ", args).replaceFirst(args[0], "").trim());
return true;
}
this.sendHelp(sender, label);
return true;
}
public void sendHelp(CommandSender sender, String label) {
if (!hasPermission(sender, "help")) {
Messages.send(sender, "command-no-permission");
return;
}
Messages.send(sender, "header", this.tracker.getName());
Map<String, Executor> executors = new HashMap<>(this.commands);
// Sort alphabetical
executors.keySet().stream()
.sorted()
.forEach(command -> {
Executor executor = executors.get(command);
String args = executor.describeArgs();
sender.sendMessage(Component.text("/" + label + " " + command + (args.isEmpty() ? "" : " " + args), NamedTextColor.YELLOW)
.append(Component.text(" " + executor.description(), NamedTextColor.GOLD)));
});
}
public void sendHelp(CommandSender sender, String label, String cmd, @Nullable Executor executor) {
if (executor == null) {
this.sendHelp(sender, label);
return;
}
Messages.send(sender, "command-usage", "/" + label + " " + cmd + " " + executor.describeArgs());
}
private void top(CommandSender sender, String argument) {
int amount = Math.max(1, argument == null || argument.isBlank() ? 5 : Math.min(100, Integer.parseInt(argument)));
Util.getSortedRecords(this.tracker, amount, StatType.RATING).whenComplete((records, e) -> {
if (records.isEmpty()) {
Messages.send(sender, "leaderboard-no-entries");
return;
}
Messages.send(sender, "header", this.tracker.getName());
int ranking = 1;
for (Map.Entry<Record, Float> entry : records.entrySet()) {
Record record = entry.getKey();
Util.sendTrackerMessage(sender, "leaderboard", ranking++, record);
}
});
}
private void rank(CommandSender sender, String playerName) {
OfflinePlayer target;
if (!(sender instanceof Player) && (playerName == null || playerName.isBlank())) {
Messages.send(sender, "command-player-not-found", "<blank>");
return;
} else if (sender instanceof Player player && (playerName == null || playerName.isBlank())) {
target = player;
} else {
target = Bukkit.getServer().getOfflinePlayerIfCached(playerName);
}
if (target == null) {
CompletableFuture.supplyAsync(() -> Bukkit.getOfflinePlayer(playerName)).thenCompose(this.tracker::getRecord).whenCompleteAsync((record, e) -> {
if (record == null) {
Messages.send(sender, "player-has-no-record", playerName);
return;
}
this.rank(sender, record);
}, Bukkit.getScheduler().getMainThreadExecutor(BattleTracker.getInstance()));
} else {
tracker.getRecord(target).whenCompleteAsync((record, e) -> {
if (record == null) {
Messages.send(sender, "player-has-no-record", playerName);
return;
}
this.rank(sender, record);
}, Bukkit.getScheduler().getMainThreadExecutor(BattleTracker.getInstance()));
}
}
private void rank(CommandSender sender, Record record) {
Util.sendTrackerMessage(sender, "rank", -1, record);
}
private void versus(CommandSender sender, String arg) {
String[] args = arg.split(" ");
if (args.length == 0) {
Messages.send(sender, "command-player-not-found", "<blank>");
return;
}
if (args.length == 1) {
if (sender instanceof Player player) {
String playerName = args[0];
CompletableFuture.supplyAsync(() -> Bukkit.getOfflinePlayer(playerName)).whenCompleteAsync((p, e) -> {
if (e != null) {
BattleTracker.getInstance().error("Failed to get versus tally for {} and {}", playerName, player.getName(), e);
return;
}
if (p == null) {
Messages.send(sender, "command-player-not-found", playerName);
return;
}
this.versus(sender, player, p);
}, Bukkit.getScheduler().getMainThreadExecutor(BattleTracker.getInstance()));
} else {
Messages.send(sender, "command-player-not-found", "<blank>");
}
} else if (args.length == 2) {
CompletableFuture<OfflinePlayer> player1Future = CompletableFuture.supplyAsync(() -> Bukkit.getOfflinePlayer(args[0]));
CompletableFuture<OfflinePlayer> player2Future = CompletableFuture.supplyAsync(() -> Bukkit.getOfflinePlayer(args[1]));
CompletableFuture.allOf(
player1Future,
player2Future
).whenCompleteAsync((players, e) -> {
if (e != null) {
BattleTracker.getInstance().error("Failed to get versus tally for {} and {}", args[0], args[1], e);
return;
}
OfflinePlayer player1 = player1Future.join();
OfflinePlayer player2 = player2Future.join();
if (player1 == null) {
Messages.send(sender, "command-player-not-found", args[0]);
return;
}
if (player2 == null) {
Messages.send(sender, "command-player-not-found", args[1]);
return;
}
this.versus(sender, player1, player2);
}, Bukkit.getScheduler().getMainThreadExecutor(BattleTracker.getInstance()));
}
}
private void recap(CommandSender sender, String argument) {
String[] args = argument.split(" ");
String arg = args.length >= 1 ? args[0] : null;
Player player = arg == null || arg.isBlank() ? null : Bukkit.getPlayer(arg);
if (player == null && sender instanceof Player senderPlayer) {
player = senderPlayer;
}
if (player == null) {
Messages.send(sender, "command-player-not-found", (arg == null || arg.isBlank()) ? "<blank>" : arg);
return;
}
Recap recap = this.tracker.feature(Recap.class).orElseThrow(() -> new IllegalStateException("Recap feature is not enabled!"));
if (!recap.enabled()) {
Messages.send(sender, "recap-not-enabled");
return;
}
BattleRecap battleRecap = recap.getPreviousRecap(player);
if (battleRecap == null || battleRecap.getLastEntry() == null) {
Messages.send(sender, "recap-no-recap");
return;
}
if (args.length >= 2) {
boolean sent = false;
switch (args[1].toLowerCase(Locale.ROOT)) {
case "item":
RecapRoundup.recapItem(sender, battleRecap);
sent = true;
break;
case "entity":
RecapRoundup.recapEntity(sender, battleRecap);
sent = true;
break;
case "cause":
RecapRoundup.recapSource(sender, battleRecap);
sent = true;
break;
case "player":
RecapRoundup.recapPlayer(sender, battleRecap);
sent = true;
break;
default:
break;
}
if (sent) {
RecapRoundup.sendFooter(sender, this.tracker, battleRecap);
return;
}
}
recap.showRecap(sender, this.tracker, battleRecap);
}
private void versus(CommandSender sender, OfflinePlayer player1, OfflinePlayer player2) {
CompletableFuture<VersusTally> future = this.tracker.getVersusTally(player1, player2);
future.whenComplete((tally, e) -> {
if (e != null) {
BattleTracker.getInstance().error("Failed to get versus tally for {} and {}", player1.getName(), player2.getName(), e);
return;
}
if (tally == null) {
Messages.send(sender, "player-has-no-tally", player1.getName(), player2.getName());
return;
}
CompletableFuture<Record> record1Future = this.tracker.getRecord(player1);
CompletableFuture<Record> record2Future = this.tracker.getRecord(player2);
CompletableFuture.allOf(
record1Future,
record2Future
).whenCompleteAsync((records, ex) -> {
if (ex != null) {
BattleTracker.getInstance().error("Failed to get records for {} and {}", player1.getName(), player2.getName(), ex);
return;
}
Record record1 = record1Future.join();
Record record2 = record2Future.join();
if (record1 == null) {
Messages.send(sender, "player-has-no-record", player1.getName());
return;
}
if (record2 == null) {
Messages.send(sender, "player-has-no-record", player2.getName());
return;
}
this.versus0(sender, player1, record1, player2, record2, tally);
}, Bukkit.getScheduler().getMainThreadExecutor(BattleTracker.getInstance()));
});
}
private void versus0(CommandSender sender, OfflinePlayer player1, Record record1, OfflinePlayer player2, Record record2, VersusTally tally) {
Messages.send(sender, "header", Messages.getPlain("versus-tally"));
Messages.send(sender, "versus", Map.of(
"player", record1.getName(),
"target", record2.getName(),
"player_rating", Util.STAT_FORMAT.format(record1.getRating()),
"target_rating", Util.STAT_FORMAT.format(record2.getRating())
));
Map<String, Object> replacements = new HashMap<>();
replacements.put("kills", Util.STAT_FORMAT.format(tally.getStat(StatType.KILLS)));
replacements.put("deaths", Util.STAT_FORMAT.format(tally.getStat(StatType.DEATHS)));
// Since versus tallies are only stored one way, we need to flip the value
// in the scenario that the "1st" player instead the 2nd player
if (tally.id2().equals(player1.getUniqueId())) {
replacements.put("player", player2.getName());
replacements.put("target", player1.getName());
} else {
replacements.put("player", player1.getName());
replacements.put("target", player2.getName());
}
Messages.send(sender, "versus-compare", replacements);
CompletableFuture<List<TallyEntry>> future = this.tracker.getTallyEntries(player1.getUniqueId(), true);
future.whenComplete((entries, e) -> {
if (e != null) {
BattleTracker.getInstance().error("Failed to get tally entries for {}", player1.getName(), e);
return;
}
if (entries == null || entries.isEmpty()) {
return;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Messages.getPlain("date-format"))
.withLocale(sender instanceof Player player ? player.locale() : Locale.ROOT)
.withZone(ZoneId.systemDefault());
Messages.send(sender, "versus-history");
// Sort entries by most recent
entries.stream()
.filter(entry -> {
// Ensure the entries are against the same two players
return (entry.id1().equals(player1.getUniqueId()) && entry.id2().equals(player2.getUniqueId())) ||
(entry.id1().equals(player2.getUniqueId()) && entry.id2().equals(player1.getUniqueId()));
})
.sorted((e1, e2) -> e2.timestamp().compareTo(e1.timestamp()))
.limit(5) // Limit to top 5
.forEach(entry -> {
// If the player is the first player, they won
if (entry.id1().equals(player1.getUniqueId())) {
Messages.send(sender, "versus-history-entry-win", Map.of(
"player", player1.getName(),
"target", player2.getName(),
"date", formatter.format(entry.timestamp())
));
} else {
Messages.send(sender, "versus-history-entry-loss", Map.of(
"player", player1.getName(),
"target", player2.getName(),
"date", formatter.format(entry.timestamp())
));
}
});
});
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
StringUtil.copyPartialMatches(args[0], this.commands.keySet(), completions);
}
return completions;
}
record SimpleExecutor(String description, Arguments args, BiConsumer<CommandSender, String> consumer) implements Executor {
@Override
public String describeArgs() {
return this.args.describe();
}
}
private static boolean hasPermission(CommandSender sender, String node) {
return sender.hasPermission("battletracker.command." + node);
}
public interface Executor {
String description();
String describeArgs();
}
public static class Arguments {
private final List<Argument> arguments = new ArrayList<>();
private Arguments() {
}
public String describe() {
if (this.arguments.isEmpty()) {
return "";
}
return this.arguments.stream()
.map(argument -> argument.required() ? "<" + argument.name() + ">" : "[" + argument.name() + "]")
.reduce((a, b) -> a + " " + b)
.orElse("");
}
private Arguments(boolean required, String... arguments) {
for (String argument : arguments) {
this.arguments.add(new Argument(argument, required));
}
}
public Arguments required(String... arguments) {
for (String argument : arguments) {
this.arguments.add(new Argument(argument, true));
}
return this;
}
public Arguments optional(String... arguments) {
for (String argument : arguments) {
this.arguments.add(new Argument(argument, false));
}
return this;
}
private Arguments(Argument... arguments) {
this.arguments.addAll(List.of(arguments));
}
public static Arguments of(String... arguments) {
return new Arguments(true, arguments);
}
public static Arguments of(Argument... arguments) {
return new Arguments(arguments);
}
public static Arguments ofOptional(String... arguments) {
return new Arguments(false, arguments);
}
public static Arguments empty() {
return new Arguments();
}
public record Argument(String name, boolean required) {
}
}
}
@@ -1,91 +0,0 @@
package org.battleplugins.tracker;
import org.battleplugins.tracker.feature.Killstreaks;
import org.battleplugins.tracker.feature.Rampage;
import org.battleplugins.tracker.feature.message.DeathMessages;
import org.battleplugins.tracker.feature.recap.Recap;
import org.battleplugins.tracker.listener.PvEListener;
import org.battleplugins.tracker.listener.PvPListener;
import org.battleplugins.tracker.stat.calculator.RatingCalculator;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.Configuration;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
record TrackerLoader(BattleTracker battleTracker, Configuration configuration, Path trackerPath) {
public void load() {
String name = this.configuration.getString("name");
List<String> trackedData = this.configuration.getStringList("tracked-statistics");
boolean enabled = this.configuration.getBoolean("enabled", !trackedData.isEmpty());
if (!enabled) {
return;
}
if (trackedData.isEmpty()) {
this.battleTracker.warn("No tracked data found for tracker {}!", name);
return;
}
String calculatorName = this.configuration.getString("calculator");
RatingCalculator calculator = this.battleTracker.getCalculator(calculatorName);
if (calculator == null) {
this.battleTracker.warn("Rating calculator {} not found!", calculatorName);
return;
}
Set<TrackedDataType> dataTypes = EnumSet.noneOf(TrackedDataType.class);
for (String data : trackedData) {
try {
TrackedDataType type = TrackedDataType.valueOf(data.toUpperCase(Locale.ROOT));
dataTypes.add(type);
} catch (IllegalArgumentException e) {
this.battleTracker.warn("Unknown tracked data type {} for tracker {}!", data, name);
}
}
List<String> disabledWorlds = this.configuration.getStringList("disabled-worlds");
Tracker tracker = new SqlTracker(this.battleTracker, name, calculator, dataTypes, disabledWorlds);
if (this.configuration.isConfigurationSection("killstreaks")) {
tracker.registerFeature(Killstreaks.load(this.configuration.getConfigurationSection("killstreaks")));
}
if (this.configuration.isConfigurationSection("rampage")) {
tracker.registerFeature(Rampage.load(this.configuration.getConfigurationSection("rampage")));
}
if (this.configuration.isConfigurationSection("death-messages")) {
tracker.registerFeature(DeathMessages.load(this.configuration.getConfigurationSection("death-messages")));
}
if (this.configuration.isConfigurationSection("recap")) {
tracker.registerFeature(Recap.load(this.configuration.getConfigurationSection("recap")));
} else {
// Recaps are always enabled as they are used throughout the tracker for
// retrieving player damages. However, whether they are displayed is determined
// by the configuration.
throw new IllegalArgumentException("Recap configuration not found!");
}
// Register command
PluginCommand command = this.battleTracker.getCommand(tracker.getName().toLowerCase(Locale.ROOT));
TrackerExecutor executor = new TrackerExecutor(tracker);
command.setExecutor(executor);
if (tracker.tracksData(TrackedDataType.PVP)) {
this.battleTracker.registerListener(tracker, new PvPListener(tracker));
}
if (tracker.tracksData(TrackedDataType.PVE) || tracker.tracksData(TrackedDataType.WORLD)) {
this.battleTracker.registerListener(tracker, new PvEListener(tracker));
}
this.battleTracker.registerTracker(tracker);
this.battleTracker.info("Loaded tracker: {}.", name);
}
}
@@ -1,38 +0,0 @@
package org.battleplugins.tracker.event;
import org.battleplugins.tracker.BattleTracker;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* Called when BattleTracker is fully initialized.
*/
public class BattleTrackerPostInitializeEvent extends Event {
private final static HandlerList HANDLERS = new HandlerList();
private final BattleTracker battleTracker;
public BattleTrackerPostInitializeEvent(BattleTracker battleTracker) {
this.battleTracker = battleTracker;
}
/**
* Returns the {@link BattleTracker} instance.
*
* @return the BattleTracker instance
*/
public BattleTracker getBattleTracker() {
return battleTracker;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,38 +0,0 @@
package org.battleplugins.tracker.event;
import org.battleplugins.tracker.BattleTracker;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* Called when BattleTracker is starting its initialization.
*/
public class BattleTrackerPreInitializeEvent extends Event {
private final static HandlerList HANDLERS = new HandlerList();
private final BattleTracker battleTracker;
public BattleTrackerPreInitializeEvent(BattleTracker battleTracker) {
this.battleTracker = battleTracker;
}
/**
* Returns the {@link BattleTracker} instance.
*
* @return the BattleTracker instance
*/
public BattleTracker getBattleTracker() {
return battleTracker;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,74 +0,0 @@
package org.battleplugins.tracker.event;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.stat.Record;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* Called when a tally is recorded for two different
* players.
*/
public class TallyRecordEvent extends Event {
private final static HandlerList HANDLERS = new HandlerList();
private final Tracker tracker;
private final Record victor;
private final Record loser;
private final boolean tie;
public TallyRecordEvent(Tracker tracker, Record victor, Record loser, boolean tie) {
this.tracker = tracker;
this.victor = victor;
this.loser = loser;
this.tie = tie;
}
/**
* Returns the {@link Tracker} instance this
* tally was recorded in.
*
* @return the Tracker instance
*/
public Tracker getTracker() {
return this.tracker;
}
/**
* Returns the victor of the tally.
*
* @return the victor of the tally
*/
public Record getVictor() {
return this.victor;
}
/**
* Returns the loser of the tally.
*
* @return the loser of the tally
*/
public Record getLoser() {
return this.loser;
}
/**
* Returns whether the tally was a tie.
*
* @return whether the tally was a tie
*/
public boolean isTie() {
return this.tie;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,107 +0,0 @@
package org.battleplugins.tracker.event;
import org.battleplugins.tracker.Tracker;
import org.bukkit.entity.Entity;
import org.bukkit.event.HandlerList;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
/**
* Called when a player dies and has their data
* tracked by BattleTracker.
*/
public class TrackerDeathEvent extends PlayerEvent {
private final static HandlerList HANDLERS = new HandlerList();
private final Tracker tracker;
private final DeathType deathType;
private final Entity killer;
private final PlayerDeathEvent deathEvent;
public TrackerDeathEvent(Tracker tracker, DeathType deathType, @Nullable Entity killer, PlayerDeathEvent deathEvent) {
super(deathEvent.getEntity());
this.tracker = tracker;
this.deathType = deathType;
this.killer = killer;
this.deathEvent = deathEvent;
}
/**
* Returns the {@link Tracker} instance this
* tally was recorded in.
*
* @return the Tracker instance
*/
public Tracker getTracker() {
return this.tracker;
}
/**
* Returns the type of death the player experienced.
*
* @return the type of death
*/
public DeathType getDeathType() {
return this.deathType;
}
/**
* Returns the entity that killed the player.
*
* @return the entity that killed the player
*/
public Optional<Entity> killer() {
return Optional.ofNullable(this.killer);
}
/**
* Returns the entity that killed the player.
*
* @return the entity that killed the player
*/
@Nullable
public Entity getKiller() {
return this.killer;
}
/**
* Returns the backing {@link PlayerDeathEvent}.
*
* @return the backing PlayerDeathEvent
*/
public PlayerDeathEvent getDeathEvent() {
return this.deathEvent;
}
/**
* Returns the type of death the player experienced.
*/
public enum DeathType {
/**
* The player died to another player.
*/
PLAYER,
/**
* The player died to an entity.
*/
ENTITY,
/**
* The player died to the world.
*/
WORLD
}
@Override
public @NotNull HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,64 +0,0 @@
package org.battleplugins.tracker.event.feature;
import net.kyori.adventure.text.Component;
import org.battleplugins.tracker.Tracker;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
/**
* Called when a player dies and has their death message
* displayed by BattleTracker.
*/
public class DeathMessageEvent extends PlayerEvent {
private final static HandlerList HANDLERS = new HandlerList();
private Component deathMessage;
private final Tracker tracker;
public DeathMessageEvent(Player player, Component deathMessage, Tracker tracker) {
super(player);
this.deathMessage = deathMessage;
this.tracker = tracker;
}
/**
* Returns the {@link Tracker} instance this
* death message was displayed for.
*
* @return the Tracker instance
*/
public Tracker getTracker() {
return this.tracker;
}
/**
* Returns the death message that will be displayed.
*
* @return the death message
*/
public Component getDeathMessage() {
return deathMessage;
}
/**
* Sets the death message that will be displayed.
*
* @param deathMessage the death message
*/
public void setDeathMessage(Component deathMessage) {
this.deathMessage = deathMessage;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,12 +0,0 @@
package org.battleplugins.tracker.feature;
import org.battleplugins.tracker.BattleTracker;
public interface Feature {
boolean enabled();
void onEnable(BattleTracker battleTracker);
void onDisable(BattleTracker battleTracker);
}
@@ -1,87 +0,0 @@
package org.battleplugins.tracker.feature;
import net.kyori.adventure.text.Component;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.event.TrackerDeathEvent;
import org.battleplugins.tracker.feature.message.MessageAudience;
import org.battleplugins.tracker.stat.Record;
import org.battleplugins.tracker.stat.StatType;
import org.battleplugins.tracker.util.MessageUtil;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.util.HashMap;
import java.util.Map;
public record Killstreaks(
boolean enabled,
int minimumKills,
int killstreakMessageInterval,
MessageAudience audience,
Map<String, Component> messages
) implements TrackerFeature {
@Override
public void onEnable(BattleTracker battleTracker, Tracker tracker) {
battleTracker.registerListener(tracker, new KillstreakListener(tracker, this));
}
public static Killstreaks load(ConfigurationSection section) {
boolean enabled = section.getBoolean("enabled");
if (!enabled) {
return new Killstreaks(false, 0, 0, MessageAudience.GLOBAL, Map.of());
}
int minimumKills = section.getInt("minimum-kills");
int killstreakMessageInterval = section.getInt("killstreak-message-interval");
MessageAudience audience = MessageAudience.get(section.getString("audience"));
Map<String, Component> messages = new HashMap<>();
ConfigurationSection messagesSection = section.getConfigurationSection("messages");
messagesSection.getKeys(false).forEach(key -> {
if (!messagesSection.isString(key)) {
throw new IllegalArgumentException("Message " + key + " is not a string!");
}
messages.put(key, MessageUtil.deserialize(messagesSection.getString(key)));
});
return new Killstreaks(true, minimumKills, killstreakMessageInterval, audience, messages);
}
private record KillstreakListener(Tracker tracker, Killstreaks killstreaks) implements Listener {
@EventHandler
public void onTrackerDeath(TrackerDeathEvent event) {
if (!event.getTracker().equals(this.tracker)) {
return;
}
if (!(event.getKiller() instanceof Player killer)) {
return;
}
Record record = this.tracker.getRecord(killer);
float streak = record.getStat(StatType.STREAK);
if (streak >= this.killstreaks.minimumKills()) {
if ((int) streak % this.killstreaks.killstreakMessageInterval() != 0) {
return;
}
String killsStr = Integer.toString(Float.valueOf(streak).intValue());
Component message = this.killstreaks.messages().get(killsStr);
if (message == null) {
message = this.killstreaks.messages().get("default");
}
message = message.replaceText(builder -> builder.matchLiteral("%player%").once().replacement(killer.name()));
message = message.replaceText(builder -> builder.matchLiteral("%kills%").once().replacement(killsStr));
this.killstreaks.audience().broadcastMessage(message, killer, event.getPlayer());
}
}
}
}
@@ -1,96 +0,0 @@
package org.battleplugins.tracker.feature;
import net.kyori.adventure.text.Component;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.event.TrackerDeathEvent;
import org.battleplugins.tracker.feature.message.MessageAudience;
import org.battleplugins.tracker.util.MessageUtil;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public record Rampage(
boolean enabled,
int rampageTime,
MessageAudience audience,
Map<String, Component> messages
) implements TrackerFeature {
@Override
public void onEnable(BattleTracker battleTracker, Tracker tracker) {
battleTracker.registerListener(tracker, new RampageListener(tracker, this));
}
public static Rampage load(ConfigurationSection section) {
boolean enabled = section.getBoolean("enabled");
if (!enabled) {
return new Rampage(false, 0, MessageAudience.GLOBAL, Map.of());
}
int rampageTime = section.getInt("rampage-time");
MessageAudience audience = MessageAudience.get(section.getString("audience"));
Map<String, Component> messages = new HashMap<>();
ConfigurationSection messagesSection = section.getConfigurationSection("messages");
messagesSection.getKeys(false).forEach(key -> {
if (!messagesSection.isString(key)) {
throw new IllegalArgumentException("Message " + key + " is not a string!");
}
messages.put(key, MessageUtil.deserialize(messagesSection.getString(key)));
});
return new Rampage(true, rampageTime, audience, messages);
}
private static class RampageListener implements Listener {
private final Tracker tracker;
private final Rampage rampage;
private final Map<UUID, Long> lastKill = new HashMap<>();
private final Map<UUID, Integer> killCount = new HashMap<>();
private RampageListener(Tracker tracker, Rampage rampage) {
this.tracker = tracker;
this.rampage = rampage;
}
@EventHandler
public void onTrackerDeath(TrackerDeathEvent event) {
if (!event.getTracker().equals(this.tracker)) {
return;
}
if (!(event.getKiller() instanceof Player killer)) {
return;
}
UUID killerUUID = killer.getUniqueId();
long lastKillTime = this.lastKill.getOrDefault(killerUUID, 0L);
if (System.currentTimeMillis() - lastKillTime > this.rampage.rampageTime() * 1000L) {
this.killCount.put(killerUUID, 0);
}
this.lastKill.put(killerUUID, System.currentTimeMillis());
int killCount = this.killCount.getOrDefault(killerUUID, 0) + 1;
this.killCount.put(killerUUID, killCount);
if (killCount > 1) {
Component message = this.rampage.messages().get(Integer.toString(killCount));
if (message == null) {
message = this.rampage.messages().get("default");
}
message = message.replaceText(builder -> builder.matchLiteral("%player%").once().replacement(killer.name()));
this.rampage.audience().broadcastMessage(message, killer, event.getPlayer());
}
}
}
}
@@ -1,14 +0,0 @@
package org.battleplugins.tracker.feature;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.Tracker;
/**
* Represents a feature that can be loaded by BattleTracker.
*/
public interface TrackerFeature {
boolean enabled();
void onEnable(BattleTracker battleTracker, Tracker tracker);
}
@@ -1,352 +0,0 @@
package org.battleplugins.tracker.feature.combatlog;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.TitlePart;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.feature.Feature;
import org.battleplugins.tracker.message.Messages;
import org.battleplugins.tracker.util.MessageType;
import org.battleplugins.tracker.util.Util;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Tameable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public final class CombatLog implements Feature {
private final boolean enabled;
private final List<String> disabledWorlds;
private final int combatTime;
private final boolean combatSelf;
private final boolean combatEntities;
private final boolean combatPlayers;
private final @Nullable MessageType displayMethod;
private final boolean allowPermissionBypass;
private final Set<EntityType> disabledEntities;
private final List<String> disabledCommands;
private CombatLogListener listener;
public CombatLog(
boolean enabled,
List<String> disabledWorlds,
int combatTime,
boolean combatSelf,
boolean combatEntities,
boolean combatPlayers,
@Nullable MessageType displayMethod,
boolean allowPermissionBypass,
Set<EntityType> disabledEntities,
List<String> disabledCommands
) {
this.enabled = enabled;
this.disabledWorlds = disabledWorlds;
this.combatTime = combatTime;
this.combatSelf = combatSelf;
this.combatEntities = combatEntities;
this.combatPlayers = combatPlayers;
this.displayMethod = displayMethod;
this.allowPermissionBypass = allowPermissionBypass;
this.disabledEntities = disabledEntities;
this.disabledCommands = disabledCommands;
}
@Override
public void onEnable(BattleTracker battleTracker) {
battleTracker.getServer().getPluginManager().registerEvents(this.listener = new CombatLogListener(battleTracker, this), battleTracker);
}
@Override
public void onDisable(BattleTracker battleTracker) {
if (this.listener != null) {
this.listener.onUnload();
}
}
public boolean isInCombat(Player player) {
if (this.listener == null) {
return false;
}
return this.listener.combatTasks.containsKey(player);
}
public static CombatLog load(ConfigurationSection section) {
boolean enabled = section.getBoolean("enabled");
if (!enabled) {
return new CombatLog(false, List.of(), 0, false, false, false, MessageType.ACTION_BAR, false, Set.of(), List.of());
}
List<String> disabledWorlds = section.getStringList("disabled-worlds");
int combatTime = section.getInt("combat-time");
boolean combatSelf = section.getBoolean("combat-self");
boolean combatEntities = section.getBoolean("combat-entities");
boolean combatPlayers = section.getBoolean("combat-players");
MessageType displayMethod = Optional.ofNullable(section.getString("display-method"))
.map(method -> MessageType.valueOf(method.toUpperCase(Locale.ROOT)))
.orElse(null);
boolean allowPermissionBypass = section.getBoolean("allow-permission-bypass");
Set<EntityType> disabledEntities = section.getStringList("disabled-entities")
.stream()
.map(key -> Registry.ENTITY_TYPE.get(NamespacedKey.fromString(key)))
.collect(Collectors.toSet());
List<String> disabledCommands = section.getStringList("disabled-commands");
return new CombatLog(true, disabledWorlds, combatTime, combatSelf, combatEntities, combatPlayers, displayMethod, allowPermissionBypass, disabledEntities, disabledCommands);
}
@Override
public boolean enabled() {
return this.enabled;
}
private static class CombatLogListener implements Listener {
private static final String BOSS_BAR_META_KEY = "combat-log-bar";
private final Map<Player, CombatEntry> combatTasks = new HashMap<>();
private final BattleTracker battleTracker;
private final CombatLog combatLog;
private BukkitTask tickTask;
private CombatLogListener(BattleTracker battleTracker, CombatLog combatLog) {
this.battleTracker = battleTracker;
this.combatLog = combatLog;
if (combatLog.displayMethod == null) {
return;
}
this.tickTask = this.battleTracker.getServer().getScheduler().runTaskTimer(this.battleTracker, this::tick, 0L, 20L);
}
public void onUnload() {
HandlerList.unregisterAll(this);
if (this.tickTask != null) {
this.tickTask.cancel();
}
}
private void tick() {
for (Map.Entry<Player, CombatEntry> entry : this.combatTasks.entrySet()) {
Duration remainingTime = Duration.ofSeconds(this.combatLog.combatTime)
.minus(Duration.ofMillis(System.currentTimeMillis() - entry.getValue().enteredCombat()))
.plusSeconds(1);
if (remainingTime.isZero() || remainingTime.isNegative()) {
continue;
}
Component message = Messages.get("combat-log-remaining-time", Util.toTimeString(remainingTime));
Player player = entry.getKey();
switch (this.combatLog.displayMethod) {
case ACTION_BAR -> player.sendActionBar(message);
case CHAT -> player.sendMessage(message);
case TITLE -> player.sendTitlePart(TitlePart.TITLE, message);
case SUBTITLE -> {
player.sendTitlePart(TitlePart.TITLE, Component.space());
player.sendTitlePart(TitlePart.SUBTITLE, message);
}
case BOSSBAR -> {
player.getMetadata(BOSS_BAR_META_KEY).stream()
.map(MetadataValue::value)
.filter(value -> value instanceof BossBar)
.map(value -> (BossBar) value)
.findFirst()
.ifPresentOrElse(bar -> {
bar.name(message);
float progress = (float) remainingTime.toSeconds() / (float) this.combatLog.combatTime;
bar.progress(progress);
}, () -> {
BossBar bar = BossBar.bossBar(message, 1.0f, BossBar.Color.BLUE, BossBar.Overlay.PROGRESS);
player.showBossBar(bar);
player.setMetadata(BOSS_BAR_META_KEY, new FixedMetadataValue(this.battleTracker, bar));
});
}
}
}
}
@EventHandler(ignoreCancelled = true)
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
if (this.combatLog.disabledWorlds.contains(event.getEntity().getWorld().getName())) {
return;
}
if (!(event.getEntity() instanceof Player player)) {
return;
}
Entity damager = this.getTrueDamager(event, true);
if (damager == null) {
return;
}
if (this.combatLog.disabledEntities.contains(damager.getType())) {
return;
}
if (damager instanceof Player damagerPlayer) {
if (!this.combatLog.combatPlayers) {
return;
}
if (damagerPlayer.equals(player) && !this.combatLog.combatSelf) {
return;
}
} else if (!this.combatLog.combatEntities) {
return;
}
this.enterCombat(player);
if (damager instanceof Player damagerPlayer) {
this.enterCombat(damagerPlayer);
}
}
@EventHandler
public void onDeath(PlayerDeathEvent event) {
if (this.combatLog.disabledWorlds.contains(event.getEntity().getWorld().getName())) {
return;
}
if (event.getPlayer().getLastDamageCause() instanceof EntityDamageByEntityEvent damageEvent) {
Entity damager = this.getTrueDamager(damageEvent, false);
if (damager instanceof Player damagerPlayer && this.combatTasks.containsKey(damagerPlayer)) {
this.exitCombat(damagerPlayer);
}
}
if (this.combatTasks.containsKey(event.getEntity())) {
this.exitCombat(event.getEntity());
}
}
@EventHandler
public void onCommandPreProcess(PlayerCommandPreprocessEvent event) {
if (!this.combatTasks.containsKey(event.getPlayer())) {
return;
}
if (this.combatLog.disabledCommands.stream().anyMatch(cmd -> event.getMessage().startsWith("/" + cmd))) {
event.setCancelled(true);
Messages.send(event.getPlayer(), "combat-log-cannot-run-command");
}
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
CombatEntry entry = this.combatTasks.get(player);
if (entry != null) {
entry.task().cancel();
this.combatTasks.remove(player);
// If player has a combat log bypass, exclude them from
// losing their items on logout
if (this.combatLog.allowPermissionBypass && player.hasPermission("battletracker.combatlog.bypass")) {
return;
}
// Kill the player if they log out in combat
player.setHealth(0);
}
}
private void enterCombat(Player player) {
CombatEntry entry = this.combatTasks.get(player);
if (entry != null) {
entry.task().cancel();
} else {
Messages.send(player, "combat-log-entered-combat");
}
BukkitTask task = this.battleTracker.getServer().getScheduler().runTaskLater(
this.battleTracker,
() -> this.exitCombat(player),
this.combatLog.combatTime * 20L
);
this.combatTasks.put(player, new CombatEntry(task, System.currentTimeMillis()));
}
private void exitCombat(Player player) {
CombatEntry entry = this.combatTasks.remove(player);
if (entry == null) {
return;
}
entry.task().cancel();
Messages.send(player, "combat-log-exited-combat");
// Remove bossbar if it exists
player.getMetadata(BOSS_BAR_META_KEY).stream()
.map(MetadataValue::value)
.filter(value -> value instanceof BossBar)
.map(value -> (BossBar) value)
.findFirst()
.ifPresent(bar -> {
player.hideBossBar(bar);
player.removeMetadata(BOSS_BAR_META_KEY, this.battleTracker);
});
}
private Entity getTrueDamager(EntityDamageByEntityEvent event, boolean checkIgnored) {
Entity damager = event.getDamager();
if (event.getDamager() instanceof Projectile projectile) {
if (checkIgnored && this.combatLog.disabledEntities.contains(projectile.getType())) {
return null;
}
if (projectile.getShooter() instanceof Entity entity) {
damager = entity;
}
}
if (damager instanceof Tameable tameable && tameable.isTamed() && tameable.getOwner() instanceof Entity owner) {
damager = owner;
}
return damager;
}
public record CombatEntry(BukkitTask task, long enteredCombat) {
}
}
}
@@ -1,108 +0,0 @@
package org.battleplugins.tracker.feature.damageindicators;
import net.kyori.adventure.text.Component;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.feature.Feature;
import org.battleplugins.tracker.util.MessageUtil;
import org.battleplugins.tracker.util.Util;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public record DamageIndicators(
boolean enabled,
List<String> disabledWorlds,
Component format
) implements Feature {
@Override
public void onEnable(BattleTracker battleTracker) {
battleTracker.getServer().getPluginManager().registerEvents(new DamageIndicatorListener(battleTracker, this), battleTracker);
}
@Override
public void onDisable(BattleTracker battleTracker) {
HandlerList.getRegisteredListeners(battleTracker).stream()
.filter(listener -> listener.getListener() instanceof DamageIndicatorListener)
.forEach(l -> HandlerList.unregisterAll(l.getListener()));
}
public static DamageIndicators load(ConfigurationSection section) {
boolean enabled = section.getBoolean("enabled");
if (!enabled) {
return new DamageIndicators(false, List.of(), Component.empty());
}
List<String> disabledWorlds = section.getStringList("disabled-worlds");
String format = section.getString("format", "");
return new DamageIndicators(true, disabledWorlds, MessageUtil.MINI_MESSAGE.deserialize(format));
}
private record DamageIndicatorListener(BattleTracker battleTracker,
DamageIndicators damageIndicators) implements Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onEntityDamage(EntityDamageByEntityEvent event) {
if (!(event.getDamager() instanceof Player damager)) {
return;
}
Entity damaged = event.getEntity();
// Item frames, paintings and other non-living entities take no combat damage; skip indicators.
if (!(damaged instanceof LivingEntity)) {
return;
}
double xRand = ThreadLocalRandom.current().nextDouble(0.4, 0.7) / (ThreadLocalRandom.current().nextBoolean() ? 1 : -1);
double yRand = ThreadLocalRandom.current().nextDouble(0.5, 1.5);
double zRand = ThreadLocalRandom.current().nextDouble(0.4, 0.7) / (ThreadLocalRandom.current().nextBoolean() ? 1 : -1);
ArmorStand indicator = damaged.getWorld().spawn(damaged.getLocation().clone().add(xRand, yRand, zRand), ArmorStand.class, entity -> {
entity.customName(this.damageIndicators.format.replaceText(builder ->
builder.matchLiteral("%damage%").once().replacement(Component.text(Util.DAMAGE_FORMAT.format(event.getFinalDamage())))
));
entity.setCustomNameVisible(true);
entity.setMarker(true);
entity.setPersistent(false);
entity.setInvisible(true);
entity.setGravity(false);
entity.setSmall(true);
entity.setVisibleByDefault(false);
});
damager.showEntity(this.battleTracker, indicator);
new BukkitRunnable() {
int counter = 0;
@Override
public void run() {
if (counter++ > 40) {
indicator.remove();
this.cancel();
return;
}
indicator.teleport(indicator.getLocation().clone().subtract(0, 0.06, 0));
if (indicator.isOnGround()) {
indicator.remove();
this.cancel();
}
}
}.runTaskTimer(this.battleTracker, 1, 1);
}
}
}
@@ -1,29 +0,0 @@
package org.battleplugins.tracker.feature.message;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.feature.TrackerFeature;
import org.bukkit.configuration.ConfigurationSection;
public record DeathMessages(
boolean enabled,
MessageAudience audience,
PlayerMessages playerMessages,
EntityMessages entityMessages,
WorldMessages worldMessages
) implements TrackerFeature {
@Override
public void onEnable(BattleTracker battleTracker, Tracker tracker) {
battleTracker.registerListener(tracker, new DeathMessagesListener(this, tracker));
}
public static DeathMessages load(ConfigurationSection section) {
boolean enabled = section.getBoolean("enabled");
MessageAudience audience = MessageAudience.get(section.getString("audience"));
PlayerMessages playerMessages = PlayerMessages.load(section.getConfigurationSection("player"));
EntityMessages entityMessages = EntityMessages.load(section.getConfigurationSection("entity"));
WorldMessages worldMessages = WorldMessages.load(section.getConfigurationSection("world"));
return new DeathMessages(enabled, audience, playerMessages, entityMessages, worldMessages);
}
}
@@ -1,157 +0,0 @@
package org.battleplugins.tracker.feature.message;
import net.kyori.adventure.text.Component;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.event.TrackerDeathEvent;
import org.battleplugins.tracker.event.feature.DeathMessageEvent;
import org.battleplugins.tracker.util.ItemCollection;
import org.battleplugins.tracker.util.Util;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
public class DeathMessagesListener implements Listener {
private final DeathMessages deathMessages;
private final Tracker tracker;
public DeathMessagesListener(DeathMessages deathMessages, Tracker tracker) {
this.deathMessages = deathMessages;
this.tracker = tracker;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onTrackerDeath(TrackerDeathEvent event) {
if (!event.getTracker().equals(this.tracker)) {
return;
}
if (event.getDeathType() == TrackerDeathEvent.DeathType.PLAYER && this.deathMessages.playerMessages().enabled()) {
this.onPlayerDeath(event);
} else if (event.getDeathType() == TrackerDeathEvent.DeathType.ENTITY && this.deathMessages.entityMessages().enabled()) {
this.onEntityDeath(event);
} else if (event.getDeathType() == TrackerDeathEvent.DeathType.WORLD && this.deathMessages.worldMessages().enabled()) {
this.onWorldDeath(event);
}
}
private void onPlayerDeath(TrackerDeathEvent event) {
PlayerMessages messages = this.deathMessages.playerMessages();
Player player = event.getDeathEvent().getPlayer();
// If we've recorded a PVP death, we can assume the killer is a player and make
// a few assumptions about the killer player.
Player killer = (Player) event.getKiller();
ItemStack item = killer.getInventory().getItemInMainHand();
Component deathMessage = null;
for (Map.Entry<ItemCollection, List<Component>> entry : messages.messages().entrySet()) {
if (!entry.getKey().contains(item.getType())) {
continue;
}
Component message = Util.getRandom(entry.getValue());
deathMessage = replace(message, killer, player, item);
}
if (deathMessage == null) {
Component defaultMessage = Util.getRandom(messages.defaultMessages());
deathMessage = replace(defaultMessage, killer, player, item);
}
event.getDeathEvent().deathMessage(null);
broadcastMessage(this.tracker, this.deathMessages.audience(), deathMessage, player, killer);
}
private void onEntityDeath(TrackerDeathEvent event) {
EntityMessages messages = this.deathMessages.entityMessages();
Player player = event.getDeathEvent().getPlayer();
// If we've recorded a PVE death, we can assume the killer is a player and make
// a few assumptions about the killer player.
Entity killer = event.getKiller();
Component deathMessage = null;
for (Map.Entry<EntityType, List<Component>> entry : messages.messages().entrySet()) {
if (entry.getKey() != killer.getType()) {
continue;
}
Component message = Util.getRandom(entry.getValue());
deathMessage = replace(message, player, player, null);
}
if (deathMessage == null) {
Component defaultMessage = Util.getRandom(messages.defaultMessages());
deathMessage = replace(defaultMessage, player, player, null);
}
event.getDeathEvent().deathMessage(null);
broadcastMessage(this.tracker, this.deathMessages.audience(), deathMessage, player, player);
}
private void onWorldDeath(TrackerDeathEvent event) {
WorldMessages messages = this.deathMessages.worldMessages();
Player player = event.getDeathEvent().getPlayer();
EntityDamageEvent lastDamageCause = player.getLastDamageCause();
EntityDamageEvent.DamageCause cause = lastDamageCause == null ? null : lastDamageCause.getCause();
Component deathMessage = null;
for (Map.Entry<EntityDamageEvent.DamageCause, List<Component>> entry : messages.messages().entrySet()) {
if (entry.getKey() != cause) {
continue;
}
Component message = Util.getRandom(entry.getValue());
deathMessage = replace(message, player, player, null);
}
if (deathMessage == null) {
Component defaultMessage = Util.getRandom(messages.defaultMessages());
deathMessage = replace(defaultMessage, player, player, null);
}
event.getDeathEvent().deathMessage(null);
broadcastMessage(this.tracker, this.deathMessages.audience(), deathMessage, player, player);
}
private static void broadcastMessage(Tracker tracker, MessageAudience audience, Component message, Player player, Player target) {
DeathMessageEvent event = new DeathMessageEvent(player, message, tracker);
event.callEvent();
if (event.getDeathMessage().equals(Component.empty())) {
return;
}
audience.broadcastMessage(event.getDeathMessage(), player, target);
}
private static Component replace(Component component, Entity player, Entity target, @Nullable ItemStack item) {
component = component.replaceText(builder -> builder.matchLiteral("%player%").once().replacement(player.name()));
component = component.replaceText(builder -> builder.matchLiteral("%target%").once().replacement(target.name()));
if (item != null) {
Component itemName;
if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) {
itemName = item.getItemMeta().displayName();
} else {
itemName = Component.translatable(item.getType());
}
itemName = itemName.hoverEvent(item.asHoverEvent());
Component finalItemName = itemName;
component = component.replaceText(builder -> builder.matchLiteral("%item%").once().replacement(finalItemName));
}
return component;
}
}
@@ -1,51 +0,0 @@
package org.battleplugins.tracker.feature.message;
import net.kyori.adventure.text.Component;
import org.battleplugins.tracker.util.MessageUtil;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public record EntityMessages(
boolean enabled,
Map<EntityType, List<Component>> messages,
List<Component> defaultMessages
) {
public static EntityMessages load(ConfigurationSection section) {
boolean enabled = section.getBoolean("enabled");
if (!enabled) {
return new EntityMessages(false, Map.of(), List.of());
}
Map<EntityType, List<Component>> messages = new HashMap<>();
List<Component> defaultMessages = section.getStringList("messages.default")
.stream()
.map(MessageUtil::deserialize)
.toList();
ConfigurationSection messagesSection = section.getConfigurationSection("messages");
messagesSection.getKeys(false).forEach(key -> {
if (!messagesSection.isList(key)) {
throw new IllegalArgumentException("Section " + key + " is not a list of messages!");
}
if (key.equalsIgnoreCase("default")) {
return;
}
List<String> messageList = messagesSection.getStringList(key);
messages.put(Registry.ENTITY_TYPE.get(NamespacedKey.fromString(key)), messageList.stream()
.map(MessageUtil::deserialize)
.collect(Collectors.toList()));
});
return new EntityMessages(true, messages, defaultMessages);
}
}
@@ -1,69 +0,0 @@
package org.battleplugins.tracker.feature.message;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* Represents a message audience.
*/
public final class MessageAudience {
private static final Map<String, MessageAudience> AUDIENCES = new HashMap<>();
public static final MessageAudience GLOBAL = new MessageAudience("global", player -> List.copyOf(Bukkit.getOnlinePlayers()));
public static final MessageAudience WORLD = new MessageAudience("world", player -> List.copyOf(player.getWorld().getPlayers()));
public static final MessageAudience LOCAL = new MessageAudience("local", List::of);
private final String name;
private final Function<Player, List<Player>> audienceProvider;
public MessageAudience(String name, Function<Player, List<Player>> audienceProvider) {
this.name = name;
this.audienceProvider = audienceProvider;
AUDIENCES.put(name, this);
}
public String getName() {
return this.name;
}
public List<Player> getAudience(Player player) {
return this.audienceProvider.apply(player);
}
public void broadcastMessage(Component message, Player player, @Nullable Player target) {
List<Player> viewers;
// Special logic for target, since we need the context from
// the death event logic earlier
if (this == MessageAudience.LOCAL) {
if (target == null) {
viewers = List.of(player);
} else {
viewers = List.of(player, target);
}
} else {
viewers = this.getAudience(player);
}
for (Player viewer : viewers) {
viewer.sendMessage(message);
}
}
public static MessageAudience create(String name, Function<Player, List<Player>> audienceProvider) {
return new MessageAudience(name, audienceProvider);
}
@Nullable
public static MessageAudience get(String name) {
return AUDIENCES.get(name);
}
}
@@ -1,49 +0,0 @@
package org.battleplugins.tracker.feature.message;
import net.kyori.adventure.text.Component;
import org.battleplugins.tracker.util.ItemCollection;
import org.battleplugins.tracker.util.MessageUtil;
import org.bukkit.configuration.ConfigurationSection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public record PlayerMessages(
boolean enabled,
Map<ItemCollection, List<Component>> messages,
List<Component> defaultMessages
) {
public static PlayerMessages load(ConfigurationSection section) {
boolean enabled = section.getBoolean("enabled");
if (!enabled) {
return new PlayerMessages(false, Map.of(), List.of());
}
Map<ItemCollection, List<Component>> messages = new HashMap<>();
List<Component> defaultMessages = section.getStringList("messages.default")
.stream()
.map(MessageUtil::deserialize)
.toList();
ConfigurationSection messagesSection = section.getConfigurationSection("messages");
messagesSection.getKeys(false).forEach(key -> {
if (!messagesSection.isList(key)) {
throw new IllegalArgumentException("Section " + key + " is not a list of messages!");
}
if (key.equalsIgnoreCase("default")) {
return;
}
List<String> messageList = messagesSection.getStringList(key);
messages.put(ItemCollection.fromString(key), messageList.stream()
.map(MessageUtil::deserialize)
.collect(Collectors.toList()));
});
return new PlayerMessages(true, messages, defaultMessages);
}
}
@@ -1,82 +0,0 @@
package org.battleplugins.tracker.feature.message;
import net.kyori.adventure.text.Component;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.util.MessageUtil;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.event.entity.EntityDamageEvent;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
public record WorldMessages(
boolean enabled,
Map<EntityDamageEvent.DamageCause, List<Component>> messages,
List<Component> defaultMessages
) {
private static final Map<String, EntityDamageEvent.DamageCause> LEGACY_DAMAGE_CAUSES = Map.of(
"KILL", EntityDamageEvent.DamageCause.SUICIDE
);
public static WorldMessages load(ConfigurationSection section) {
boolean enabled = section.getBoolean("enabled");
if (!enabled) {
return new WorldMessages(false, Map.of(), List.of());
}
Map<EntityDamageEvent.DamageCause, List<Component>> messages = new HashMap<>();
List<Component> defaultMessages = section.getStringList("messages.default")
.stream()
.map(MessageUtil::deserialize)
.toList();
ConfigurationSection messagesSection = section.getConfigurationSection("messages");
messagesSection.getKeys(false).forEach(key -> {
if (!messagesSection.isList(key)) {
throw new IllegalArgumentException("Section " + key + " is not a list of messages!");
}
if (key.equalsIgnoreCase("default")) {
return;
}
EntityDamageEvent.DamageCause cause = parseDamageCause(key);
if (cause == null) {
BattleTracker.getInstance().warn("Skipping unsupported world death message cause '{}' at {}.{}.",
key, messagesSection.getCurrentPath(), key);
return;
}
List<String> messageList = messagesSection.getStringList(key);
if (messages.containsKey(cause)) {
BattleTracker.getInstance().warn("Ignoring duplicate world death message cause '{}' at {}.{}.",
key, messagesSection.getCurrentPath(), key);
return;
}
messages.put(cause, messageList.stream()
.map(MessageUtil::deserialize)
.collect(Collectors.toList()));
});
return new WorldMessages(true, messages, defaultMessages);
}
private static @Nullable EntityDamageEvent.DamageCause parseDamageCause(String key) {
String normalizedKey = key.toUpperCase(Locale.ROOT);
EntityDamageEvent.DamageCause legacyCause = LEGACY_DAMAGE_CAUSES.get(normalizedKey);
if (legacyCause != null) {
return legacyCause;
}
try {
return EntityDamageEvent.DamageCause.valueOf(normalizedKey);
} catch (IllegalArgumentException ignored) {
return null;
}
}
}
@@ -1,129 +0,0 @@
package org.battleplugins.tracker.feature.placeholderapi;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.SqlTracker;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.stat.Record;
import org.battleplugins.tracker.stat.StatType;
import org.battleplugins.tracker.util.Util;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
class BattleTrackerExpansion extends PlaceholderExpansion {
private final BattleTracker battleTracker;
public BattleTrackerExpansion(BattleTracker battleTracker) {
this.battleTracker = battleTracker;
}
@Override
public @NotNull String getIdentifier() {
return "bt";
}
@Override
public @NotNull String getAuthor() {
return "BattlePlugins";
}
@Override
public @NotNull String getVersion() {
return this.battleTracker.getPluginMeta().getVersion();
}
@Override
public @Nullable String onPlaceholderRequest(Player player, @NotNull String params) {
String[] split = params.split("_");
if (split.length < 2) {
return null;
}
String trackerName = split[0];
// The tracker is not tracked or does not exist
Tracker tracker = this.battleTracker.getTracker(trackerName);
if (tracker == null) {
return "";
}
// Gets leaderboard stats (ex: %bt_pvp_top_kills_1%)
if (split[1].equalsIgnoreCase("top")) {
String stat = split[2];
StatType type = StatType.get(stat);
int idxOffset = 0;
if (type == null && split.length >= 4) {
type = StatType.get(split[2] + "_" + split[3]);
idxOffset = 1;
}
if (type != null) {
int place;
try {
place = Integer.parseInt(split[3 + idxOffset]);
} catch (NumberFormatException ex) {
return null; // Not a number at the end of the placeholder
}
List<Record> topRecords = tracker.getTopRecords(place, type).join();
if (topRecords.size() < place) {
return "";
}
Record topRecord = topRecords.get(place - 1);
if (split.length >= 5) {
switch (split[split.length - 1]) {
case "name" -> {
return topRecord.getName();
}
case "uuid" -> {
return topRecord.getId().toString();
}
};
}
return Util.STAT_FORMAT.format(topRecord.getStat(type));
}
}
// Gets player stats (ex: %bt_pvp_kills%)
if (player == null || !player.isOnline()) {
return "";
}
StatType type = StatType.get(split[1]);
if (type == null && split.length >= 3) {
type = StatType.get(split[1] + "_" + split[2]);
}
Record record = this.getRecord(tracker, player);
if (record == null) {
return "";
}
if (type != null) {
return Util.STAT_FORMAT.format(record.getStat(type));
}
return null;
}
private Record getRecord(Tracker tracker, Player player) {
if (tracker instanceof SqlTracker sqlTracker) {
Record record = sqlTracker.getRecords().getCached(player.getUniqueId());
if (record != null) {
return record;
}
// PlaceholderAPI resolves synchronously, but SQL records load asynchronously on join.
tracker.getRecord((OfflinePlayer) player);
return null;
}
return tracker.getRecord(player);
}
}
@@ -1,37 +0,0 @@
package org.battleplugins.tracker.feature.placeholderapi;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.feature.Feature;
import org.bukkit.Bukkit;
public class PlaceholderApiFeature implements Feature {
private final boolean enabled;
private BattleTrackerExpansion expansion;
public PlaceholderApiFeature() {
this.enabled = Bukkit.getServer().getPluginManager().getPlugin("PlaceholderAPI") != null;
}
@Override
public boolean enabled() {
return this.enabled;
}
@Override
public void onEnable(BattleTracker battleTracker) {
if (!this.enabled) {
return;
}
this.expansion = new BattleTrackerExpansion(battleTracker);
this.expansion.register();
}
@Override
public void onDisable(BattleTracker battleTracker) {
if (this.expansion != null) {
this.expansion.unregister();
}
}
}
@@ -1,145 +0,0 @@
package org.battleplugins.tracker.feature.recap;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class BattleRecap {
private final List<RecapEntry> entries = new ArrayList<>();
private final double startingHealth;
private final Instant createTime;
private final String recapOwner;
private PlayerInventory inventory;
private ItemStack[] inventorySnapshot;
public BattleRecap(Player player) {
this(player.getInventory(), player.getHealth(), player.getName());
}
public BattleRecap(PlayerInventory inventory, double startingHealth, String recapOwner) {
this.inventory = inventory;
this.startingHealth = startingHealth;
this.recapOwner = recapOwner;
this.createTime = Instant.now();
}
public void record(RecapEntry entry) {
this.entries.add(entry);
}
public Optional<RecapEntry> lastEntry() {
return Optional.ofNullable(this.getLastEntry());
}
@Nullable
public RecapEntry getLastEntry() {
if (this.entries.isEmpty()) {
return null;
}
return this.entries.get(this.entries.size() - 1);
}
public List<RecapEntry> getEntries() {
return List.copyOf(this.entries);
}
public List<RecapEntry> getCombinedEntries() {
List<RecapEntry> entries = new ArrayList<>();
RecapEntry lastEntry = null;
for (RecapEntry entry : this.entries) {
// We only want to combine the health gain entries, since they can get excessive
if (entry.kind() == RecapEntry.Kind.LOSS) {
if (lastEntry != null) {
entries.add(lastEntry);
lastEntry = null;
}
entries.add(entry);
continue;
}
if (lastEntry != null) {
if (lastEntry.kind() == RecapEntry.Kind.GAIN) {
lastEntry = lastEntry.toBuilder()
.amount(lastEntry.amount() + entry.amount())
.logTime(entry.logTime())
.build();
} else {
lastEntry = entry;
}
} else {
lastEntry = entry;
}
}
if (lastEntry != null) {
entries.add(lastEntry);
}
return entries;
}
public double getStartingHealth() {
return this.startingHealth;
}
public String getRecapOwner() {
return this.recapOwner;
}
public Instant getCreateTime() {
return this.createTime;
}
public void markDeath() {
this.inventorySnapshot = this.getInventorySnapshot();
this.inventory = null;
}
public ItemStack[] getInventorySnapshot() {
if (this.inventorySnapshot != null) {
return this.inventorySnapshot;
}
ItemStack[] contents = this.inventory.getStorageContents();
// Size is contents + armor + main & offhand
ItemStack[] snapshot = new ItemStack[contents.length + 6];
// Main inventory contents
for (int i = 0; i < contents.length; i++) {
ItemStack item = contents[i];
if (item != null) {
snapshot[i] = item.clone();
}
}
// Armor contents
snapshot[contents.length] = nullify(this.inventory.getHelmet());
snapshot[contents.length + 1] = nullify(this.inventory.getChestplate());
snapshot[contents.length + 2] = nullify(this.inventory.getLeggings());
snapshot[contents.length + 3] = nullify(this.inventory.getBoots());
// Hand + offhand contents
snapshot[contents.length + 4] = nullify(this.inventory.getItemInMainHand());
snapshot[contents.length + 5] = nullify(this.inventory.getItemInOffHand());
return snapshot;
}
private static ItemStack nullify(ItemStack item) {
if (item != null && item.getType() == Material.AIR) {
return null;
}
return item;
}
}
@@ -1,13 +0,0 @@
package org.battleplugins.tracker.feature.recap;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
public record EntitySnapshot(EntityType type, Component displayedName) {
public EntitySnapshot(Entity entity) {
this(entity.getType(), entity instanceof Player player ? player.name() : Component.translatable(entity.getType()));
}
}
@@ -1,295 +0,0 @@
package org.battleplugins.tracker.feature.recap;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.event.ClickCallback;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.TrackedDataType;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.event.feature.DeathMessageEvent;
import org.battleplugins.tracker.feature.TrackerFeature;
import org.battleplugins.tracker.message.Messages;
import org.battleplugins.tracker.util.TrackerInventoryHolder;
import org.battleplugins.tracker.util.Util;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
public record Recap(
boolean enabled,
DisplayContent displayContent,
boolean hoverRecap,
Map<UUID, BattleRecap> previousRecaps,
Map<UUID, BattleRecap> recaps
) implements TrackerFeature {
public Recap(boolean enabled, DisplayContent displayContent, boolean hoverRecap) {
this(enabled, displayContent, hoverRecap, new HashMap<>(), new HashMap<>());
}
public static Recap load(ConfigurationSection section) {
return new Recap(
section.getBoolean("enabled", true),
DisplayContent.valueOf(section.getString("display-content", "ALL").toUpperCase(Locale.ROOT)),
section.getBoolean("hover-recap", true)
);
}
@Override
public void onEnable(BattleTracker battleTracker, Tracker tracker) {
battleTracker.registerListener(tracker, new RecapListener(tracker, this));
}
public BattleRecap getRecap(Player player) {
return this.recaps.computeIfAbsent(player.getUniqueId(), uuid -> new BattleRecap(player));
}
public Optional<BattleRecap> previousRecap(Player player) {
return Optional.ofNullable(this.getPreviousRecap(player));
}
@Nullable
public BattleRecap getPreviousRecap(Player player) {
return this.previousRecaps.get(player.getUniqueId());
}
@NotNull
public static ItemStack getRecapItem(Duration deathDuration, BattleRecap battleRecap) {
ItemStack recapItem = new ItemStack(Material.BOOK);
recapItem.editMeta(meta -> {
meta.displayName(Messages.get("recap-info").decoration(TextDecoration.ITALIC, false));
List<Component> lore = new ArrayList<>();
lore.add(Messages.get("recap-death-time", Util.toTimeStringShort(deathDuration)));
lore.add(Messages.get("recap-starting-health", Util.formatHealth(battleRecap.getStartingHealth(), false)));
lore.add(Component.empty());
lore.add(Messages.get("recap-damage-log"));
processRecapEntry(battleRecap.getCombinedEntries(), 10, true, lore::add);
lore.add(Component.empty());
lore.add(Messages.get("recap-click-for-more"));
// Make sure the lore lines are not italic
ListIterator<Component> itr = lore.listIterator();
while (itr.hasNext()) {
itr.set(itr.next().decoration(TextDecoration.ITALIC, false));
}
meta.lore(lore);
meta.getPersistentDataContainer().set(TrackerInventoryHolder.RECAP_KEY, PersistentDataType.BOOLEAN, true);
});
return recapItem;
}
public static void processRecapEntry(List<RecapEntry> entries, int max, boolean showItem, Consumer<Component> componentConsumer) {
for (int i = entries.size() - 1; i >= Math.max(0, entries.size() - max); i--) {
RecapEntry entry = entries.get(i);
ItemStack item = entry.itemUsed();
// TODO: Translation support
Component cause = null;
if (entry.causingEntity() != null) {
cause = entry.causingEntity().displayedName();
if (entry.sourceEntity() != null && !entry.sourceEntity().equals(entry.causingEntity())) {
cause = entry.sourceEntity().displayedName()
.append(Component.text(" ("))
.append(entry.causingEntity().displayedName())
.append(Component.text(")"));
} else if (showItem && item != null && entry.causingEntity().type() == EntityType.PLAYER) {
Component itemName;
if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) {
itemName = item.getItemMeta().displayName();
} else {
itemName = Component.translatable(item.getType());
}
cause = cause.append(Component.text(" (").append(itemName).append(Component.text(")")));
}
}
if (cause == null && entry.damageCause() != null) {
cause = Component.text(Util.capitalize(entry.damageCause().name().toLowerCase(Locale.ROOT).replace("_", " ")));
}
boolean loss = entry.kind() == RecapEntry.Kind.LOSS;
Component recapLog = Messages.get("recap-log", Map.of(
"health", Component.text(Util.formatHealth(entry.amount(), loss), loss ? NamedTextColor.RED : NamedTextColor.GREEN),
"time", Util.toTimeStringShort(Duration.between(entry.logTime(), Instant.now().plusSeconds(1))),
"damage_cause", cause == null ? Component.empty() : cause
));
componentConsumer.accept(recapLog);
}
}
public void showRecap(Audience audience, Tracker tracker, BattleRecap battleRecap) {
Duration deathDuration = Duration.between(battleRecap.getLastEntry().logTime(), Instant.now());
ItemStack recapItem = Recap.getRecapItem(deathDuration, battleRecap);
Inventory inventory = TrackerInventoryHolder.create(TrackerInventoryHolder.RECAP_KEY, tracker, 54, Messages.get("recap"), handler -> {
handler.onClick(TrackerInventoryHolder.RECAP_KEY, () -> {
if (tracker.tracksData(TrackedDataType.PVP)) {
RecapRoundup.recapPlayer(audience, battleRecap);
} else if (tracker.tracksData(TrackedDataType.PVE)) {
RecapRoundup.recapEntity(audience, battleRecap);
} else {
RecapRoundup.recapSource(audience, battleRecap);
}
RecapRoundup.sendFooter(audience, tracker, battleRecap);
});
});
Recap.DisplayContent displayContent = this.displayContent;
if (displayContent == Recap.DisplayContent.ALL) {
if (!(audience instanceof Player senderPlayer)) {
Messages.send(audience, "command-must-be-player");
return;
}
ItemStack[] snapshot = battleRecap.getInventorySnapshot();
for (int i = 0; i < snapshot.length; i++) {
inventory.setItem(i, snapshot[i]);
}
inventory.setItem(52, recapItem);
senderPlayer.openInventory(inventory);
} else if (displayContent == Recap.DisplayContent.ARMOR) {
if (!(audience instanceof Player senderPlayer)) {
Messages.send(audience, "command-must-be-player");
return;
}
ItemStack empty = new ItemStack(Material.BONE);
empty.editMeta(meta -> meta.displayName(Component.empty()));
ItemStack[] snapshot = battleRecap.getInventorySnapshot();
inventory.setItem(13, Optional.ofNullable(snapshot[36]).orElse(empty));
inventory.setItem(22, Optional.ofNullable(snapshot[37]).orElse(empty));
inventory.setItem(31, Optional.ofNullable(snapshot[38]).orElse(empty));
inventory.setItem(40, Optional.ofNullable(snapshot[39]).orElse(empty));
inventory.setItem(21, Optional.ofNullable(snapshot[40]).orElse(empty));
inventory.setItem(23, Optional.ofNullable(snapshot[41]).orElse(empty));
inventory.setItem(25, recapItem);
senderPlayer.openInventory(inventory);
} else if (displayContent == Recap.DisplayContent.RECAP) {
if (tracker.tracksData(TrackedDataType.PVP)) {
RecapRoundup.recapPlayer(audience, battleRecap);
} else if (tracker.tracksData(TrackedDataType.PVE)) {
RecapRoundup.recapEntity(audience, battleRecap);
} else {
RecapRoundup.recapSource(audience, battleRecap);
}
RecapRoundup.sendFooter(audience, tracker, battleRecap);
}
}
public enum DisplayContent {
ALL,
ARMOR,
RECAP
}
private record RecapListener(Tracker tracker, Recap recap) implements Listener {
@EventHandler
public void onQuit(PlayerQuitEvent event) {
this.recap.recaps.remove(event.getPlayer().getUniqueId());
}
@EventHandler
public void onDeath(PlayerDeathEvent event) {
Player player = event.getEntity();
BattleRecap recap = this.recap.getRecap(player);
recap.markDeath();
this.recap.previousRecaps.put(player.getUniqueId(), recap);
this.recap.recaps.remove(player.getUniqueId());
}
@EventHandler
public void onHealthRegain(EntityRegainHealthEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
BattleRecap recap = this.recap.getRecap(player);
recap.record(RecapEntry.builder()
.amount(event.getAmount())
.logTime(Instant.now())
.kind(RecapEntry.Kind.GAIN)
.build());
}
@EventHandler
public void onDeathMessage(DeathMessageEvent event) {
if (!this.tracker.equals(event.getTracker())) {
return;
}
if (!this.recap.hoverRecap) {
return;
}
Component deathMessage = event.getDeathMessage();
if (deathMessage == null) {
return;
}
BattleRecap recap = this.recap.previousRecaps.get(event.getPlayer().getUniqueId());
if (recap == null) {
return;
}
List<Component> lines = new ArrayList<>();
processRecapEntry(recap.getCombinedEntries(), 5, false, lines::add);
deathMessage = deathMessage.hoverEvent(
HoverEvent.showText(Messages.get("recap-damage-log")
.append(Component.newline())
.append(Component.join(JoinConfiguration.newlines(), lines))
.append(Component.newline())
.append(Component.newline())
.append(Messages.get("recap-click-for-more"))
));
deathMessage = deathMessage.clickEvent(ClickEvent.callback(audience -> this.recap.showRecap(audience, this.tracker, recap), builder -> builder.lifetime(Duration.ofMinutes(5)).uses(ClickCallback.UNLIMITED_USES)));
event.setDeathMessage(deathMessage);
}
}
}
@@ -1,102 +0,0 @@
package org.battleplugins.tracker.feature.recap;
import org.bukkit.entity.Entity;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
public record RecapEntry(
@Nullable EntityDamageEvent.DamageCause damageCause,
double amount,
@Nullable EntitySnapshot causingEntity,
@Nullable EntitySnapshot sourceEntity,
@Nullable ItemStack itemUsed,
Instant logTime,
Kind kind
) {
public RecapEntry(EntityDamageEvent.DamageCause damageCause, double amount, Entity causingEntity, Entity sourceEntity, ItemStack itemUsed, Instant logTime, Kind kind) {
this(damageCause, amount, new EntitySnapshot(causingEntity), new EntitySnapshot(sourceEntity), itemUsed, logTime, kind);
}
public Builder toBuilder() {
return new Builder()
.damageCause(this.damageCause)
.amount(this.amount)
.causingEntity(this.causingEntity)
.sourceEntity(this.sourceEntity)
.itemUsed(this.itemUsed)
.logTime(this.logTime)
.kind(this.kind);
}
public enum Kind {
GAIN,
LOSS
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private EntityDamageEvent.DamageCause damageCause;
private double amount;
private EntitySnapshot causingEntity;
private EntitySnapshot sourceEntity;
private ItemStack itemUsed;
private Instant logTime;
private Kind kind = Kind.LOSS;
public Builder damageCause(EntityDamageEvent.DamageCause cause) {
this.damageCause = cause;
return this;
}
public Builder amount(double amount) {
this.amount = amount;
return this;
}
public Builder causingEntity(Entity causingEntity) {
this.causingEntity = new EntitySnapshot(causingEntity);
return this;
}
public Builder causingEntity(EntitySnapshot causingEntity) {
this.causingEntity = causingEntity;
return this;
}
public Builder sourceEntity(Entity sourceEntity) {
this.sourceEntity = new EntitySnapshot(sourceEntity);
return this;
}
public Builder sourceEntity(EntitySnapshot sourceEntity) {
this.sourceEntity = sourceEntity;
return this;
}
public Builder itemUsed(ItemStack itemUsed) {
this.itemUsed = itemUsed;
return this;
}
public Builder logTime(Instant logTime) {
this.logTime = logTime;
return this;
}
public Builder kind(Kind kind) {
this.kind = kind;
return this;
}
public RecapEntry build() {
return new RecapEntry(this.damageCause, this.amount, this.causingEntity, this.sourceEntity, this.itemUsed, this.logTime, this.kind);
}
}
}
@@ -1,232 +0,0 @@
package org.battleplugins.tracker.feature.recap;
import it.unimi.dsi.fastutil.Hash;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import org.battleplugins.tracker.TrackedDataType;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.message.Messages;
import org.battleplugins.tracker.util.Util;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.inventory.ItemStack;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class RecapRoundup {
public static void recapItem(Audience sender, BattleRecap recap) {
Messages.send(sender, "header", Messages.getPlain("recap-damage-item"));
Map<ItemStack, List<RecapEntry>> entriesByStack = new Object2ObjectOpenCustomHashMap<>(new Hash.Strategy<>() {
@Override
public int hashCode(ItemStack o) {
if (o.getType() == Material.AIR) {
return o.getType().hashCode();
}
int hash = 1;
hash = hash * 31 + o.getType().hashCode();
hash = hash * 31 + (o.hasItemMeta() ? (o.getItemMeta().hashCode()) : 0);
return hash;
}
@Override
public boolean equals(ItemStack a, ItemStack b) {
if (a == null && b == null) {
return true;
}
if (a == null && b.getType() == Material.AIR) {
return true;
}
if (a != null && a.getType() == Material.AIR && b == null) {
return true;
}
if (a != null && b != null && a.getType() == Material.AIR && b.getType() == Material.AIR) {
return true;
}
return a != null && a.isSimilar(b);
}
});
for (RecapEntry entry : recap.getEntries()) {
if (entry.itemUsed() == null || entry.kind() != RecapEntry.Kind.LOSS) {
continue;
}
entriesByStack.computeIfAbsent(entry.itemUsed(), k -> new ArrayList<>()).add(entry);
}
for (Map.Entry<ItemStack, List<RecapEntry>> entry : entriesByStack.entrySet()) {
double damage = entry.getValue().stream().mapToDouble(RecapEntry::amount).sum();
if (damage == 0) {
continue;
}
TranslatableComponent itemComponent = Component.translatable(entry.getKey());
if (entry.getKey().getType() != Material.AIR) {
itemComponent = itemComponent.hoverEvent(entry.getKey());
}
Messages.send(sender, "recap-log-item", Map.of(
"item", itemComponent,
"hits", Integer.toString(entry.getValue().size()),
"damage", Util.HEALTH_FORMAT.format(damage)
));
}
Messages.send(sender, "recap-log-general", Map.of(
"time", Util.toTimeStringShort(Duration.between(recap.getCreateTime(), recap.getLastEntry().logTime())),
"health", Util.formatHealth(recap.getEntries().stream().filter(entry -> entry.kind() == RecapEntry.Kind.GAIN).mapToDouble(RecapEntry::amount).sum(), false),
"damage", Util.HEALTH_FORMAT.format(recap.getEntries().stream().filter(entry -> entry.kind() == RecapEntry.Kind.LOSS).mapToDouble(RecapEntry::amount).sum())
));
}
public static void recapPlayer(Audience sender, BattleRecap recap) {
Messages.send(sender, "header", Messages.getPlain("recap-damage-player"));
Map<EntitySnapshot, List<RecapEntry>> entriesByStack = new HashMap<>();
for (RecapEntry entry : recap.getEntries()) {
if (entry.kind() != RecapEntry.Kind.LOSS) {
continue;
}
if (entry.causingEntity() == null) {
continue;
}
entriesByStack.computeIfAbsent(entry.causingEntity(), k -> new ArrayList<>()).add(entry);
}
for (Map.Entry<EntitySnapshot, List<RecapEntry>> entry : entriesByStack.entrySet()) {
if (entry.getKey().type() != EntityType.PLAYER) {
continue;
}
double damage = entry.getValue().stream().mapToDouble(RecapEntry::amount).sum();
if (damage == 0) {
continue;
}
Messages.send(sender, "recap-log-player", Map.of(
"player", entry.getKey().displayedName(),
"hits", Integer.toString(entry.getValue().size()),
"damage", Util.HEALTH_FORMAT.format(damage)
));
}
Messages.send(sender, "recap-log-general", Map.of(
"time", Util.toTimeStringShort(Duration.between(recap.getCreateTime(), recap.getLastEntry().logTime())),
"health", Util.formatHealth(recap.getEntries().stream().filter(entry -> entry.kind() == RecapEntry.Kind.GAIN).mapToDouble(RecapEntry::amount).sum(), false),
"damage", Util.HEALTH_FORMAT.format(recap.getEntries().stream().filter(entry -> entry.kind() == RecapEntry.Kind.LOSS).mapToDouble(RecapEntry::amount).sum())
));
}
public static void recapEntity(Audience sender, BattleRecap recap) {
Messages.send(sender, "header", Messages.getPlain("recap-damage-entity"));
Map<EntitySnapshot, List<RecapEntry>> entriesByStack = new HashMap<>();
for (RecapEntry entry : recap.getEntries()) {
if (entry.kind() != RecapEntry.Kind.LOSS) {
continue;
}
if (entry.causingEntity() == null) {
continue;
}
entriesByStack.computeIfAbsent(entry.causingEntity(), k -> new ArrayList<>()).add(entry);
}
for (Map.Entry<EntitySnapshot, List<RecapEntry>> entry : entriesByStack.entrySet()) {
double damage = entry.getValue().stream().mapToDouble(RecapEntry::amount).sum();
if (damage == 0) {
continue;
}
Component entityComponent = entry.getKey().displayedName();
if (entry.getKey().type() == EntityType.PLAYER) {
entityComponent = entityComponent.append(Component.text(" ("))
.append(Component.translatable(entry.getKey().type()))
.append(Component.text(")"));
}
Messages.send(sender, "recap-log-entity", Map.of(
"entity", entityComponent,
"hits", Integer.toString(entry.getValue().size()),
"damage", Util.HEALTH_FORMAT.format(damage)
));
}
Messages.send(sender, "recap-log-general", Map.of(
"time", Util.toTimeStringShort(Duration.between(recap.getCreateTime(), recap.getLastEntry().logTime())),
"health", Util.formatHealth(recap.getEntries().stream().filter(entry -> entry.kind() == RecapEntry.Kind.GAIN).mapToDouble(RecapEntry::amount).sum(), false),
"damage", Util.HEALTH_FORMAT.format(recap.getEntries().stream().filter(entry -> entry.kind() == RecapEntry.Kind.LOSS).mapToDouble(RecapEntry::amount).sum())
));
}
public static void recapSource(Audience sender, BattleRecap recap) {
Messages.send(sender, "header", Messages.getPlain("recap-damage-cause"));
Map<EntityDamageEvent.DamageCause, List<RecapEntry>> entriesByStack = new HashMap<>();
for (RecapEntry entry : recap.getEntries()) {
if (entry.kind() != RecapEntry.Kind.LOSS) {
continue;
}
if (entry.damageCause() == null) {
continue;
}
entriesByStack.computeIfAbsent(entry.damageCause(), k -> new ArrayList<>()).add(entry);
}
for (Map.Entry<EntityDamageEvent.DamageCause, List<RecapEntry>> entry : entriesByStack.entrySet()) {
double damage = entry.getValue().stream().mapToDouble(RecapEntry::amount).sum();
if (damage == 0) {
continue;
}
// TODO: Translation support
Component causeComponent = Component.text(Util.capitalize(entry.getKey().name().toLowerCase(Locale.ROOT).replace("_", " ")));
Messages.send(sender, "recap-log-cause", Map.of(
"cause", causeComponent,
"hits", Integer.toString(entry.getValue().size()),
"damage", Util.HEALTH_FORMAT.format(damage)
));
}
Messages.send(sender, "recap-log-general", Map.of(
"time", Util.toTimeStringShort(Duration.between(recap.getCreateTime(), recap.getLastEntry().logTime())),
"health", Util.formatHealth(recap.getEntries().stream().filter(entry -> entry.kind() == RecapEntry.Kind.GAIN).mapToDouble(RecapEntry::amount).sum(), false),
"damage", Util.HEALTH_FORMAT.format(recap.getEntries().stream().filter(entry -> entry.kind() == RecapEntry.Kind.LOSS).mapToDouble(RecapEntry::amount).sum())
));
}
public static void sendFooter(Audience audience, Tracker tracker, BattleRecap recap) {
if (tracker.tracksData(TrackedDataType.PVP)) {
Messages.send(audience, "recap-footer-pvp", Map.of(
"tracker", tracker.getName().toLowerCase(Locale.ROOT),
"player", recap.getRecapOwner()
));
} else {
Messages.send(audience, "recap-footer-pve", Map.of(
"tracker", tracker.getName().toLowerCase(Locale.ROOT),
"player", recap.getRecapOwner()
));
}
}
}
@@ -1,224 +0,0 @@
package org.battleplugins.tracker.listener;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.battleplugins.tracker.TrackedDataType;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.event.TrackerDeathEvent;
import org.battleplugins.tracker.feature.recap.Recap;
import org.battleplugins.tracker.feature.recap.RecapEntry;
import org.battleplugins.tracker.stat.Record;
import org.battleplugins.tracker.stat.StatType;
import org.battleplugins.tracker.util.DuelsHook;
import org.battleplugins.tracker.util.SpleefHook;
import org.battleplugins.tracker.util.Util;
import org.bukkit.entity.AnimalTamer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Tameable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.inventory.ItemStack;
import java.time.Instant;
import java.util.HashMap;
import java.util.Locale;
import java.util.UUID;
public class PvEListener implements Listener {
private final Tracker tracker;
public PvEListener(Tracker tracker) {
this.tracker = tracker;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onDeath(PlayerDeathEvent event) {
Player killed = event.getEntity();
if (this.tracker.getDisabledWorlds().contains(killed.getWorld().getName())) {
return;
}
// Minigame PvE/world deaths must not count; use pre-captured markers because match state can change during death.
if (DuelsHook.isDuelDeath(killed) || SpleefHook.isSpleefDeath(killed)) {
return;
}
TrackedDataType dataType = TrackedDataType.PVE;
EntityDamageEvent lastDamageCause = killed.getLastDamageCause();
Entity killer = null;
String killerName;
if (lastDamageCause instanceof EntityDamageByEntityEvent damageEvent) {
EntityType killerType;
killer = damageEvent.getDamager();
if (killer instanceof Player) {
return;
}
killerType = killer.getType();
if (killer instanceof Projectile projectile && projectile.getShooter() instanceof Entity entity) {
if (projectile.getShooter() instanceof Player) {
return;
}
killer = entity;
killerType = projectile.getType();
}
if (killer instanceof Tameable tameable && tameable.isTamed()) {
return; // only players can tame animals
}
killerName = PlainTextComponentSerializer.plainText().serialize(Component.translatable(killerType));
} else {
dataType = TrackedDataType.WORLD;
// TODO: Translation support, and use new damage API when it is
// ready for widespread adoption
if (lastDamageCause == null) {
killerName = "Unknown";
} else {
killerName = Util.capitalize(lastDamageCause.getCause().name().toLowerCase(Locale.ROOT).replace("_", " "));
}
}
if (!this.tracker.tracksData(dataType)) {
return;
}
Record killerRecord = this.tracker.getOrCreateRecord(killed);
if (killerRecord.isTracking()) {
this.tracker.incrementValue(StatType.DEATHS, killed);
}
Record record = new Record(this.tracker, UUID.randomUUID(), killerName, new HashMap<>());
record.setRating(this.tracker.getRatingCalculator().getDefaultRating());
this.tracker.getRatingCalculator().updateRating(record, killerRecord, false);
new TrackerDeathEvent(this.tracker, dataType == TrackedDataType.PVE ? TrackerDeathEvent.DeathType.ENTITY : TrackerDeathEvent.DeathType.WORLD, killer, event).callEvent();
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onEntityDeath(EntityDeathEvent event) {
Entity killed = event.getEntity();
if (this.tracker.getDisabledWorlds().contains(killed.getWorld().getName())) {
return;
}
if (killed instanceof Player) {
return;
}
if (!this.tracker.tracksData(TrackedDataType.PVE)) {
return;
}
if (!(killed.getLastDamageCause() instanceof EntityDamageByEntityEvent lastDamageCause)) {
return;
}
if (!(lastDamageCause.getDamager() instanceof Player killer)) {
return;
}
// A minigame player killing a mob mid-match must not be tracked.
if (DuelsHook.isInMatch(killer) || SpleefHook.isInSpleef(killer)) {
return;
}
Record killerRecord = this.tracker.getOrCreateRecord(killer);
if (killerRecord.isTracking()) {
this.tracker.incrementValue(StatType.KILLS, killer);
}
String killerName = PlainTextComponentSerializer.plainText().serialize(Component.translatable(killer.getType()));
Record record = new Record(this.tracker, UUID.randomUUID(), killerName, new HashMap<>());
record.setRating(this.tracker.getRatingCalculator().getDefaultRating());
this.tracker.getRatingCalculator().updateRating(killerRecord, record, false);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEntityDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
if (this.tracker.getDisabledWorlds().contains(event.getEntity().getWorld().getName())) {
return;
}
Recap recap = this.tracker.getFeature(Recap.class);
if (recap == null) {
return;
}
// Don't build a damage recap from a minigame (after cheap exits to keep reflection off the common path).
if (DuelsHook.isInMatch(player) || SpleefHook.isInSpleef(player)) {
return;
}
if (!(event instanceof EntityDamageByEntityEvent entityEvent)) {
if (!this.tracker.tracksData(TrackedDataType.WORLD)) {
return;
}
recap.getRecap(player).record(RecapEntry.builder()
.damageCause(event.getCause())
.amount(event.getFinalDamage())
.logTime(Instant.now())
.build()
);
return;
}
if (!this.tracker.tracksData(TrackedDataType.PVE)) {
return;
}
Entity causingEntity = entityEvent.getDamager();
Entity sourceEntity = causingEntity;
if (causingEntity instanceof Projectile proj) {
if (proj.getShooter() instanceof Entity shooter) {
sourceEntity = shooter;
}
}
if (causingEntity instanceof Tameable tameable && tameable.isTamed()) {
AnimalTamer owner = tameable.getOwner();
if (owner instanceof Entity entity) {
sourceEntity = entity;
}
}
if (sourceEntity instanceof Player) {
return;
}
ItemStack itemUsed = null;
if (causingEntity instanceof LivingEntity livingEntity && livingEntity.getEquipment() != null) {
itemUsed = livingEntity.getEquipment().getItemInMainHand();
}
recap.getRecap(player).record(RecapEntry.builder()
.damageCause(event.getCause())
.causingEntity(causingEntity)
.sourceEntity(sourceEntity)
.amount(event.getFinalDamage())
.itemUsed(itemUsed)
.logTime(Instant.now())
.build()
);
}
}
@@ -1,170 +0,0 @@
package org.battleplugins.tracker.listener;
import org.battleplugins.tracker.TrackedDataType;
import org.battleplugins.tracker.Tracker;
import org.battleplugins.tracker.event.TrackerDeathEvent;
import org.battleplugins.tracker.feature.recap.Recap;
import org.battleplugins.tracker.feature.recap.RecapEntry;
import org.battleplugins.tracker.stat.Record;
import org.battleplugins.tracker.stat.StatType;
import org.battleplugins.tracker.stat.TallyEntry;
import org.battleplugins.tracker.util.DuelsHook;
import org.battleplugins.tracker.util.SpleefHook;
import org.bukkit.entity.AnimalTamer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Tameable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.inventory.ItemStack;
import java.time.Instant;
public class PvPListener implements Listener {
private final Tracker tracker;
public PvPListener(Tracker tracker) {
this.tracker = tracker;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onDeath(PlayerDeathEvent event) {
if (!this.tracker.tracksData(TrackedDataType.PVP)) {
return;
}
Player killed = event.getEntity();
if (this.tracker.getDisabledWorlds().contains(killed.getWorld().getName())) {
return;
}
// Minigame deaths must not be tracked; use pre-captured markers because match state can change during death.
if (DuelsHook.isDuelDeath(killed) || SpleefHook.isSpleefDeath(killed)) {
return;
}
EntityDamageEvent lastDamageCause = killed.getLastDamageCause();
Player killer = null;
if (lastDamageCause instanceof EntityDamageByEntityEvent damageByEntityEvent) {
Entity damager = damageByEntityEvent.getDamager();
if (damager instanceof Player) {
killer = (Player) damager;
}
if (damager instanceof Projectile proj) {
if (proj.getShooter() instanceof Player player) {
killer = player;
}
}
if (damager instanceof Tameable tameable && tameable.isTamed()) {
AnimalTamer owner = tameable.getOwner();
if (owner instanceof Player player) {
killer = player;
}
}
}
if (killer == null) {
return;
}
if (DuelsHook.isInMatch(killer) || SpleefHook.isInSpleef(killer)) {
return;
}
Record killerRecord = this.tracker.getOrCreateRecord(killer);
Record killedRecord = this.tracker.getOrCreateRecord(killed);
if (killerRecord.isTracking()) {
this.tracker.incrementValue(StatType.KILLS, killer);
}
if (killedRecord.isTracking()) {
this.tracker.incrementValue(StatType.DEATHS, killed);
}
this.tracker.updateRating(killer, killed, false);
new TrackerDeathEvent(this.tracker, TrackerDeathEvent.DeathType.PLAYER, killer, event).callEvent();
Player finalKiller = killer;
this.tracker.getOrCreateVersusTally(killer, killed).whenComplete((versusTally, e) -> {
// The format is killer : killed : stat1 : stat2 ....
// If the killer is in place of the killed, we need to swap the values
boolean addToKills = !versusTally.id2().equals(finalKiller.getUniqueId());
if (addToKills) {
this.tracker.modifyTally(versusTally, ctx -> ctx.recordStat(StatType.KILLS, versusTally.getStat(StatType.KILLS) + 1));
} else {
this.tracker.modifyTally(versusTally, ctx -> ctx.recordStat(StatType.DEATHS, versusTally.getStat(StatType.DEATHS) + 1));
}
// Record a tally entry at the current timestamp
TallyEntry entry = new TallyEntry(finalKiller, killed, false, Instant.now());
this.tracker.recordTallyEntry(entry);
});
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEntityDamage(EntityDamageByEntityEvent event) {
if (!this.tracker.tracksData(TrackedDataType.PVP)) {
return;
}
if (!(event.getEntity() instanceof Player damaged)) {
return;
}
if (this.tracker.getDisabledWorlds().contains(damaged.getWorld().getName())) {
return;
}
Recap recap = this.tracker.getFeature(Recap.class);
if (recap == null) {
return;
}
// Don't build a damage recap from a minigame (after cheap exits to keep reflection off the common path).
if (DuelsHook.isInMatch(damaged) || SpleefHook.isInSpleef(damaged)) {
return;
}
Entity sourceEntity = event.getDamager();
if (event.getDamager() instanceof Projectile proj) {
if (proj.getShooter() instanceof Player shooter) {
sourceEntity = shooter;
}
}
if (event.getDamager() instanceof Tameable tameable && tameable.isTamed()) {
AnimalTamer owner = tameable.getOwner();
if (owner instanceof Player player) {
sourceEntity = player;
}
}
if (!(sourceEntity instanceof Player player)) {
return;
}
if (DuelsHook.isInMatch(player) || SpleefHook.isInSpleef(player)) {
return;
}
ItemStack itemUsed = player.getInventory().getItemInMainHand();
recap.getRecap(damaged).record(RecapEntry.builder()
.damageCause(event.getCause())
.causingEntity(event.getDamager())
.sourceEntity(sourceEntity)
.amount(event.getFinalDamage())
.itemUsed(itemUsed)
.logTime(Instant.now())
.build()
);
}
}
@@ -1,180 +0,0 @@
package org.battleplugins.tracker.message;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.util.MessageUtil;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Messages {
private static final Map<String, Component> MESSAGES = new HashMap<>();
public static void load(Path messagesPath) {
MESSAGES.clear();
File messagesFile = messagesPath.toFile();
FileConfiguration messagesConfig = YamlConfiguration.loadConfiguration(messagesFile);
for (String key : messagesConfig.getKeys(false)) {
String messageText = messagesConfig.getString(key);
if (messageText == null) {
BattleTracker.getInstance().warn("Message key {} has no value in messages file! Skipping", key);
continue;
}
Component message = MessageUtil.MINI_MESSAGE.deserialize(messageText);
MESSAGES.put(key, message);
}
boolean newMessages = false;
// 08-28-2024
newMessages |= addMessage(messagesConfig, "rank-arena-stats", "<yellow>Arena Statistics:</yellow>");
if (newMessages) {
try {
messagesConfig.save(messagesFile);
} catch (Exception e) {
BattleTracker.getInstance().warn("Failed to save new messages to messages file!");
}
}
}
public static Component get(String key, String... replacements) {
Component message = MESSAGES.get(key);
if (message == null) {
BattleTracker.getInstance().warn("Unknown message key {} in messages file! Skipping", key);
return Component.empty();
}
for (String replacement : replacements) {
message = message.replaceText(builder -> builder.matchLiteral("{}").once().replacement(Component.text(replacement)));
}
return message;
}
public static Component get(String key, Map<String, Object> replacements) {
Component message = MESSAGES.get(key);
if (message == null) {
BattleTracker.getInstance().warn("Unknown message key {} in messages file! Skipping", key);
return Component.empty();
}
for (Map.Entry<String, Object> entry : replacements.entrySet()) {
if (entry.getValue() instanceof ComponentLike componentLike) {
message = message.replaceText(builder -> builder.matchLiteral("%" + entry.getKey() + "%").once().replacement(componentLike));
} else {
message = message.replaceText(builder -> builder.matchLiteral("%" + entry.getKey() + "%").once().replacement(Component.text(entry.getValue().toString())));
}
}
return message;
}
public static String getPlain(String key, String... replacements) {
Component message = MESSAGES.get(key);
if (message == null) {
BattleTracker.getInstance().warn("Unknown message key {} in messages file! Skipping", key);
return "";
}
for (String replacement : replacements) {
message = message.replaceText(builder -> builder.matchLiteral("{}").once().replacement(replacement));
}
return PlainTextComponentSerializer.plainText().serialize(MESSAGES.get(key));
}
public static void send(Audience audience, String key, Map<String, Object> replacements) {
Component message = MESSAGES.get(key);
if (message == null) {
BattleTracker.getInstance().warn("Unknown message key {} in messages file! Skipping", key);
return;
}
for (Map.Entry<String, Object> entry : replacements.entrySet()) {
if (entry.getValue() instanceof ComponentLike componentLike) {
message = message.replaceText(builder -> builder.matchLiteral("%" + entry.getKey() + "%").once().replacement(componentLike));
} else {
message = message.replaceText(builder -> builder.matchLiteral("%" + entry.getKey() + "%").once().replacement(Component.text(entry.getValue().toString())));
}
}
message = processClickEvent(message, replacements);
audience.sendMessage(message);
}
public static void send(Audience audience, String key) {
send(audience, key, Map.of());
}
public static void send(Audience audience, String key, String... replacements) {
send(audience, key, Arrays.stream(replacements).map(Component::text).toArray(Component[]::new));
}
public static void send(Audience audience, String key, Component... replacements) {
Component message = MESSAGES.get(key);
if (message == null) {
BattleTracker.getInstance().warn("Unknown message key {} in messages file! Skipping", key);
return;
}
for (Component replacement : replacements) {
message = message.replaceText(builder -> builder.matchLiteral("{}").once().replacement(replacement));
}
audience.sendMessage(message);
}
private static Component processClickEvent(Component component, Map<String, Object> replacements) {
ClickEvent clickEvent = component.clickEvent();
if (clickEvent != null) {
for (Map.Entry<String, Object> entry : replacements.entrySet()) {
clickEvent = ClickEvent.clickEvent(clickEvent.action(), clickEvent.value().replace("%" + entry.getKey() + "%", entry.getValue().toString()));
}
component = component.clickEvent(clickEvent);
}
List<Component> children = new ArrayList<>();
for (Component child : component.children()) {
ClickEvent childClickEvent = child.clickEvent();
if (childClickEvent != null) {
for (Map.Entry<String, Object> entry : replacements.entrySet()) {
childClickEvent = ClickEvent.clickEvent(childClickEvent.action(), childClickEvent.value().replace("%" + entry.getKey() + "%", entry.getValue().toString()));
}
child = child.clickEvent(childClickEvent);
}
child = processClickEvent(child, replacements);
children.add(child);
}
component = component.children(children);
return component;
}
private static boolean addMessage(Configuration configuration, String key, String message) {
if (configuration.contains(key)) {
return false;
}
configuration.set(key, message);
return true;
}
}
@@ -1,304 +0,0 @@
package org.battleplugins.tracker.sql;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* A cache holding data from a database.
* <p>
* This is an async, thread-safe cache that can be used to store data
* from a database. This cache is designed for used of data that is
* frequently accessed and should be stored in memory for faster access.
* <p>
* This cache has flushing capabilities, meaning that the cache can be
* saved to the database at any time. This is useful for saving data
* that has been modified in the cache. Additionally, this can be scaled
* or modified in cases where lots of data is loaded.
*/
public interface DbCache {
/**
* Creates a new Set cache.
*
* @param <V> the value of the cache
* @return a new Set cache
*/
static <V> SetCache<V> createSet() {
return new DbCacheSet<>();
}
/**
* Creates a new Map cache.
*
* @param <K> the key of the cache
* @param <V> the value of the cache
* @return a new Map cache
*/
static <K, V> MapCache<K, V> createMap() {
return new DbCacheMap<>();
}
/**
* Creates a new Multimap cache.
*
* @param <K> the key of the cache
* @param <V> the value of the cache
* @return a new Multimap cache
*/
static <K, V> MultimapCache<K, V> createMultimap() {
return new DbCacheMultimap<>();
}
interface SetCache<V> extends DbCache {
/**
* Adds a value to the cache.
*
* @param value the value to add
*/
void add(V value);
/**
* Modifies a value in the cache.
*
* @param value the value to modify
*/
void modify(V value);
/**
* Locks a value in the cache.
* <p>
* This will ensure that an entry is not removed
* from the cache until it is unlocked.
*
* @param value the value to lock
*/
void lock(V value);
/**
* Unlocks a value in the cache.
* <p>
* This will allow an entry to be removed from
* the cache.
*
* @param value the value to unlock
*/
void unlock(V value);
/**
* Returns a cached value from the cache immediately.
* <p>
* This method should be used when the value is expected to be
* in the cache. If the value is not in the cache, this method
* will return null.
*
* @param predicate the predicate to get the value from
* @return the value from the cache, or null if the value is not in the cache
*/
@Nullable
V getCached(Predicate<V> predicate);
/**
* Returns a value from the cache or loads it if it is not in the cache.
* <p>
* This method should be used when the value is not guaranteed to be
* in the cache. If the value is in the cache, this method will
* return the value immediately. If the value is not in the cache,
* this method will load the value from the database and return it.
*
* @param predicate the predicate to get the value from. If the predicate returns
* false, the value will be loaded from the database
* @param loader the loader to load the value from the database
* @return the value from the cache or the value loaded from the database
*/
CompletableFuture<V> getOrLoad(Predicate<V> predicate, CompletableFuture<V> loader);
/**
* Saves the cache to the database.
* <p>
* The consumer will be called for each value in the cache that
* has been modified. This is useful for saving data that has been
* modified in the cache.
*
* @param value the value to save
*/
default void save(Consumer<V> value) {
this.save(ignored -> true, value);
}
/**
* Saves matching dirty values in the cache to the database.
*
* @param predicate selects which dirty values should be persisted
* @param value the value to save
*/
void save(Predicate<V> predicate, Consumer<V> value);
/**
* Flushes the cache.
* <p>
* This method will remove the key from the cache and save the
* values to the database. This is useful for saving data that
* has been modified in the cache.
* <p>
* NOTE: This should be used in conjunction with the
* {@link #save} method as flushing with
* unsaved entries will not damageCause the objects to be flushed
* from memory.
*
* @param all whether to flush all entries
*/
void flush(boolean all);
}
interface MapBase<K, V, C> extends DbCache {
/**
* Returns a {@link Set} of all the keys in
* the cache.
*
* @return a set of all the keys in the cache
*/
Set<K> keySet();
/**
* Puts a value into the cache.
*
* @param key the key to store the value under
* @param value the value to store
*/
void put(K key, V value);
/**
* Locks a key in the cache.
* <p>
* This will ensure that an entry is not removed
* from the cache until it is unlocked.
*
* @param key the key to lock
*/
void lock(K key);
/**
* Unlocks a key in the cache.
* <p>
* This will allow an entry to be removed from
* the cache.
*
* @param key the key to unlock
*/
void unlock(K key);
/**
* Removes an entry from the cache.
*
* @param key the key of the entry
*/
void remove(K key);
/**
* Returns a cached value from the cache immediately.
* <p>
* This method should be used when the value is expected to be
* in the cache. If the value is not in the cache, this method
* will return null.
*
* @param key the key to get the value from
* @return the value from the cache, or null if the value is not in the cache
*/
@Nullable
C getCached(K key);
/**
* Returns a value from the cache or loads it if it is not in the cache.
* <p>
* This method should be used when the value is not guaranteed to be
* in the cache. If the value is in the cache, this method will
* return the value immediately. If the value is not in the cache,
* this method will load the value from the database and return it.
*
* @param key the key to get the value from
* @param loader the loader to load the value from the database
* @return the value from the cache or the value loaded from the database
*/
CompletableFuture<C> getOrLoad(K key, CompletableFuture<C> loader);
/**
* Bulk loads data into this cache.
*
* @param loader the loader to load the data from the database
* @param keyFunction the function to get the key from the value
* @return the loaded data
*/
CompletableFuture<? extends Collection<C>> loadBulk(CompletableFuture<? extends Collection<C>> loader, Function<C, K> keyFunction);
/**
* Saves the cache to the database.
* <p>
* The consumer will be called for each value in the cache that
* has been modified. This is useful for saving data that has been
* modified in the cache.
*
* @param key the key to save the value under
* @param value the value to save
*/
void save(K key, Consumer<V> value);
/**
* Flushes the cache.
* <p>
* This method will remove the key from the cache and save the
* values to the database. This is useful for saving data that
* has been modified in the cache.
* <p>
* NOTE: This should be used in conjunction with the
* {@link #save(Object, Consumer)} method as flushing with
* unsaved entries will not damageCause the objects to be flushed
* from memory.
*
* @param key the key to flush
* @param all whether to flush all entries
*/
void flush(K key, boolean all);
}
interface MapCache<K, V> extends MapBase<K, V, V> {
/**
* Marks the entry for the given key as saved (clears its dirty flag).
* <p>
* Unlike {@link #save(Object, Consumer)}, this unconditionally clears
* the dirty flag and does not invoke a consumer. It is used when the
* value has already been persisted through another code path (such as
* a batch insert) and the cache simply needs to be told the value is
* now in sync with the database. Without this, {@link #flush(Object, boolean)}
* would incorrectly report the value as unsaved.
*
* @param key the key to mark as saved
*/
void markSaved(K key);
}
interface MultimapCache<K, V> extends MapBase<K, V, List<V>> {
@NotNull
@Override
List<V> getCached(K key);
/**
* Puts a collection of values into the cache.
*
* @param key the key to store the values under
* @param values the values to store
*/
void putAll(K key, Collection<V> values);
}
}
@@ -1,139 +0,0 @@
package org.battleplugins.tracker.sql;
import org.battleplugins.tracker.BattleTracker;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
class DbCacheMap<K, V> implements DbCache.MapCache<K, V> {
private final Map<K, DbValue<V>> entries = new ConcurrentHashMap<>();
@Override
public Set<K> keySet() {
return this.entries.keySet();
}
@Override
public void put(K key, V value) {
this.entries.put(key, new DbValue<>(value, true));
}
@Override
public void remove(K key) {
this.entries.remove(key);
}
@Override
public V getCached(K key) {
DbValue<V> dbValue = this.entries.get(key);
if (dbValue == null) {
return null;
}
dbValue.resetLastAccess();
return dbValue.value;
}
@Override
public CompletableFuture<V> getOrLoad(K key, CompletableFuture<V> loader) {
if (this.entries.containsKey(key)) {
return CompletableFuture.completedFuture(this.getCached(key));
}
return loader.thenApply(value -> {
if (value == null) {
return null;
}
this.entries.put(key, new DbValue<>(value, false));
return value;
});
}
@Override
public CompletableFuture<? extends Collection<V>> loadBulk(CompletableFuture<? extends Collection<V>> loader, Function<V, K> keyFunction) {
return loader.thenApply(values -> {
for (V value : values) {
K key = keyFunction.apply(value);
// If we have this data in our cache already, let's use
// the cache as the source of truth, since the data may
// have been updated
if (this.entries.containsKey(key)) {
continue;
}
this.entries.put(key, new DbValue<>(value, false));
}
return values;
});
}
@Override
public void save(K key, Consumer<V> valueConsumer) {
DbValue<V> value = this.entries.get(key);
if (value == null) {
BattleTracker.getInstance().warn("No value found in cache for key {}", key);
return;
}
if (value.dirty) {
valueConsumer.accept(value.value);
value.dirty = false;
}
}
@Override
public void markSaved(K key) {
DbValue<V> dbValue = this.entries.get(key);
if (dbValue != null) {
dbValue.dirty = false;
}
}
@Override
public void flush(K key, boolean all) {
DbValue<V> dbValue = this.entries.get(key);
if (dbValue == null) {
this.entries.remove(key);
return;
}
if (!all && !dbValue.shouldFlush()) {
return;
}
// If the db value is locked, do not flush
if (dbValue.locked) {
return;
}
if (!dbValue.dirty) {
this.entries.remove(key);
} else {
BattleTracker.getInstance().warn("Unsaved DB value found in cache: {} for key {}", dbValue.value, key);
}
}
@Override
public void lock(K key) {
DbValue<V> dbValue = this.entries.get(key);
if (dbValue != null) {
dbValue.lock();
}
}
@Override
public void unlock(K key) {
DbValue<V> dbValue = this.entries.get(key);
if (dbValue != null) {
dbValue.unlock();
}
}
}
@@ -1,187 +0,0 @@
package org.battleplugins.tracker.sql;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multimaps;
import org.battleplugins.tracker.BattleTracker;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
class DbCacheMultimap<K, V> implements DbCache.MultimapCache<K, V> {
private final ListMultimap<K, DbValue<V>> entries = Multimaps.synchronizedListMultimap(
MultimapBuilder.hashKeys()
.arrayListValues()
.build()
);
private final Set<K> lockedKeys = new HashSet<>();
private final Set<K> loadedKeys = new HashSet<>();
@Override
public Set<K> keySet() {
return this.entries.keySet();
}
@Override
public void put(K key, V value) {
this.entries.put(key, new DbValue<>(value, true));
}
@Override
public void lock(K key) {
this.lockedKeys.add(key);
}
@Override
public void unlock(K key) {
this.lockedKeys.remove(key);
}
@Override
public void remove(K key) {
this.entries.removeAll(key);
}
@Override
public void putAll(K key, Collection<V> values) {
values.forEach(value -> this.put(key, value));
}
@NotNull
@Override
public List<V> getCached(K key) {
if (!this.entries.containsKey(key)) {
return List.of();
}
List<DbValue<V>> entries = this.entries.get(key);
if (entries.isEmpty()) {
return List.of();
}
List<V> cached = new ArrayList<>(entries.size());
for (DbValue<V> entry : entries) {
cached.add(entry.value);
entry.resetLastAccess();
}
return cached;
}
@Override
public CompletableFuture<List<V>> getOrLoad(K key, CompletableFuture<List<V>> loader) {
if (this.loadedKeys.contains(key)) {
return CompletableFuture.completedFuture(this.getCached(key));
}
List<V> cachedAndLoaded = new ArrayList<>();
if (this.entries.containsKey(key)) {
List<DbValue<V>> cachedData = this.entries.get(key);
cachedAndLoaded.addAll(cachedData.stream()
.peek(DbValue::resetLastAccess)
.map(dbValue -> dbValue.value)
.toList());
}
return loader.thenApply(value -> {
if (value == null) {
return List.of();
}
for (V v : value) {
this.entries.put(key, new DbValue<>(v, false));
}
this.loadedKeys.add(key);
// If there is cached data, we need to merge the cached data
// with the loaded data. This will only be called once, so if
// we take a slight performance hit on load, that's fine, as the
// data will be cached for future use
if (!cachedAndLoaded.isEmpty()) {
cachedAndLoaded.addAll(value);
return cachedAndLoaded;
}
return value;
});
}
@Override
public CompletableFuture<? extends Collection<List<V>>> loadBulk(CompletableFuture<? extends Collection<List<V>>> loader, Function<List<V>, K> keyFunction) {
return loader.thenApply(values -> {
for (List<V> value : values) {
K key = keyFunction.apply(value);
for (V v : value) {
// If we have this data in our cache already, let's use
// the cache as the source of truth, since the data may
// have been updated
if (this.entries.containsEntry(key, v)) {
continue;
}
this.entries.put(key, new DbValue<>(v, false));
}
this.loadedKeys.add(key);
}
return values;
});
}
@Override
public void save(K key, Consumer<V> valueConsumer) {
this.entries.get(key).forEach(dbValue -> {
if (dbValue.dirty) {
valueConsumer.accept(dbValue.value);
dbValue.dirty = false;
}
});
// If this key has never been loaded from the database
// before, we need to flush the data from the cache as
// keeping it here will mean that if a db load is called,
// the data could exist twice in our cached instance
if (!this.loadedKeys.contains(key)) {
this.flush(key, true);
}
}
@Override
public void flush(K key, boolean all) {
this.loadedKeys.remove(key);
if (!this.entries.containsKey(key)) {
return;
}
Iterator<DbValue<V>> iterator = this.entries.get(key).iterator();
while (iterator.hasNext()) {
DbValue<V> dbValue = iterator.next();
if (!all && !dbValue.shouldFlush()) {
continue;
}
// If the db value is locked, do not flush
if (this.lockedKeys.contains(key)) {
continue;
}
if (!dbValue.dirty) {
iterator.remove();
} else {
BattleTracker.getInstance().warn("Unsaved DB value found in cache: {} for key {}", dbValue.value, key);
}
}
}
}
@@ -1,108 +0,0 @@
package org.battleplugins.tracker.sql;
import org.battleplugins.tracker.BattleTracker;
import org.jetbrains.annotations.Nullable;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
class DbCacheSet<V> implements DbCache.SetCache<V> {
private final Set<DbValue<V>> entries = ConcurrentHashMap.newKeySet();
@Override
public void add(V value) {
this.entries.add(new DbValue<>(value, true));
}
@Override
public void modify(V value) {
for (DbValue<V> entry : this.entries) {
if (entry.value.equals(value)) {
entry.dirty = true;
entry.resetLastAccess();
return;
}
}
}
@Override
public void lock(V value) {
for (DbValue<V> entry : this.entries) {
if (entry.value.equals(value)) {
entry.lock();
return;
}
}
}
@Override
public void unlock(V value) {
for (DbValue<V> entry : this.entries) {
if (entry.value.equals(value)) {
entry.unlock();
return;
}
}
}
@Nullable
@Override
public V getCached(Predicate<V> predicate) {
for (DbValue<V> entry : this.entries) {
if (predicate.test(entry.value)) {
entry.resetLastAccess();
return entry.value;
}
}
return null;
}
@Override
public CompletableFuture<V> getOrLoad(Predicate<V> predicate, CompletableFuture<V> loader) {
V cached = this.getCached(predicate);
if (cached != null) {
return CompletableFuture.completedFuture(cached);
}
return loader.thenApply(value -> {
if (value == null) {
return null;
}
this.entries.add(new DbValue<>(value, false));
return value;
});
}
@Override
public void save(Predicate<V> predicate, Consumer<V> valueConsumer) {
this.entries.forEach(dbValue -> {
if (dbValue.dirty && predicate.test(dbValue.value)) {
valueConsumer.accept(dbValue.value);
dbValue.dirty = false;
}
});
}
@Override
public void flush(boolean all) {
Iterator<DbValue<V>> iterator = this.entries.iterator();
while (iterator.hasNext()) {
DbValue<V> dbValue = iterator.next();
if (!all && !dbValue.shouldFlush()) {
continue;
}
if (!dbValue.dirty) {
iterator.remove();
} else {
BattleTracker.getInstance().warn("Unsaved DB value found in cache: {}", dbValue.value);
}
}
}
}
@@ -1,32 +0,0 @@
package org.battleplugins.tracker.sql;
import org.battleplugins.tracker.BattleTracker;
class DbValue<V> {
final V value;
boolean dirty;
boolean locked;
long lastAccess = System.currentTimeMillis();
public DbValue(V value, boolean dirty) {
this.value = value;
this.dirty = dirty;
}
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public void resetLastAccess() {
this.lastAccess = System.currentTimeMillis();
}
public boolean shouldFlush() {
long staleEntryTime = BattleTracker.getInstance().getMainConfig().getAdvanced().staleEntryTime() * 1000L;
return !this.dirty && System.currentTimeMillis() - this.lastAccess > staleEntryTime;
}
}
@@ -1,208 +0,0 @@
package org.battleplugins.tracker.sql;
import org.apache.commons.dbcp2.ConnectionFactory;
import org.apache.commons.dbcp2.DriverManagerConnectionFactory;
import org.apache.commons.dbcp2.PoolableConnection;
import org.apache.commons.dbcp2.PoolableConnectionFactory;
import org.apache.commons.dbcp2.PoolingDataSource;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.battleplugins.tracker.BattleTracker;
import org.battleplugins.tracker.BattleTrackerConfig;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Holds an SQL connection to a database.
* <p>
* Only one of these should ever exist during a program's runtime, as
* databases such as SQLite can only have one connection at a time. It is
* fine if this is shared across multiple "tracker" instances, as long as
* it is only ever instantiated once.
*/
public final class SqlInstance {
private static final String CREATE_DATABASE = "CREATE DATABASE IF NOT EXISTS `%s`";
private static SqlInstance instance;
private final String db;
private final SqlSerializer.SqlType type;
private final String tablePrefix;
private final String url;
private final String port;
private final String username;
private final String password;
private final ExecutorService asyncExecutor;
private PoolingDataSource<PoolableConnection> dataSource;
private SqlInstance(BattleTrackerConfig.DatabaseOptions options) {
this.tablePrefix = options.prefix();
this.db = options.db();
this.type = options.type();
if (options.type() == SqlSerializer.SqlType.SQLITE) {
this.url = BattleTracker.getInstance().getDataFolder().toString();
} else {
this.url = options.url();
}
this.port = options.port();
this.username = options.user();
this.password = options.password();
this.asyncExecutor = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "BattleTracker-SQL");
thread.setDaemon(true);
return thread;
});
this.initDataSource();
instance = this;
}
public void close() {
this.asyncExecutor.shutdown();
try {
if (!this.asyncExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
BattleTracker.getInstance().warn("Timed out waiting for SQL tasks to finish before shutdown.");
this.asyncExecutor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
this.asyncExecutor.shutdownNow();
}
try {
this.dataSource.close();
} catch (SQLException e) {
BattleTracker.getInstance().error("Could not close SQL connection!", e);
}
instance = null;
}
private void initDataSource() {
Connection connection = null;
boolean driverLoaded = false;
for (String driver : this.type.getDrivers()) {
try {
Class.forName(driver);
driverLoaded = true;
break;
} catch (ClassNotFoundException ignored) {
}
}
if (!driverLoaded) {
throw new IllegalStateException("Could not find SQL driver for " + this.type.getName() + ": " + this.type.getDrivers());
}
String connectionString;
String datasourceString;
int minIdle;
int maxActive;
switch (this.type) {
case SQLITE:
datasourceString = connectionString = "jdbc:sqlite:" + this.url + "/" + this.db + ".sqlite";
maxActive = 2;
minIdle = 1;
break;
case MYSQL, MARIADB:
default:
minIdle = 10;
maxActive = 20;
datasourceString = this.type.getJdbcProtocol() + "://" + this.url + ":" + this.port + "/" + this.db + "?autoReconnect=true";
connectionString = this.type.getJdbcProtocol() + "://" + this.url + ":" + this.port + "?autoReconnect=true";
break;
}
// Create the data source
try {
this.dataSource = setupDataSource(datasourceString, this.username, this.password, minIdle, maxActive);
} catch (Exception e) {
throw new IllegalStateException("Could not create data source for SQL connection", e);
}
if (this.type.isMysqlLike()) {
String statement = String.format(CREATE_DATABASE, this.db);
try {
connection = DriverManager.getConnection(connectionString, this.username, this.password);
Statement st = connection.createStatement();
st.executeUpdate(statement);
} catch (SQLException e) {
throw new IllegalStateException("Could not create database " + this.db, e);
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
public String getTablePrefix() {
return this.tablePrefix;
}
public String getDatabase() {
return this.db;
}
public SqlSerializer.SqlType getType() {
return this.type;
}
public DataSource getDataSource() {
return this.dataSource;
}
public CompletableFuture<Void> runAsync(Runnable runnable) {
return CompletableFuture.runAsync(runnable, this.asyncExecutor);
}
public static SqlInstance getInstance() {
return instance;
}
public static void init(BattleTrackerConfig.DatabaseOptions options) {
if (instance != null) {
throw new IllegalStateException("Cannot create more than one SQL instance!");
}
new SqlInstance(options);
}
private static PoolingDataSource<PoolableConnection> setupDataSource(String connectURI, String username, String password,
int minIdle, int maxTotal) {
ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(connectURI, username, password);
PoolableConnectionFactory factory = new PoolableConnectionFactory(connectionFactory, null);
factory.setValidationQuery("SELECT 1");
GenericObjectPoolConfig<PoolableConnection> poolConfig = new GenericObjectPoolConfig<>();
if (minIdle != -1) {
poolConfig.setMinIdle(minIdle);
}
poolConfig.setMaxTotal(maxTotal);
poolConfig.setTestOnBorrow(true); // Test before the connection is made
// Object pool
GenericObjectPool<PoolableConnection> connectionPool = new GenericObjectPool<>(factory, poolConfig);
factory.setPool(connectionPool);
// Pooling data source
return new PoolingDataSource<>(connectionPool);
}
}
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more