Compare commits

..
15 Commits
Author SHA1 Message Date
NotAlexNoyle f898d46c95 Exclude CODEOWNERS from language statistics 2026-10-29 16:19:06 -04:00
NotAlexNoyle 05c0c298ec Add CODEOWNERS (one staff approval) 2026-10-29 16:19:05 -04:00
NotAlexNoyle b6d0d224d8 Update MCSL URL 2026-10-09 15:29:01 -04:00
NotAlexNoyle 26afe69a95 Spleef-OG and Duels-OG support 2026-08-05 12:18:06 -04:00
NotAlexNoyle d38e70e012 Fix voting rewards 2026-08-03 09:28:10 -04:00
NotAlexNoyle 12b77c50ce Fix voting 2026-07-29 16:54:49 -04:00
NotAlexNoyle 0fbcd8ca11 Fix config 2026-07-29 09:33:41 -04:00
NotAlexNoyle 2e68f4e058 Use mariadb 2026-07-22 15:08:51 -04:00
NotAlexNoyle 8b538ad86e Barebones config 2026-07-22 08:53:49 -04:00
NotAlexNoyle 1d4973f550 Disable action bar, <1 voting reminder in chat 2026-06-24 11:34:15 -04:00
NotAlexNoyle b13e4f9635 Remove vault 2026-04-24 14:09:50 -04:00
NotAlexNoyle b075866f83 Update shadow 2026-04-08 12:50:53 -04:00
NotAlexNoyle 68896b32c3 Clean up again 2025-09-25 01:20:48 -04:00
NotAlexNoyle 8e81dc99c5 Clean up 2025-09-25 01:19:50 -04:00
NotAlexNoyle 1b202f246f Merge upstream, build using gradle 2025-09-25 01:18:15 -04:00
310 changed files with 13146 additions and 39863 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# 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
-14
View File
@@ -1,14 +0,0 @@
# These are supported funding model platforms
github: BenCodez # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
#patreon: # Replace with a single Patreon username
#open_collective: # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
#liberapay: # Replace with a single Liberapay username
#issuehunt: # Replace with a single IssueHunt username
#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
#polar: # Replace with a single Polar username
#buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
custom: ["https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YQBX25RTXJ5DG"]
-30
View File
@@ -1,30 +0,0 @@
---
name: Bug report
about: Bug report for VotingPlugin
title: ''
labels: Possible Bug
assignees: ''
---
**Versions**
Plugin version and spigot version.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots/Configs**
If applicable, add screenshots to help explain your problem and/or configs relating to the issue.
**Additional context**
Add any other context about the problem here.
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea
title: ''
labels: Possible Feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -1,17 +0,0 @@
---
name: Question/Concerns
about: Ask about a question or a concern about the plugin
title: ''
labels: Question/Concern
assignees: ''
---
**Versions**
Plugin version and spigot version
**Relevant information**
Configs that relate to the issue in a pastebin.
**Context**
Context about the question/concern
-11
View File
@@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "maven" # See documentation for possible values
directory: "/VotingPlugin/" # Location of package manifests
schedule:
interval: "weekly"
-41
View File
@@ -1,41 +0,0 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support documentation.
name: Java CI with Maven
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: write # required for dependency submission
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B -f VotingPlugin/pom.xml package
# Only submit dependency snapshot on push (not on pull_request)
- name: Submit Dependency Snapshot
if: github.event_name == 'push'
uses: advanced-security/maven-dependency-submission-action@v5
with:
directory: VotingPlugin # path to pom.xml
# optional: maven-args: "-DskipTests"
-24
View File
@@ -1,24 +0,0 @@
name: Deploy Javadoc
on:
release:
types: [published]
branches:
- master
- main
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Deploy JavaDoc 🚀
uses: MathieuSoysal/Javadoc-publisher.yml@main
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
javadoc-branch: gh-pages
java-version: 21
target-folder: ''
project: maven # or gradle
# subdirectories: moduleA moduleB #for subdirectories support, needs to be run with custom command
custom-command: mvn -f VotingPlugin/pom.xml deploy -P javadoc javadoc:aggregate
javadoc-source-folder: 'VotingPlugin/target/reports/apidocs'
-27
View File
@@ -1,27 +0,0 @@
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
- cron: '19 16 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Stale issue message'
stale-pr-message: 'Stale pull request message'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'
+5
View File
@@ -0,0 +1,5 @@
.gradle/
.classpath
.project
.settings/
build/
-5
View File
@@ -1,5 +0,0 @@
---
description: Describe what this custom agent does and when to use it.
tools: []
---
Write instructions for this custom agent. These will guide Copilot on how to respond when this mode is selected.
-9
View File
@@ -1,9 +0,0 @@
/bin/
/target/
/doc/
/.idea/
/*.iml
/.settings/
/.classpath
/.factorypath
/.apt_generated_tests/
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>VotingPlugin</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
-663
View File
@@ -1,663 +0,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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bencodez</groupId>
<artifactId>votingplugin</artifactId>
<version>7.1</version>
<packaging>jar</packaging>
<name>VotingPlugin</name>
<properties>
<github.global.server>github</github.global.server>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<build.number>NOTSET</build.number>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<velocity.version>3.4.0</velocity.version>
</properties>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>plugin.yml</include>
<include>bungee.yml</include>
<include>votingpluginversion.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>plugin.yml</exclude>
<exclude>bungee.yml</exclude>
<exclude>votingpluginversion.yml</exclude>
</excludes>
</resource>
</resources>
<sourceDirectory>src/main/java</sourceDirectory>
<testSourceDirectory>src/test/java</testSourceDirectory>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<versionRange>[1,)</versionRange>
<goals>
<goal>delombok</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore />
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</path>
<path>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>${velocity.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<shadedArtifactAttached>false</shadedArtifactAttached>
<minimizeJar>false</minimizeJar>
<filters>
<filter>
<artifact>mysql-connector-java</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<relocations>
<relocation>
<pattern>com.tcoded.folialib</pattern>
<shadedPattern>
${project.groupId}.votingplugin.simpleapi.folialib</shadedPattern>
</relocation>
<relocation>
<pattern>com.bencodez.simpleapi</pattern>
<shadedPattern>
${project.groupId}.votingplugin.simpleapi</shadedPattern>
</relocation>
<relocation>
<pattern>com.bencodez.advancedcore</pattern>
<shadedPattern>
${project.groupId}.votingplugin.advancedcore</shadedPattern>
</relocation>
<relocation>
<pattern>net.pl3x.bukkit.chatapi</pattern>
<shadedPattern>${project.groupId}.votingplugin</shadedPattern>
</relocation>
<relocation>
<pattern>me.mrten.mysqlapi</pattern>
<shadedPattern>
${project.groupId}.votingplugin.mysqlapi</shadedPattern>
</relocation>
<relocation>
<pattern>com.zaxxer.hikari</pattern>
<shadedPattern>${project.groupId}.votingplugin.simpleapi.hikari</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>
${project.groupId}.votingplugin.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>xyz.upperlevel.spigot</pattern>
<shadedPattern>
${project.groupId}.votingplugin.advancedcore.xyz.upperlevel.spigot</shadedPattern>
</relocation>
<relocation>
<pattern>org.spongepowered.configurate</pattern>
<shadedPattern>${project.groupId}.simpleapi.configurate</shadedPattern>
</relocation>
<relocation>
<pattern>io.leangen.geantyref</pattern>
<shadedPattern>${project.groupId}.simpleapi.geantyref</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<!-- Lets exclude google code, because the
server already has this
code in it (guava, etc) -->
<excludes>
<exclude>com.google.*:*</exclude>
</excludes>
</artifactSet>
<minimizeJar>false</minimizeJar>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>module-info.class</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer" />
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>maven-central</id>
<url>https://oss.sonatype.org/content/groups/public</url>
</repository>
<repository>
<id>bungeecord-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
<repository>
<id>velocity</id>
<url>https://nexus.velocitypowered.com/repository/maven-public/</url>
</repository>
<repository>
<id>bencodez repo</id>
<url>https://nexus.bencodez.com/repository/maven-public/</url>
</repository>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>placeholderapi</id>
<url>
https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
<repository>
<id>scarsz</id>
<url>https://nexus.scarsz.me/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>26.1.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.21-R0.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.vexsoftware</groupId>
<artifactId>nuvotifier-universal</artifactId>
<version>2.7.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>be.maximvdw</groupId>
<artifactId>mvdwplaceholderapi</artifactId>
<version>3.1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>${velocity.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-velocity</artifactId>
<version>3.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.12.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.bencodez</groupId>
<artifactId>advancedcore</artifactId>
<version>3.8.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.12.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.21.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.12.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.discordsrv</groupId>
<artifactId>discordsrv</artifactId>
<version>1.30.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>configurate-core</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>configurate-yaml</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>configurate-gson</artifactId>
<version>4.2.0</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>default</id>
<properties>
<build.profile.id>default</build.profile.id>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifestEntries>
<paperweight-mappings-namespace>mojang</paperweight-mappings-namespace>
</manifestEntries>
</archive>
<finalName>${project.name}</finalName>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>deploy-snapshot</id>
<properties>
<build.profile.id>dev-snapshot</build.profile.id>
</properties>
<distributionManagement>
<snapshotRepository>
<id>nexus</id>
<url>https://nexus.bencodez.com/repository/maven-snapshots/</url>
</snapshotRepository>
<repository>
<id>nexus</id>
<url>https://nexus.bencodez.com/repository/maven-releases/</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifestEntries>
<paperweight-mappings-namespace>mojang</paperweight-mappings-namespace>
</manifestEntries>
</archive>
<finalName>
${project.name}-${build.number}</finalName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<phase>deploy</phase>
<configuration>
<failOnError>false</failOnError>
<doclint>none</doclint>
<sourcepath>
${project.basedir}/target/delombok</sourcepath>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.4</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.7.0</version>
<executions>
<execution>
<id>default-deploy</id>
<phase>deploy</phase>
<goals>
<goal>deploy</goal>
</goals>
</execution>
</executions>
<configuration>
<serverId>nexus</serverId>
<nexusUrl>https://nexus.bencodez.com/nexus/</nexusUrl>
<skipStaging>true</skipStaging>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>javadoc</id>
<properties>
<build.profile.id>javadoc</build.profile.id>
</properties>
<distributionManagement>
<repository>
<id>internal.repo</id>
<name>Temporary Staging Repository</name>
<url>file://${project.build.directory}/mvn-repo</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifestEntries>
<paperweight-mappings-namespace>mojang</paperweight-mappings-namespace>
</manifestEntries>
</archive>
<finalName>${project.name}</finalName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<phase>deploy</phase>
<configuration>
<sourcepath>
${project.basedir}/target/delombok</sourcepath>
<outputDirectory>
${project.basedir}/target/apidocs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.coderplus.maven.plugins</groupId>
<artifactId>copy-rename-maven-plugin</artifactId>
<version>1.0.1</version>
<executions>
<execution>
<id>copy-file</id>
<phase>deploy</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<sourceFile>
${project.basedir}/target/${project.name}.jar</sourceFile>
<destinationFile>
${project.basedir}/target/${project.name}-${project.version}.jar</destinationFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>dev</id>
<properties>
<build.profile.id>dev</build.profile.id>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifestEntries>
<paperweight-mappings-namespace>mojang</paperweight-mappings-namespace>
</manifestEntries>
</archive>
<finalName>${project.name}</finalName>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>install</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>
${user.home}/Documents/Test_Server/plugins</outputDirectory>
<resources>
<resource>
<directory>${basedir}/target</directory>
<includes>
<include>VotingPlugin.jar</include>
</includes>
</resource>
</resources>
<overwrite>true</overwrite>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>deploy</id>
<properties>
<build.profile.id>deploy</build.profile.id>
</properties>
<distributionManagement>
<snapshotRepository>
<id>nexus</id>
<url>https://nexus.bencodez.com/repository/maven-snapshots/</url>
</snapshotRepository>
<repository>
<id>nexus</id>
<url>https://nexus.bencodez.com/repository/maven-releases/</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifestEntries>
<paperweight-mappings-namespace>mojang</paperweight-mappings-namespace>
</manifestEntries>
</archive>
<finalName>${project.name}</finalName>
</configuration>
</plugin>
<plugin>
<groupId>com.coderplus.maven.plugins</groupId>
<artifactId>copy-rename-maven-plugin</artifactId>
<version>1.0.1</version>
<executions>
<execution>
<id>copy-file</id>
<phase>deploy</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<sourceFile>
${project.basedir}/target/${project.name}.jar</sourceFile>
<destinationFile>
${project.basedir}/target/${project.name}-${project.version}.jar</destinationFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<phase>deploy</phase>
<configuration>
<sourcepath>
${project.basedir}/target/delombok</sourcepath>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.4</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.7.0</version>
<executions>
<execution>
<id>default-deploy</id>
<phase>deploy</phase>
<goals>
<goal>deploy</goal>
</goals>
</execution>
</executions>
<configuration>
<serverId>nexus</serverId>
<nexusUrl>https://nexus.bencodez.com/nexus/</nexusUrl>
<skipStaging>true</skipStaging>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
@@ -1,734 +0,0 @@
// File: com/bencodez/votingplugin/BungeeHandler.java
package com.bencodez.votingplugin;
import java.io.File;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.eclipse.paho.client.mqttv3.MqttException;
import com.bencodez.advancedcore.api.misc.MiscUtils;
import com.bencodez.advancedcore.api.rewards.RewardBuilder;
import com.bencodez.advancedcore.api.time.TimeType;
import com.bencodez.advancedcore.api.user.UserStorage;
import com.bencodez.advancedcore.bungeeapi.globaldata.GlobalDataHandler;
import com.bencodez.advancedcore.bungeeapi.globaldata.GlobalMySQL;
import com.bencodez.simpleapi.encryption.EncryptionHandler;
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
import com.bencodez.simpleapi.servercomm.global.GlobalMessageHandler;
import com.bencodez.simpleapi.servercomm.global.GlobalMessageListener;
import com.bencodez.simpleapi.servercomm.mqtt.MqttHandler;
import com.bencodez.simpleapi.servercomm.mqtt.MqttServerComm;
import com.bencodez.simpleapi.servercomm.mysql.MySqlMessenger;
import com.bencodez.simpleapi.servercomm.pluginmessage.PluginMessageHandler;
import com.bencodez.simpleapi.servercomm.redis.RedisHandler;
import com.bencodez.simpleapi.servercomm.redis.RedisListener;
import com.bencodez.simpleapi.servercomm.sockets.ClientHandler;
import com.bencodez.simpleapi.servercomm.sockets.SocketHandler;
import com.bencodez.simpleapi.servercomm.sockets.SocketReceiver;
import com.bencodez.simpleapi.sql.data.DataValue;
import com.bencodez.simpleapi.sql.data.DataValueBoolean;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot;
import com.bencodez.votingplugin.proxy.BungeeMethod;
import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votesites.VoteSite;
import lombok.Getter;
/**
* Handler for Bungee/proxy server integration.
*/
public class BungeeHandler implements Listener {
@Getter
private ClientHandler clientHandler;
private EncryptionHandler encryptionHandler;
@Getter
private BungeeMethod method;
private VotingPluginMain plugin;
@Getter
private int bungeeVotePartyCurrent = -2;
@Getter
private int bungeeVotePartyRequired = -2;
@Getter
private SocketHandler socketHandler;
private GlobalDataHandler globalDataHandler;
@Getter
private ScheduledExecutorService timer;
@Getter
private RedisHandler redisHandler;
@Getter
private GlobalMessageHandler globalMessageHandler;
private Thread redisThread;
@Getter
private MySqlMessenger backendMysqlMessenger;
@Getter
private MqttHandler mqttHandler;
/**
* Constructs a new BungeeHandler.
*
* @param plugin the main plugin instance
*/
public BungeeHandler(VotingPluginMain plugin) {
this.plugin = plugin;
}
/**
* Checks and processes global data from the global data handler.
*/
public void checkGlobalData() {
HashMap<String, DataValue> data = globalDataHandler.getExact(plugin.getBungeeSettings().getServer());
if (data.containsKey("ForceUpdate")) {
boolean b = checkGlobalDataTimeValue(data.get("ForceUpdate"));
if (b) {
if (plugin.getStorageType().equals(UserStorage.MYSQL)) {
plugin.getMysql().clearCacheBasic();
}
plugin.getUserManager().getDataManager().clearCache();
plugin.setUpdate(true);
plugin.update();
globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), "ForceUpdate", false);
}
}
boolean forceUpdate = false;
if (checkGlobalDataTime(TimeType.MONTH, data)) {
forceUpdate = true;
}
if (checkGlobalDataTime(TimeType.WEEK, data)) {
forceUpdate = true;
}
if (checkGlobalDataTime(TimeType.DAY, data)) {
forceUpdate = true;
}
if (forceUpdate) {
HashMap<String, DataValue> dataToSet = new HashMap<>();
dataToSet.put("FinishedProcessing", new DataValueBoolean(true));
dataToSet.put("Processing", new DataValueBoolean(false));
globalDataHandler.setData(plugin.getBungeeSettings().getServer(), dataToSet);
}
}
/**
* Checks global data for a time type change.
*
* @param type the time type to check
* @param data the global data map
* @return true if currently processing a time change
*/
public boolean checkGlobalDataTime(TimeType type, HashMap<String, DataValue> data) {
boolean isProcessing = false;
if (data.containsKey(type.toString())) {
DataValue value = data.get(type.toString());
boolean b = checkGlobalDataTimeValue(value);
if (b) {
long lastUpdated = Long.valueOf(data.get("LastUpdated").getString()).longValue();
plugin.debug("LastUpdated: " + lastUpdated);
if (LocalDateTime.now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli() - lastUpdated > 1000 * 60 * 60
* 2) {
plugin.getLogger().warning("Ignoring bungee time change since it was more than 2 hours ago");
globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), type.toString(), false);
return false;
}
globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), "Processing", true);
isProcessing = true;
plugin.debug("Detected time change from bungee: " + type.toString());
plugin.getTimeChecker().forceChanged(type, false, true, true);
globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), type.toString(), false);
HashMap<String, Object> fields = new HashMap<>();
fields.put("server", plugin.getBungeeSettings().getServer());
sendSubChannel("TimeChangeFinished", fields);
}
}
return isProcessing;
}
/**
* Checks and extracts the boolean value from a DataValue.
*
* @param data the data value
* @return the boolean value
*/
public boolean checkGlobalDataTimeValue(DataValue data) {
if (data.isBoolean()) {
return data.getBoolean();
}
return Boolean.valueOf(data.getString());
}
/**
* Closes and cleans up all handlers and connections.
*/
public void close() {
if (backendMysqlMessenger != null) {
backendMysqlMessenger.shutdown();
}
if (socketHandler != null) {
socketHandler.closeConnection();
}
if (clientHandler != null) {
clientHandler.stopConnection();
}
plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent);
plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired);
if (globalDataHandler != null) {
globalDataHandler.getGlobalMysql().close();
}
}
/**
* Loads and initializes the bungee handler with the configured method.
*/
public void load() {
plugin.debug("Loading bungee handler");
method = BungeeMethod.getByName(plugin.getBungeeSettings().getBungeeMethod());
plugin.getLogger().info("Using BungeeMethod: " + method.toString());
loadGlobalMysql();
globalMessageHandler = new GlobalMessageHandler() {
@Override
public void sendMessage(JsonEnvelope envelope) {
if (method.equals(BungeeMethod.MYSQL)) {
try {
backendMysqlMessenger.sendToProxy(envelope);
} catch (SQLException e) {
e.printStackTrace();
}
} else if (method.equals(BungeeMethod.PLUGINMESSAGING)) {
plugin.getPluginMessaging().sendEnvelope(envelope);
} else if (method.equals(BungeeMethod.SOCKETS)) {
sendEnvelopeSocket(envelope);
} else if (method.equals(BungeeMethod.REDIS)) {
redisHandler.publishEnvelope(plugin.getBungeeSettings().getRedisPrefix() + "VotingPlugin",
envelope);
} else if (method.equals(BungeeMethod.MQTT)) {
try {
mqttHandler.publishEnvelope(
plugin.getBungeeSettings().getMqttPrefix() + "votingplugin/servers/proxy", envelope);
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
// ==========================
// Vote / VoteOnline (wire decode)
// ==========================
globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE) {
@Override
public void onReceive(JsonEnvelope msg) {
handleWireVote(msg);
}
});
globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_ONLINE) {
@Override
public void onReceive(JsonEnvelope msg) {
handleWireVote(msg);
}
});
globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_UPDATE) {
@Override
public void onReceive(JsonEnvelope msg) {
// Wire decode
VotingPluginWire.VoteUpdate v = VotingPluginWire.readVoteUpdate(msg);
String playerUuid = v.uuid;
if (playerUuid == null || playerUuid.isEmpty()) {
return;
}
plugin.debug("pluginmessaging voteupdate received for " + playerUuid + ": " + v.votePartyCurrent + "/"
+ v.votePartyRequired + " on " + v.service);
// Vote party cache update
if (v.votePartyCurrent >= 0 || bungeeVotePartyCurrent == -2) {
bungeeVotePartyCurrent = v.votePartyCurrent;
}
if (v.votePartyRequired >= 0 || bungeeVotePartyRequired == -2) {
bungeeVotePartyRequired = v.votePartyRequired;
}
plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent);
plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired);
VotingPluginUser user = plugin.getVotingPluginUserManager()
.getVotingPluginUser(UUID.fromString(playerUuid));
user.cache();
user.offVote();
// Optional: update last vote time for a service
String service = v.service;
long time = v.time;
if (service != null && !service.isEmpty() && time > 0) {
user.setTime(plugin.getVoteSiteManager().getVoteSite(service, true), time);
} else if (service != null && !service.isEmpty() && time <= 0
&& plugin.getBungeeSettings().isBungeeDebug()) {
plugin.debug("Invalid last vote time received from bungee: " + time);
}
plugin.setUpdate(true);
}
});
globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_BUNGEE_TIME_CHANGE) {
@Override
public void onReceive(JsonEnvelope msg) {
checkGlobalData();
}
});
globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_BROADCAST) {
@Override
public void onReceive(JsonEnvelope msg) {
Map<String, String> f = msg.getFields();
final String uuidStr = nvl(f.get(VotingPluginWire.K_UUID));
final String playerNameRaw = nvl(f.get(VotingPluginWire.K_PLAYER));
final String service = nvl(f.get(VotingPluginWire.K_SERVICE));
if (uuidStr.isEmpty() || service.isEmpty()) {
return;
}
UUID javaUuid;
try {
javaUuid = UUID.fromString(uuidStr);
} catch (Exception e) {
plugin.getLogger().warning("Invalid UUID in VoteBroadcast: " + uuidStr);
return;
}
// New fields (May use later)
@SuppressWarnings("unused")
final long time = readLongSafe(f.get(VotingPluginWire.K_TIME), 0L);
@SuppressWarnings("unused")
final String totals = nvl(f.get(VotingPluginWire.K_TOTALS));
VoteSite voteSite = plugin.getVoteSiteManager()
.getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, service), true);
if (voteSite == null) {
plugin.getLogger().warning("No voting site with the service site: '" + service + "'");
return;
}
if (!voteSite.isEnabled()) {
plugin.debug("Votesite: " + voteSite.getKey() + " is not enabled (VoteBroadcast)");
return;
}
// Same user retrieval strategy: UUID + (possibly empty) name
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(javaUuid,
playerNameRaw);
// Keep cache/name current like normal vote path does
user.cache();
user.updateName(true);
// Same broadcast logic as PlayerVoteListener
if (plugin.getBroadcastHandler() == null) {
return;
}
if (user.isVanished()) {
plugin.debug("Not broadcasting vote for vanished user: " + user.getPlayerName());
return;
}
final boolean online = user.isOnline(); // same behavior as non-bungee vote path
plugin.getBroadcastHandler().broadcastVote(user.getJavaUUID(), user.getPlayerName(),
voteSite.getDisplayName(), online);
}
});
globalMessageHandler.addListener(new GlobalMessageListener("Status") {
@Override
public void onReceive(JsonEnvelope msg) {
String server = nvl(msg.getFields().get("server"));
HashMap<String, Object> out = new HashMap<>();
out.put("server", server);
sendSubChannel("statusokay", out);
}
});
globalMessageHandler.addListener(new GlobalMessageListener("ServerName") {
@Override
public void onReceive(JsonEnvelope msg) {
String server = nvl(msg.getFields().get("server"));
if (!plugin.getOptions().getServer().equals(server)) {
plugin.getLogger().warning("Server name doesn't match in BungeeSettings.yml, should be " + server);
}
}
});
globalMessageHandler.addListener(new GlobalMessageListener("VotePartyBungee") {
@Override
public void onReceive(JsonEnvelope msg) {
for (final String cmd : plugin.getBungeeSettings().getBungeeVotePartyGlobalCommands()) {
plugin.getBukkitScheduler().runTask(plugin, new Runnable() {
@Override
public void run() {
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), cmd);
}
});
}
for (Player p : Bukkit.getOnlinePlayers()) {
new RewardBuilder(plugin.getBungeeSettings().getData(), "BungeeVotePartyRewards").send(p);
}
}
});
globalMessageHandler.addListener(new GlobalMessageListener("VotePartyBroadcast") {
@Override
public void onReceive(JsonEnvelope msg) {
String broadcast = nvl(msg.getFields().get("broadcast"));
MiscUtils.getInstance().broadcast(broadcast);
}
});
if (method.equals(BungeeMethod.MYSQL)) {
plugin.registerBungeeChannels(plugin.getBungeeSettings().getPluginMessagingChannel());
try {
backendMysqlMessenger = new MySqlMessenger("VotingPlugin",
plugin.getMysql().getMysql().getConnectionManager().getDataSource(),
MySqlMessenger.Mode.BACKEND, plugin.getOptions().getServer(), msg -> {
if (plugin.getBungeeSettings().isBungeeDebug()) {
plugin.debug("Proxy sent envelope: " + msg.envelope.getSubChannel() + " "
+ msg.envelope.getFields());
}
globalMessageHandler.onMessage(msg.envelope);
});
} catch (SQLException e) {
e.printStackTrace();
}
} else if (method.equals(BungeeMethod.REDIS)) {
redisHandler = new RedisHandler(plugin.getBungeeSettings().getRedisHost(),
plugin.getBungeeSettings().getRedisPort(), plugin.getBungeeSettings().getRedisUsername(),
plugin.getBungeeSettings().getRedisPassword(), plugin.getBungeeSettings().getRedisdbindex()) {
@Override
public void debug(String message) {
if (plugin.getBungeeSettings().isBungeeDebug()) {
plugin.debug(message);
}
}
};
redisThread = new Thread(new Runnable() {
@Override
public void run() {
if (plugin.isEnabled()) {
RedisListener listener = redisHandler.createEnvelopeListener(
plugin.getBungeeSettings().getRedisPrefix() + "VotingPlugin_"
+ plugin.getBungeeSettings().getServer(),
(ch, env) -> globalMessageHandler.onMessage(env));
redisHandler.loadListener(listener);
}
}
});
redisThread.start();
} else if (method.equals(BungeeMethod.PLUGINMESSAGING)) {
plugin.registerBungeeChannels(plugin.getBungeeSettings().getPluginMessagingChannel());
if (plugin.getBungeeSettings().isPluginMessageEncryption()) {
encryptionHandler = new EncryptionHandler(plugin.getName(),
new File(plugin.getDataFolder(), "secretkey.key"));
plugin.getPluginMessaging().setEncryptionHandler(encryptionHandler);
}
plugin.getPluginMessaging().setDebug(plugin.getBungeeSettings().isBungeeDebug());
plugin.getPluginMessaging().add(new PluginMessageHandler() {
@Override
public void onReceive(JsonEnvelope envelope) {
globalMessageHandler.onMessage(envelope);
}
});
} else if (method.equals(BungeeMethod.SOCKETS)) {
encryptionHandler = new EncryptionHandler(plugin.getName(),
new File(plugin.getDataFolder(), "secretkey.key"));
clientHandler = new ClientHandler(plugin.getBungeeSettings().getBungeeServerHost(),
plugin.getBungeeSettings().getBungeeServerPort(), encryptionHandler,
plugin.getBungeeSettings().isBungeeDebug());
socketHandler = new SocketHandler("vp-socket", plugin.getBungeeSettings().getSpigotServerHost(),
plugin.getBungeeSettings().getSpigotServerPort(), encryptionHandler,
plugin.getBungeeSettings().isBungeeDebug()) {
@Override
public void log(String str) {
plugin.getLogger().info(str);
}
};
socketHandler.add(new SocketReceiver() {
@Override
public void onReceiveEnvelope(JsonEnvelope envelope) {
globalMessageHandler.onMessage(envelope);
}
});
} else if (method.equals(BungeeMethod.MQTT)) {
try {
String id = plugin.getBungeeSettings().getMqttClientID();
if (id.isEmpty()) {
id = plugin.getOptions().getServer();
}
mqttHandler = new MqttHandler(new MqttServerComm(id, plugin.getBungeeSettings().getMqttBrokerURL(),
plugin.getBungeeSettings().getMqttUsername(), plugin.getBungeeSettings().getMqttPassword()), 2);
mqttHandler.subscribeEnvelopes(
plugin.getBungeeSettings().getMqttPrefix() + "votingplugin/servers/"
+ plugin.getOptions().getServer(),
(topic, envelope) -> globalMessageHandler.onMessage(envelope));
} catch (MqttException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
bungeeVotePartyCurrent = plugin.getServerData().getBungeeVotePartyCurrent();
bungeeVotePartyRequired = plugin.getServerData().getBungeeVotePartyRequired();
if (plugin.getOptions().getServer().equalsIgnoreCase("pleaseset")) {
plugin.getLogger().warning("Server name for bungee voting is not set, please set it");
}
}
private static long readLongSafe(String v, long def) {
if (v == null)
return def;
try {
return Long.parseLong(v);
} catch (Exception ignored) {
return def;
}
}
/**
* Wire vote handler (Vote + VoteOnline).
*/
private void handleWireVote(JsonEnvelope msg) {
// Strict schema check (wire uses envelope schema, not a "bungeeVersion" field)
int schema = msg.getSchema();
if (schema != VotingPluginWire.SCHEMA_VERSION) {
plugin.getLogger().warning("Incompatible version with bungee/proxy, please update all servers: " + schema
+ " != " + VotingPluginWire.SCHEMA_VERSION);
return;
}
VotingPluginWire.Vote v = VotingPluginWire.readVote(msg);
String uuidStr = v.uuid;
String player = v.player;
String service = v.service;
if (uuidStr == null || uuidStr.isEmpty()) {
return;
}
plugin.debug("wire vote received from " + player + "/" + uuidStr + " on " + service);
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(UUID.fromString(uuidStr),
player);
VoteTotalsSnapshot text = VoteTotalsSnapshot.parseStorage(v.totals == null ? "" : v.totals);
bungeeVotePartyCurrent = text.getVotePartyCurrent();
bungeeVotePartyRequired = text.getVotePartyRequired();
plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent);
plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired);
user.cache();
boolean bungeeBroadcast = v.broadcast;
// Wire has no "setTotals" flag; treat as always true (so doNotSetTotals =
// false)
boolean doNotSetTotals = false;
user.bungeeVotePluginMessaging(service, v.time, text, doNotSetTotals, v.wasOnline, bungeeBroadcast, v.num);
if (plugin.getBungeeSettings().isPerServerPoints()) {
user.addPoints(plugin.getConfigFile().getPointsOnVote());
}
if (v.manageTotals) {
plugin.getServerData().addServiceSite(service);
}
@SuppressWarnings("unused")
int _ignored = v.numberOfVotes;
}
/**
* Loads the global MySQL handler for cross-server data synchronization.
*/
public void loadGlobalMysql() {
if (plugin.getBungeeSettings().isGloblalDataEnabled()) {
if (timer != null) {
timer.shutdown();
try {
timer.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
timer.shutdownNow();
}
timer = Executors.newScheduledThreadPool(1);
timer.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
checkGlobalData();
}
}, 60, 10, TimeUnit.SECONDS);
timer.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
globalDataHandler.setString(plugin.getBungeeSettings().getServer(), "LastOnline",
"" + LocalDateTime.now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli());
}
}, 1, 60, TimeUnit.MINUTES);
if (globalDataHandler != null) {
globalDataHandler.getGlobalMysql().close();
}
if (plugin.getBungeeSettings().isGloblalDataUseMainMySQL()
&& plugin.getStorageType().equals(UserStorage.MYSQL)) {
globalDataHandler = new GlobalDataHandler(
new GlobalMySQL("VotingPlugin_GlobalData", plugin.getMysql().getMysql()) {
@Override
public void debugEx(Exception e) {
plugin.debug(e);
}
@Override
public void debugLog(String text) {
plugin.debug(text);
}
@Override
public void info(String text) {
plugin.getLogger().info(text);
}
@Override
public void logSevere(String text) {
plugin.getLogger().severe(text);
}
@Override
public void warning(String text) {
plugin.getLogger().warning(text);
}
});
} else {
globalDataHandler = new GlobalDataHandler(
new GlobalMySQL("VotingPlugin_GlobalData", new MysqlConfigSpigot(
plugin.getBungeeSettings().getData().getConfigurationSection("GlobalData"))) {
@Override
public void debugEx(Exception e) {
plugin.debug(e);
}
@Override
public void debugLog(String text) {
plugin.debug(text);
}
@Override
public void info(String text) {
plugin.getLogger().info(text);
}
@Override
public void logSevere(String text) {
plugin.getLogger().severe(text);
}
@Override
public void warning(String text) {
plugin.getLogger().warning(text);
}
});
}
globalDataHandler.getGlobalMysql().alterColumnType("IgnoreTime", "VARCHAR(5)");
globalDataHandler.getGlobalMysql().alterColumnType("MONTH", "VARCHAR(5)");
globalDataHandler.getGlobalMysql().alterColumnType("WEEK", "VARCHAR(5)");
globalDataHandler.getGlobalMysql().alterColumnType("DAY", "VARCHAR(5)");
globalDataHandler.getGlobalMysql().alterColumnType("FinishedProcessing", "VARCHAR(5)");
globalDataHandler.getGlobalMysql().alterColumnType("Processing", "VARCHAR(5)");
globalDataHandler.getGlobalMysql().alterColumnType("LastUpdated", "MEDIUMTEXT");
globalDataHandler.getGlobalMysql().alterColumnType("ForceUpdate", "VARCHAR(5)");
plugin.getTimeChecker().setProcessingEnabled(false);
}
}
private void sendEnvelopeSocket(JsonEnvelope envelope) {
if (clientHandler != null) {
clientHandler.sendEnvelope(envelope);
}
}
private void sendSubChannel(String subChannel, HashMap<String, Object> fields) {
JsonEnvelope.Builder b = JsonEnvelope.builder(subChannel).schema(VotingPluginWire.SCHEMA_VERSION);
if (fields != null) {
for (Map.Entry<String, Object> e : fields.entrySet()) {
b.put(e.getKey(), e.getValue());
}
}
globalMessageHandler.sendMessage(b.build());
}
private static String nvl(String s) {
return s == null ? "" : s;
}
}
@@ -1,126 +0,0 @@
package com.bencodez.votingplugin.broadcast;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
/**
* Defines HOW vote broadcasts are rendered.
*
* Terminology:
* - BroadcastMsg : used when EXACTLY ONE item exists (single-site vote)
* - Header : used ONCE when MULTIPLE items exist (batch/interval)
* - ListLine : used ONCE PER ITEM when MULTIPLE items exist
*
* Placeholders (always available):
* - %player% : player name (or "Server" for interval summaries)
* - %site% : the current site/item for this line
* - %sites_count% : number of items in the list
* - %sites% : csv of items in the list
* - %reason% : internal reason string ("vote", "batch", "interval", etc)
*
* Extra context placeholders (INTERVAL_SUMMARY_GLOBAL):
* - %players% / %numberofplayers%
* - %sites% / %numberofsites% (note: %sites% is csv; %numberofsites% is count)
*/
public final class BroadcastFormat {
private final String broadcastMsg;
private final String header;
private final String listLine;
/**
* Constructor for BroadcastFormat.
*
* @param broadcastMsg the broadcast message template
* @param header the header template
* @param listLine the list line template
*/
public BroadcastFormat(String broadcastMsg, String header, String listLine) {
this.broadcastMsg = broadcastMsg == null ? "" : broadcastMsg;
this.header = header == null ? "" : header;
this.listLine = listLine == null ? "" : listLine;
}
/**
* Render the broadcast with player name, items, and reason.
*
* @param playerName the player name
* @param items the list of items
* @param reason the reason string
* @return the rendered list of strings
*/
public List<String> render(String playerName, List<String> items, String reason) {
return render(playerName, items, reason, Collections.<String, String>emptyMap());
}
/**
* Render the broadcast with player name, items, reason, and context.
*
* @param playerName the player name
* @param items the list of items
* @param reason the reason string
* @param context additional context placeholders
* @return the rendered list of strings
*/
public List<String> render(String playerName, List<String> items, String reason, Map<String, String> context) {
List<String> output = new ArrayList<String>();
int count = items == null ? 0 : items.size();
String csv = "";
if (count > 0) {
StringJoiner joiner = new StringJoiner(", ");
for (String s : items) {
if (s != null && !s.isEmpty()) {
joiner.add(s);
}
}
csv = joiner.toString();
}
// SINGLE ITEM → BroadcastMsg
if (count == 1 && !broadcastMsg.isEmpty()) {
output.add(apply(broadcastMsg, playerName, items.get(0), count, csv, reason, context));
return output;
}
// MULTI ITEM → Header + ListLine per item
if (!header.isEmpty()) {
output.add(apply(header, playerName, "", count, csv, reason, context));
}
if (count > 0 && !listLine.isEmpty()) {
for (String s : items) {
output.add(apply(listLine, playerName, s, count, csv, reason, context));
}
}
return output;
}
private String apply(String template,
String player,
String item,
int count,
String csv,
String reason,
Map<String, String> context) {
String out = template
.replace("%player%", player == null ? "" : player)
.replace("%site%", item == null ? "" : item)
.replace("%sites_count%", String.valueOf(count))
.replace("%sites%", csv == null ? "" : csv)
.replace("%reason%", reason == null ? "" : reason);
if (context != null && !context.isEmpty()) {
for (Map.Entry<String, String> e : context.entrySet()) {
out = out.replace("%" + e.getKey() + "%", e.getValue());
}
}
return out;
}
}
@@ -1,443 +0,0 @@
package com.bencodez.votingplugin.broadcast;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import com.bencodez.advancedcore.api.misc.MiscUtils;
import com.bencodez.simpleapi.messages.MessageAPI;
import com.bencodez.simpleapi.time.ParsedDuration;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
/**
* Core broadcast logic.
*
* Decides WHEN to broadcast and builds lines/messages. Uses your existing
* "eligible online players" behavior (disable broadcast respected).
*
* NOTE: Backend servers only.
*/
public final class BroadcastHandler {
private final VotingPluginMain plugin;
/**
* Only used for day-boundary logic (FIRST_VOTE_OF_DAY).
*/
private final ZoneId zoneId;
private volatile BroadcastSettings settings;
// Per-player cooldown tracking
private final ConcurrentHashMap<UUID, Instant> lastBroadcastAt = new ConcurrentHashMap<UUID, Instant>();
// Batch window tracking
private final ConcurrentHashMap<UUID, LinkedHashSet<String>> pendingSites = new ConcurrentHashMap<UUID, LinkedHashSet<String>>();
private final ConcurrentHashMap<UUID, BukkitTask> pendingFlush = new ConcurrentHashMap<UUID, BukkitTask>();
// First vote of day tracking
private final ConcurrentHashMap<UUID, LocalDate> firstVoteDay = new ConcurrentHashMap<UUID, LocalDate>();
// Interval summary tracking (uuid -> set of sites during interval)
private final ConcurrentHashMap<UUID, LinkedHashSet<String>> intervalSites = new ConcurrentHashMap<UUID, LinkedHashSet<String>>();
private volatile BukkitTask intervalTask;
/**
* Constructs a new BroadcastHandler.
*
* @param plugin the main plugin instance
* @param settings the broadcast settings
* @param zoneId the time zone ID, or null to use system default
*/
public BroadcastHandler(VotingPluginMain plugin, BroadcastSettings settings, ZoneId zoneId) {
this.plugin = plugin;
this.settings = settings;
this.zoneId = zoneId == null ? ZoneId.systemDefault() : zoneId;
rescheduleIntervalIfNeeded();
}
/**
* Updates the broadcast settings and reschedules interval task if needed.
*
* @param settings the new broadcast settings
*/
public void setSettings(BroadcastSettings settings) {
this.settings = settings;
rescheduleIntervalIfNeeded();
}
/**
* Call whenever a vote is received.
*
* @param uuid player's uuid
* @param playerName player name (optional; if null/empty, resolved from Bukkit)
* @param siteName vote site name (display name)
* @param wasOnline whether the player was online when the vote was received
*/
public void broadcastVote(UUID uuid, String playerName, String siteName, boolean wasOnline) {
BroadcastSettings s = settings;
if (s == null || s.isDisabled()) {
return;
}
// Always collect for interval summaries (even if Type isn't interval right
// now).
recordInterval(uuid, siteName);
String name = playerName == null || playerName.isEmpty() ? resolveName(uuid) : playerName;
VoteBroadcastType type = s.getType();
// New: types that only broadcast if the voting player is online
if (type == VoteBroadcastType.EVERY_VOTE_ONLINE_ONLY) {
if (!wasOnline) {
return;
}
broadcastNow(name, single(siteName), "vote_online_only", null);
return;
}
if (type == VoteBroadcastType.EVERY_VOTE) {
broadcastNow(name, single(siteName), "vote", null);
} else if (type == VoteBroadcastType.COOLDOWN_PER_PLAYER) {
if (checkAndMarkCooldown(uuid, s.getDuration())) {
broadcastNow(name, single(siteName), "cooldown", null);
}
} else if (type == VoteBroadcastType.BATCH_WINDOW_PER_PLAYER) {
bufferBatch(uuid, name, siteName, s.getDuration());
} else if (type == VoteBroadcastType.FIRST_VOTE_OF_DAY) {
if (markFirstVoteOfDay(uuid)) {
broadcastNow(name, single(siteName), "first_day", null);
}
} else if (type == VoteBroadcastType.INTERVAL_SUMMARY_GLOBAL) {
// handled by scheduled task
}
}
/*
* ======================= Broadcast helpers =======================
*/
private void broadcastNow(String playerName, List<String> sites, String reason, Map<String, String> extraContext) {
BroadcastSettings s = settings;
if (s == null || s.isDisabled()) {
return;
}
List<String> cleaned = applyMaxSites(sites, s);
cleaned.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
String aa = a == null ? "" : a.toLowerCase(Locale.ROOT);
String bb = b == null ? "" : b.toLowerCase(Locale.ROOT);
return aa.compareTo(bb);
}
});
List<String> lines;
if (extraContext == null) {
lines = s.getFormat().render(playerName, cleaned, reason);
} else {
lines = s.getFormat().render(playerName, cleaned, reason, extraContext);
}
for (String line : lines) {
broadcastToEligiblePlayers(line);
}
}
/**
* Your reliable broadcast style: - only sends to online players - respects
* per-player disable broadcast toggle - runs PlaceholderAPI style per-player
* placeholders (if your PlaceholderUtils does) - supports %newline% / %NewLine%
* splitting
*/
private void broadcastToEligiblePlayers(String broadcastMsg) {
if (broadcastMsg == null || broadcastMsg.isEmpty()) {
return;
}
ArrayList<Player> players = new ArrayList<Player>();
for (Player p : Bukkit.getOnlinePlayers()) {
VotingPluginUser u = plugin.getVotingPluginUserManager().getVotingPluginUser(p);
if (u != null && !u.getDisableBroadcast()) {
players.add(p);
}
}
// If you prefer your manual loop, keep it here (not in main)
MiscUtils.getInstance().broadcast(broadcastMsg, players);
Bukkit.getServer().getConsoleSender().sendMessage(MessageAPI.colorize(broadcastMsg));
}
/*
* ======================= Cooldown logic (fixed millis) =======================
*/
private boolean checkAndMarkCooldown(UUID uuid, ParsedDuration cooldown) {
if (cooldown == null || cooldown.isEmpty()) {
return true;
}
Instant now = Instant.now();
Instant last = lastBroadcastAt.get(uuid);
if (last == null) {
lastBroadcastAt.put(uuid, now);
return true;
}
Instant next = cooldown.addTo(last);
if (!now.isBefore(next)) {
lastBroadcastAt.put(uuid, now);
return true;
}
return false;
}
/*
* ======================= Batch window logic (fixed millis)
* =======================
*/
private void bufferBatch(UUID uuid, String playerName, String siteName, ParsedDuration window) {
if (siteName != null && !siteName.isEmpty()) {
LinkedHashSet<String> set = pendingSites.get(uuid);
if (set == null) {
LinkedHashSet<String> created = new LinkedHashSet<String>();
set = pendingSites.putIfAbsent(uuid, created);
if (set == null) {
set = created;
}
}
synchronized (set) {
set.add(siteName);
}
}
if (!pendingFlush.containsKey(uuid)) {
BukkitTask task = scheduleBatchFlush(uuid, playerName, window);
BukkitTask prev = pendingFlush.putIfAbsent(uuid, task);
if (prev != null) {
task.cancel();
}
}
}
private BukkitTask scheduleBatchFlush(final UUID uuid, final String playerName, ParsedDuration window) {
long delayMs = window == null ? 1L : window.delayMillisFromNow();
long ticks = Math.max(1L, delayMs / 50L);
return Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
@Override
public void run() {
try {
flushBatch(uuid, playerName);
} finally {
pendingFlush.remove(uuid);
}
}
}, ticks);
}
private void flushBatch(UUID uuid, String playerName) {
Set<String> set = pendingSites.remove(uuid);
if (set == null || set.isEmpty()) {
return;
}
List<String> list;
synchronized (set) {
list = new ArrayList<String>(set);
}
broadcastNow(playerName, list, "batch", null);
}
/*
* ======================= First vote of day (calendar) =======================
*/
private boolean markFirstVoteOfDay(UUID uuid) {
LocalDate today = LocalDate.now(zoneId);
LocalDate last = firstVoteDay.putIfAbsent(uuid, today);
if (last == null) {
return true;
}
if (!last.equals(today)) {
firstVoteDay.put(uuid, today);
return true;
}
return false;
}
/*
* ======================= Interval summary =======================
*/
private void rescheduleIntervalIfNeeded() {
if (intervalTask != null) {
intervalTask.cancel();
intervalTask = null;
}
BroadcastSettings s = settings;
if (s == null || s.getType() != VoteBroadcastType.INTERVAL_SUMMARY_GLOBAL) {
return;
}
long delayMs = s.getDuration() == null ? 1L : s.getDuration().delayMillisFromNow();
long ticks = Math.max(1L, delayMs / 50L);
intervalTask = Bukkit.getScheduler().runTaskTimer(plugin, new Runnable() {
@Override
public void run() {
broadcastIntervalSummary();
}
}, ticks, ticks);
}
private void recordInterval(UUID uuid, String siteName) {
if (siteName == null || siteName.isEmpty()) {
return;
}
LinkedHashSet<String> set = intervalSites.get(uuid);
if (set == null) {
LinkedHashSet<String> created = new LinkedHashSet<String>();
set = intervalSites.putIfAbsent(uuid, created);
if (set == null) {
set = created;
}
}
synchronized (set) {
set.add(siteName);
}
}
/**
* Interval summary supports extra context placeholders: - %players% /
* %numberofplayers% - %sites% / %numberofsites%
*/
private void broadcastIntervalSummary() {
BroadcastSettings s = settings;
if (s == null || s.isDisabled() || s.getType() != VoteBroadcastType.INTERVAL_SUMMARY_GLOBAL) {
return;
}
ConcurrentHashMap<UUID, LinkedHashSet<String>> snapshot = new ConcurrentHashMap<UUID, LinkedHashSet<String>>(
intervalSites);
intervalSites.clear();
if (snapshot.isEmpty()) {
return;
}
List<String> entries = new ArrayList<String>();
List<String> players = new ArrayList<String>();
LinkedHashSet<String> uniqueSites = new LinkedHashSet<String>();
for (Map.Entry<UUID, LinkedHashSet<String>> entry : snapshot.entrySet()) {
LinkedHashSet<String> siteSet = entry.getValue();
if (siteSet == null || siteSet.isEmpty()) {
continue;
}
String name = resolveName(entry.getKey());
players.add(name);
int siteCount;
synchronized (siteSet) {
siteCount = siteSet.size();
for (String site : siteSet) {
if (site != null && !site.isEmpty()) {
uniqueSites.add(site);
}
}
}
entries.add(name + " (" + siteCount + ")");
}
if (entries.isEmpty()) {
return;
}
String playersCsv = join(players);
String sitesCsv = join(new ArrayList<String>(uniqueSites));
ConcurrentHashMap<String, String> context = new ConcurrentHashMap<String, String>();
context.put("players", playersCsv);
context.put("numberofplayers", String.valueOf(players.size()));
context.put("sites", sitesCsv);
context.put("numberofsites", String.valueOf(uniqueSites.size()));
broadcastNow("Server", entries, "interval", context);
}
/*
* ======================= Utilities =======================
*/
private List<String> single(String site) {
List<String> list = new ArrayList<String>();
if (site != null && !site.isEmpty()) {
list.add(site);
}
return list;
}
private List<String> applyMaxSites(List<String> sites, BroadcastSettings s) {
if (sites == null) {
return new ArrayList<String>();
}
int max = s.getMaxSitesListed();
if (max <= 0 || sites.size() <= max) {
return new ArrayList<String>(sites);
}
return new ArrayList<String>(sites.subList(0, max));
}
private String resolveName(UUID uuid) {
try {
OfflinePlayer op = Bukkit.getOfflinePlayer(uuid);
String name = op == null ? null : op.getName();
return name == null || name.isEmpty() ? "Player" : name;
} catch (Exception e) {
return "Player";
}
}
private String join(List<String> list) {
StringJoiner joiner = new StringJoiner(", ");
for (String s : list) {
if (s != null && !s.isEmpty()) {
joiner.add(s);
}
}
return joiner.toString();
}
}
@@ -1,112 +0,0 @@
package com.bencodez.votingplugin.broadcast;
import java.util.concurrent.TimeUnit;
import org.bukkit.configuration.ConfigurationSection;
import com.bencodez.simpleapi.time.ParsedDuration;
/**
* Immutable broadcast configuration.
*
* Type: NONE disables broadcasting.
*/
public final class BroadcastSettings {
private final VoteBroadcastType type;
private final ParsedDuration duration;
private final int maxSitesListed;
private final BroadcastFormat format;
/**
* Constructs a BroadcastSettings with the specified parameters.
*
* @param type the broadcast type
* @param duration the broadcast duration
* @param maxSitesListed the maximum number of sites to list
* @param format the broadcast format
*/
public BroadcastSettings(VoteBroadcastType type, ParsedDuration duration, int maxSitesListed,
BroadcastFormat format) {
this.type = type == null ? VoteBroadcastType.NONE : type;
this.duration = duration == null ? ParsedDuration.empty() : duration;
this.maxSitesListed = Math.max(0, maxSitesListed);
this.format = format == null ? new BroadcastFormat("", "", "") : format;
}
/**
* Returns the broadcast type.
*
* @return the broadcast type
*/
public VoteBroadcastType getType() {
return type;
}
/**
* Returns the broadcast duration.
*
* @return the broadcast duration
*/
public ParsedDuration getDuration() {
return duration;
}
/**
* Returns the maximum number of sites to list.
*
* @return the maximum number of sites to list
*/
public int getMaxSitesListed() {
return maxSitesListed;
}
/**
* Returns the broadcast format.
*
* @return the broadcast format
*/
public BroadcastFormat getFormat() {
return format;
}
/**
* Returns whether broadcasting is disabled.
*
* @return true if broadcasting is disabled
*/
public boolean isDisabled() {
return type == VoteBroadcastType.NONE;
}
/**
* Loads VoteBroadcast settings from config.yml.
*
* Expected structure: VoteBroadcast: Type: EVERY_VOTE Duration: 2m
* MaxSitesListed: 0 Format: BroadcastMsg: '...' Header: '...' ListLine: '...'
*
* @param sec the configuration section to load from
* @return the loaded broadcast settings
*/
public static BroadcastSettings load(ConfigurationSection sec) {
VoteBroadcastType type = VoteBroadcastType.parse(sec == null ? null : sec.getString("Type"),
VoteBroadcastType.NONE);
ParsedDuration duration = ParsedDuration.parse(sec == null ? "2m" : sec.getString("Duration", "2m"),
TimeUnit.MINUTES);
int maxSites = sec == null ? 0 : sec.getInt("MaxSitesListed", 0);
ConfigurationSection fmt = sec == null ? null : sec.getConfigurationSection("Format");
String broadcastMsg = fmt == null ? "&6[Vote] &aThanks &e%player% &afor voting on &e%site%&a!"
: fmt.getString("BroadcastMsg", "&6[Vote] &aThanks &e%player% &afor voting on &e%site%&a!");
String header = fmt == null ? "&6[Vote] &a%player% voted! &7(%sites_count%)"
: fmt.getString("Header", "&6[Vote] &a%player% voted! &7(%sites_count%)");
String listLine = fmt == null ? " &7- &e%site%" : fmt.getString("ListLine", " &7- &e%site%");
return new BroadcastSettings(type, duration, maxSites, new BroadcastFormat(broadcastMsg, header, listLine));
}
}
@@ -1,69 +0,0 @@
package com.bencodez.votingplugin.broadcast;
import java.util.Locale;
/**
* Defines WHEN vote broadcasts occur.
*
* Set Type: NONE to disable broadcasting entirely.
*/
public enum VoteBroadcastType {
/**
* No vote broadcasting.
*/
NONE,
/**
* Broadcast on every vote (even if the player is offline). Uses
* Format.BroadcastMsg (+ optional header/list if you choose).
*/
EVERY_VOTE,
/**
* Broadcast on every vote ONLY if the voting player is online. Useful when
* processing offline votes but you don't want global spam for them.
*/
EVERY_VOTE_ONLINE_ONLY,
/**
* Broadcast at most once per Duration per player. (Even if they vote multiple
* sites inside the cooldown.)
*/
COOLDOWN_PER_PLAYER,
/**
* Batch sites per player for Duration, then broadcast one message listing
* sites.
*/
BATCH_WINDOW_PER_PLAYER,
/**
* Broadcast only the first vote of the day per player (calendar day).
*/
FIRST_VOTE_OF_DAY,
/**
* Every Duration, broadcast a global summary of who voted and how many sites.
* Uses context placeholders: %players%, %numberofplayers%, %sites%,
* %numberofsites%.
*/
INTERVAL_SUMMARY_GLOBAL;
/**
* Parses a vote broadcast type from a string.
* @param s the string to parse
* @param def the default value if parsing fails
* @return the parsed broadcast type or default
*/
public static VoteBroadcastType parse(String s, VoteBroadcastType def) {
if (s == null || s.trim().isEmpty()) {
return def;
}
try {
return VoteBroadcastType.valueOf(s.trim().toUpperCase(Locale.ROOT));
} catch (Exception ignored) {
return def;
}
}
}
@@ -1,120 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.milestones;
import java.util.ArrayList;
import java.util.Set;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.advancedcore.command.gui.RewardEditGUI;
import com.bencodez.simpleapi.valuerequest.InputMethod;
import com.bencodez.simpleapi.valuerequest.NumberListener;
import com.bencodez.simpleapi.valuerequest.StringListener;
import com.bencodez.simpleapi.valuerequest.ValueRequest;
import com.bencodez.votingplugin.VotingPluginMain;
/**
* Admin vote milestones GUI handler.
*/
public class AdminVoteVoteMilestones extends GUIHandler {
private VotingPluginMain plugin;
/**
* Constructor for AdminVoteVoteMilestones.
*
* @param plugin the VotingPluginMain instance
* @param player the command sender
*/
public AdminVoteVoteMilestones(VotingPluginMain plugin, CommandSender player) {
super(plugin, player);
this.plugin = plugin;
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
BInventory inv = new BInventory("Edit VoteMilestones");
inv.requirePermission("VotingPlugin.Commands.AdminVote.Edit.VoteMilestones");
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER)
.setName("&cEdit existing vote milestone rewards (Legacy milestones not included)")) {
@Override
public void onClick(ClickEvent clickEvent) {
Set<String> keys = plugin.getVoteMilestonesManager().getConfig().getMilestones().keySet();
ArrayList<String> milestoneKeys = new ArrayList<String>();
for (String key : keys) {
if (!key.startsWith("Legacy_")) {
milestoneKeys.add(key);
}
}
new ValueRequest(plugin, plugin.getDialogService(), InputMethod.INVENTORY).requestString(
clickEvent.getPlayer(), "", milestoneKeys, false, "Select milestone", new StringListener() {
@Override
public void onInput(Player player, String value) {
RewardEditGUI.getInstance().openRewardGUI(clickEvent.getPlayer(),
plugin.getRewardHandler().getDirectlyDefined("VoteMilestones." + value + ".Rewards"));
}
});
}
});
inv.addButton(new BInventoryButton(new ItemBuilder(Material.EMERALD_BLOCK).setName("&aAdd milestone")) {
@Override
public void onClick(ClickEvent clickEvent) {
new ValueRequest(plugin, plugin.getDialogService(), null).requestNumber(player, 0, null, true,
"Enter milestone", new NumberListener() {
@Override
public void onInput(Player player, Number value) {
plugin.getSpecialRewardsConfig().setVoteMilestone(value.intValue());
plugin.reload();
}
});
}
});
inv.addButton(new BInventoryButton(new ItemBuilder(Material.REDSTONE_BLOCK).setName("&cRemove milestone")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteVoteMilestoneRemove(plugin, clickEvent.getPlayer()).open();
}
});
inv.openInventory(player);
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,169 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: Select an event filter, then open AdminVoteLogRecent (filtered).
*/
public class AdminVoteLogEventSelect extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int days;
private final int page;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogEventSelect.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
*/
public AdminVoteLogEventSelect(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days) {
this(plugin, sender, table, user, days, 0);
}
@Override
public void onDialog(Player player) {
}
/**
* Constructor for AdminVoteLogEventSelect with page.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogEventSelect(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
this.days = days;
this.page = Math.max(0, page);
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Events";
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
List<VoteLogMysqlTable.VoteLogEvent> events = Arrays.asList(VoteLogMysqlTable.VoteLogEvent.values());
int start = page * PAGE_SIZE;
int end = Math.min(events.size(), start + PAGE_SIZE);
if (start >= events.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo events")
.addLoreLine("&7No events on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
}
});
} else {
for (int i = start; i < end; i++) {
VoteLogMysqlTable.VoteLogEvent ev = events.get(i);
inv.addButton(makeEventButton(ev));
}
}
// back button
if (user != null && plugin.getGui().isChestVoteTopBackButton()) {
inv.getPageButtons().add(plugin.getCommandLoader().getBackButton(user).setSlot(7));
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makeEventButton(VoteLogMysqlTable.VoteLogEvent ev) {
ItemBuilder item = new ItemBuilder(Material.BOOK).setName("&a" + ev.name())
.addLoreLine("&7Click to view recent logs filtered by this event")
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time");
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogRecent(plugin, clickEvent.getPlayer(), table, user, 0, days, ev).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,125 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.bukkit.Material;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Helper class for admin vote log GUI operations.
*/
public class AdminVoteLogHelpers {
/**
* Get the material icon for a vote log event.
*
* @param event the vote log event
* @param status the status of the vote
* @return the material to use for the icon
*/
public static Material getMaterialForEvent(VoteLogMysqlTable.VoteLogEvent event, String status) {
// Cached entries still override to chest (keeps your current behavior)
if ("CACHED".equalsIgnoreCase(status)) {
return Material.CHEST;
}
if (event == null) {
return Material.PAPER;
}
switch (event) {
case VOTE_RECEIVED:
return Material.EMERALD;
case VOTEMILESTONE:
return Material.ANVIL;
case VOTE_STREAK_REWARD:
return Material.FIREWORK_ROCKET;
case TOP_VOTER_REWARD:
return Material.GOLD_BLOCK;
default:
return Material.PAPER;
}
}
/**
* Get the color code for a vote log event.
*
* @param event the vote log event
* @return the color code string
*/
public static String getEventColor(VoteLogMysqlTable.VoteLogEvent event) {
if (event == null) {
return "&7";
}
switch (event) {
case VOTE_RECEIVED:
return "&a";
case VOTEMILESTONE:
return "&6";
case VOTE_STREAK_REWARD:
return "&d";
case TOP_VOTER_REWARD:
return "&c";
default:
return "&7";
}
}
/**
* Returns a safe string, converting null to empty string.
*
* @param s the string to check
* @return the string or empty string if null
*/
public static String safe(String s) {
return s == null ? "" : s;
}
/**
* Checks if a string is not null and not empty.
*
* @param s the string to check
* @return true if the string is not empty
*/
public static boolean notEmpty(String s) {
return s != null && !s.trim().isEmpty();
}
/**
* Formats a timestamp in milliseconds to a readable date string.
*
* @param millis the timestamp in milliseconds
* @return the formatted date string
*/
public static String formatTime(long millis) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date(millis));
} catch (Exception e) {
return String.valueOf(millis);
}
}
/**
* Returns a shortened version of a UUID string.
*
* @param uuid the UUID string
* @return the first 8 characters of the UUID or "unknown"
*/
public static String shortUuid(String uuid) {
if (uuid == null || uuid.isEmpty()) {
return "unknown";
}
return uuid.length() > 8 ? uuid.substring(0, 8) : uuid;
}
}
@@ -1,208 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin vote log menu GUI handler.
*/
public class AdminVoteLogMenu extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int days; // window used by selectors/stats (0 = all time)
// Default to "voting" (Vote received)
private final VoteLogMysqlTable.VoteLogEvent defaultEvent = VoteLogMysqlTable.VoteLogEvent.VOTE_RECEIVED;
/**
* Constructor for AdminVoteLogMenu.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param days the number of days to filter
*/
public AdminVoteLogMenu(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, int days) {
this(plugin, sender, table, null, days);
}
@Override
public void onDialog(Player player) {
}
/**
* Constructor for AdminVoteLogMenu with user.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
*/
public AdminVoteLogMenu(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
this.days = Math.max(0, days);
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Admin";
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
inv.addButton(new BInventoryButton(new ItemBuilder(Material.NAME_TAG).setName("&aVotes by Player")
.addLoreLine("&7List stored player names").addLoreLine("&7Click a player to view their votelog")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogPlayerSelect(plugin, clickEvent.getPlayer(), table, user, days, 0).open();
}
});
inv.addButton(new BInventoryButton(new ItemBuilder(Material.NAME_TAG).setName("&aVotes by Player (Online)")
.addLoreLine("&7List online players").addLoreLine("&7Click a player to view their votelog")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogPlayerSelectOnline(plugin, clickEvent.getPlayer(), table, user, days, 0).open();
}
});
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&aVotes by Service")
.addLoreLine("&7List all services found").addLoreLine("&7Click a service to view votelog")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogServiceSelect(plugin, clickEvent.getPlayer(), table, user, days, 0).open();
}
});
// NEW: Votes by Server
inv.addButton(new BInventoryButton(new ItemBuilder(Material.COMPASS).setName("&aVotes by Server")
.addLoreLine("&7List all servers found").addLoreLine("&7Click a server to view votelog")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogServerSelect(plugin, clickEvent.getPlayer(), table, user, days, 0).open();
}
});
inv.addButton(new BInventoryButton(new ItemBuilder(Material.MAP).setName("&aStats")
.addLoreLine("&7Overview counts/uniques/top services")
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogStats(plugin, clickEvent.getPlayer(), table, user, days, 10).open();
}
});
inv.addButton(new BInventoryButton(new ItemBuilder(Material.CLOCK).setName("&aRecent Logs")
.addLoreLine("&7Recent logs filtered by event").addLoreLine("&7Default: &f" + defaultEvent.name())
.addLoreLine("&8(Use bottom buttons to switch event)")) {
@Override
public void onClick(ClickEvent clickEvent) {
// Open recent with default event filter
new AdminVoteLogRecent(plugin, clickEvent.getPlayer(), table, user, 0, days, defaultEvent).open();
}
});
addEventSwitchButtons(inv);
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private void addEventSwitchButtons(BInventory inv) {
inv.getPageButtons().add(new BInventoryButton(new ItemBuilder(Material.LIME_DYE)
.setName("&aEvent: &fVOTE_RECEIVED").addLoreLine("&7Show recent vote received events")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogRecent(plugin, clickEvent.getPlayer(), table, user, 0, days,
VoteLogMysqlTable.VoteLogEvent.VOTE_RECEIVED).open();
}
}.setSlot(3));
// More... (open selector)
inv.getPageButtons().add(new BInventoryButton(
new ItemBuilder(Material.BOOK).setName("&aEvent: &fMore...").addLoreLine("&7Pick any event filter")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogEventSelect(plugin, clickEvent.getPlayer(), table, user, days).open();
}
}.setSlot(4));
// All events
inv.getPageButtons().add(new BInventoryButton(new ItemBuilder(Material.GRAY_DYE).setName("&aEvent: &fALL")
.addLoreLine("&7Show recent logs for all events")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogRecent(plugin, clickEvent.getPlayer(), table, user, 0, days, null).open();
}
}.setSlot(5));
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,185 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: VoteLog entries for a given player (paged).
*
* Uses VoteLogMysqlTable instance passed in from CommandLoader.
*
* Supports input as player name OR uuid string (you decide in CommandLoader
* which constructor to call).
*/
public class AdminVoteLogPlayer extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private final int page;
private final String playerName;
private final int days;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogPlayer.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param playerName the player name or UUID
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogPlayer(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, String playerName,
int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.playerName = playerName;
this.days = days;
this.page = Math.max(0, page);
}
@Override
public void onDialog(Player player) {
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Player";
if (playerName != null && !playerName.isEmpty()) {
title += " &7(" + playerName + ")";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
int needed = (page + 1) * PAGE_SIZE;
List<VoteLogMysqlTable.VoteLogEntry> rows = table.getByPlayerName(playerName, days, needed);
int start = page * PAGE_SIZE;
int end = Math.min(rows.size(), start + PAGE_SIZE);
if (start >= rows.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo entries")
.addLoreLine("&7No vote log entries on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
} else {
for (int i = start; i < end; i++) {
inv.addButton(makeEntryButton(rows.get(i)));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makeEntryButton(final VoteLogMysqlTable.VoteLogEntry entry) {
Material mat = "CACHED".equalsIgnoreCase(entry.status) ? Material.CHEST : Material.EMERALD;
String timeStr = AdminVoteLogHelpers.formatTime(entry.voteTime);
String uuidShort = AdminVoteLogHelpers.shortUuid(entry.playerUuid);
String voteShort = AdminVoteLogHelpers.shortUuid(entry.voteId);
// keep same name/lore layout you already have
ItemBuilder item = new ItemBuilder(mat)
.setName("&a" + AdminVoteLogHelpers.safe(entry.event) + " &7(" + voteShort + ")");
if (AdminVoteLogHelpers.notEmpty(entry.playerName) || AdminVoteLogHelpers.notEmpty(entry.playerUuid)) {
item.addLoreLine("&7Player: &f" + AdminVoteLogHelpers.safe(entry.playerName) + " &7(" + uuidShort + ")");
}
if (entry.voteTime > 0) {
item.addLoreLine("&7Time: &f" + timeStr);
}
if (AdminVoteLogHelpers.notEmpty(entry.service)) {
item.addLoreLine("&7Service: &f" + AdminVoteLogHelpers.safe(entry.service));
}
if (AdminVoteLogHelpers.notEmpty(entry.context)) {
item.addLoreLine("&7Context: &f" + AdminVoteLogHelpers.safe(entry.context));
}
if (AdminVoteLogHelpers.notEmpty(entry.status)) {
item.addLoreLine("&7Status: &f" + AdminVoteLogHelpers.safe(entry.status));
}
if (entry.proxyCachedTotal != 0) {
item.addLoreLine("&7Cached Total: &f" + entry.proxyCachedTotal);
}
if (AdminVoteLogHelpers.notEmpty(entry.voteId)) {
item.addLoreLine("&7VoteId: &f" + AdminVoteLogHelpers.safe(entry.voteId));
}
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
// vote_id is now the correlation id entry.voteId
new AdminVoteLogVoteId(plugin, clickEvent.getPlayer(), table, entry.voteId, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,155 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: Select an ONLINE player, then open AdminVoteLogPlayer.
*/
public class AdminVoteLogPlayerSelect extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int days;
private final int page;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogPlayerSelect.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogPlayerSelect(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
this.days = Math.max(0, days);
this.page = Math.max(0, page);
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onDialog(Player player) {
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Players";
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
List<Player> online = new ArrayList<>(Bukkit.getOnlinePlayers());
int start = page * PAGE_SIZE;
int end = Math.min(online.size(), start + PAGE_SIZE);
if (start >= online.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo players")
.addLoreLine("&7No online players on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
} else {
for (int i = start; i < end; i++) {
Player p = online.get(i);
inv.addButton(makePlayerButton(p));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makePlayerButton(final Player target) {
String name = target.getName();
ItemBuilder item = new ItemBuilder(Material.NAME_TAG).setName("&a" + name)
.addLoreLine("&7Click to open votelog for this player")
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time");
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogPlayer(plugin, clickEvent.getPlayer(), table, name, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,151 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: Select an ONLINE player, then open AdminVoteLogPlayer.
*/
public class AdminVoteLogPlayerSelectOnline extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int days;
private final int page;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogPlayerSelectOnline.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogPlayerSelectOnline(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
this.days = days;
this.page = Math.max(0, page);
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public void onDialog(Player player) {
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Players";
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
List<Player> online = new ArrayList<>(Bukkit.getOnlinePlayers());
int start = page * PAGE_SIZE;
int end = Math.min(online.size(), start + PAGE_SIZE);
if (start >= online.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo players")
.addLoreLine("&7No online players on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
}
});
} else {
for (int i = start; i < end; i++) {
Player p = online.get(i);
inv.addButton(makePlayerButton(p));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makePlayerButton(Player target) {
String name = target.getName();
ItemBuilder item = new ItemBuilder(Material.NAME_TAG).setName("&a" + name)
.addLoreLine("&7Click to open votelog for this player")
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time");
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogPlayer(plugin, clickEvent.getPlayer(), table, name, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,255 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: Recent VoteLog entries (paged).
*
* Now supports optional event filter + days window.
*/
public class AdminVoteLogRecent extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int page;
private final int days; // 0 = all time
private final VoteLogMysqlTable.VoteLogEvent event; // null = all events
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogRecent.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param page the page number
*/
public AdminVoteLogRecent(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, int page) {
this(plugin, sender, table, null, page, 0, null);
}
/**
* Constructor for AdminVoteLogRecent with user.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param page the page number
*/
public AdminVoteLogRecent(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int page) {
this(plugin, sender, table, user, page, 0, null);
}
/**
* Constructor for AdminVoteLogRecent with full parameters.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param page the page number
* @param days the number of days to filter
* @param event the event type to filter
*/
// NEW constructor used by Menu/EventSelect
public AdminVoteLogRecent(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int page, int days, VoteLogMysqlTable.VoteLogEvent event) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
this.page = Math.max(0, page);
this.days = Math.max(0, days);
this.event = event;
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Recent";
if (event != null) {
title += " &7(" + event.name() + ")";
}
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
int needed = (page + 1) * PAGE_SIZE;
List<VoteLogMysqlTable.VoteLogEntry> rows;
if (event == null) {
rows = (days > 0) ? table.getRecentAll(days, needed) : table.getRecent(needed);
} else {
rows = table.getRecentByEvent(event, days, needed);
}
int start = page * PAGE_SIZE;
int end = Math.min(rows.size(), start + PAGE_SIZE);
if (start >= rows.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo entries")
.addLoreLine("&7No vote log entries on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
}
});
} else {
for (int i = start; i < end; i++) {
inv.addButton(makeEntryButton(rows.get(i)));
}
}
addEventSwitchButtons(inv);
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private void addEventSwitchButtons(BInventory inv) {
inv.getPageButtons().add(new BInventoryButton(new ItemBuilder(Material.LIME_DYE)
.setName("&aEvent: &fVOTE_RECEIVED").addLoreLine("&7Show recent vote received events")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogRecent(plugin, clickEvent.getPlayer(), table, user, 0, days,
VoteLogMysqlTable.VoteLogEvent.VOTE_RECEIVED).open();
}
}.setSlot(3));
// More... (open selector)
inv.getPageButtons().add(new BInventoryButton(
new ItemBuilder(Material.BOOK).setName("&aEvent: &fMore...").addLoreLine("&7Pick any event filter")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogEventSelect(plugin, clickEvent.getPlayer(), table, user, days).open();
}
}.setSlot(4));
// All events
inv.getPageButtons().add(new BInventoryButton(new ItemBuilder(Material.GRAY_DYE).setName("&aEvent: &fALL")
.addLoreLine("&7Show recent logs for all events")) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogRecent(plugin, clickEvent.getPlayer(), table, user, 0, days, null).open();
}
}.setSlot(5));
}
private BInventoryButton makeEntryButton(final VoteLogMysqlTable.VoteLogEntry entry) {
VoteLogMysqlTable.VoteLogEvent event = entry.event != null ? VoteLogMysqlTable.VoteLogEvent.valueOf(entry.event)
: null;
Material mat = AdminVoteLogHelpers.getMaterialForEvent(event, entry.status);
String eventColor = AdminVoteLogHelpers.getEventColor(event);
String timeStr = AdminVoteLogHelpers.formatTime(entry.voteTime);
String uuidShort = AdminVoteLogHelpers.shortUuid(entry.playerUuid);
String voteShort = AdminVoteLogHelpers.shortUuid(entry.voteId);
ItemBuilder item = new ItemBuilder(mat)
.setName(eventColor + AdminVoteLogHelpers.safe(entry.event) + " &7(" + voteShort + ")");
if (AdminVoteLogHelpers.notEmpty(entry.playerName) || AdminVoteLogHelpers.notEmpty(entry.playerUuid)) {
item.addLoreLine("&7Player: &f" + AdminVoteLogHelpers.safe(entry.playerName) + " &7(" + uuidShort + ")");
}
if (entry.voteTime > 0) {
item.addLoreLine("&7Time: &f" + timeStr);
}
if (AdminVoteLogHelpers.notEmpty(entry.service)) {
item.addLoreLine("&7Service: &f" + AdminVoteLogHelpers.safe(entry.service));
}
if (AdminVoteLogHelpers.notEmpty(entry.context)) {
item.addLoreLine("&7Context: &f" + AdminVoteLogHelpers.safe(entry.context));
}
if (AdminVoteLogHelpers.notEmpty(entry.status)) {
item.addLoreLine("&7Status: &f" + AdminVoteLogHelpers.safe(entry.status));
}
if (entry.proxyCachedTotal != 0) {
item.addLoreLine("&7Cached Total: &f" + entry.proxyCachedTotal);
}
if (AdminVoteLogHelpers.notEmpty(entry.voteId)) {
item.addLoreLine("&7VoteId: &f" + AdminVoteLogHelpers.safe(entry.voteId));
}
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogVoteId(plugin, clickEvent.getPlayer(), table, entry.voteId, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,204 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: VoteLog entries for a given server (paged).
*
* Uses VoteLogMysqlTable instance passed in from CommandLoader.
*/
public class AdminVoteLogServer extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int page;
private final String server;
private final int days;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogServer.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param server the server name
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogServer(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, String server,
int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.server = server;
this.days = Math.max(0, days);
this.page = Math.max(0, page);
if (sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
/**
* Constructor for AdminVoteLogServer with user.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param server the server name
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogServer(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, String server, int days, int page) {
this(plugin, sender, table, server, days, page);
this.user = user;
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
try {
BInventory inv = new BInventory("&aVoteLog - Server &7(" + AdminVoteLogHelpers.safe(server) + ")");
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
int needed = (page + 1) * PAGE_SIZE;
List<VoteLogMysqlTable.VoteLogEntry> rows = table.getByServer(server, days, needed);
int start = page * PAGE_SIZE;
int end = Math.min(rows.size(), start + PAGE_SIZE);
if (start >= rows.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo entries")
.addLoreLine("&7No vote log entries on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
} else {
for (int i = start; i < end; i++) {
inv.addButton(makeEntryButton(rows.get(i)));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makeEntryButton(final VoteLogMysqlTable.VoteLogEntry entry) {
VoteLogMysqlTable.VoteLogEvent event = entry.event != null ? VoteLogMysqlTable.VoteLogEvent.valueOf(entry.event)
: null;
Material mat = AdminVoteLogHelpers.getMaterialForEvent(event, entry.status);
String eventColor = AdminVoteLogHelpers.getEventColor(event);
String timeStr = AdminVoteLogHelpers.formatTime(entry.voteTime);
String uuidShort = AdminVoteLogHelpers.shortUuid(entry.playerUuid);
String voteShort = AdminVoteLogHelpers.shortUuid(entry.voteId);
ItemBuilder item = new ItemBuilder(mat)
.setName(eventColor + AdminVoteLogHelpers.safe(entry.event) + " &7(" + voteShort + ")");
if (AdminVoteLogHelpers.notEmpty(entry.playerName) || AdminVoteLogHelpers.notEmpty(entry.playerUuid)) {
item.addLoreLine("&7Player: &f" + AdminVoteLogHelpers.safe(entry.playerName) + " &7(" + uuidShort + ")");
}
if (entry.voteTime > 0) {
item.addLoreLine("&7Time: &f" + timeStr);
}
if (AdminVoteLogHelpers.notEmpty(entry.server)) {
item.addLoreLine("&7Server: &f" + AdminVoteLogHelpers.safe(entry.server));
}
if (AdminVoteLogHelpers.notEmpty(entry.service)) {
item.addLoreLine("&7Service: &f" + AdminVoteLogHelpers.safe(entry.service));
}
if (AdminVoteLogHelpers.notEmpty(entry.context)) {
item.addLoreLine("&7Context: &f" + AdminVoteLogHelpers.safe(entry.context));
}
if (AdminVoteLogHelpers.notEmpty(entry.status)) {
item.addLoreLine("&7Status: &f" + AdminVoteLogHelpers.safe(entry.status));
}
if (entry.proxyCachedTotal != 0) {
item.addLoreLine("&7Cached Total: &f" + entry.proxyCachedTotal);
}
if (AdminVoteLogHelpers.notEmpty(entry.voteId)) {
item.addLoreLine("&7VoteId: &f" + AdminVoteLogHelpers.safe(entry.voteId));
}
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
// vote_id is the correlation id
new AdminVoteLogVoteId(plugin, clickEvent.getPlayer(), table, entry.voteId, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,152 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: Select a server (distinct list), then open AdminVoteLogServer.
*/
public class AdminVoteLogServerSelect extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int days;
private final int page;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogServerSelect.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogServerSelect(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
this.days = Math.max(0, days);
this.page = Math.max(0, page);
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onDialog(Player player) {
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Servers";
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
int needed = (page + 1) * PAGE_SIZE;
List<String> servers = table.getDistinctServers(days, needed);
int start = page * PAGE_SIZE;
int end = Math.min(servers.size(), start + PAGE_SIZE);
if (start >= servers.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo servers")
.addLoreLine("&7No servers on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
} else {
for (int i = start; i < end; i++) {
String srv = servers.get(i);
inv.addButton(makeServerButton(srv));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makeServerButton(final String server) {
ItemBuilder item = new ItemBuilder(Material.PAPER).setName("&a" + AdminVoteLogHelpers.safe(server))
.addLoreLine("&7Click to open votelog for this server")
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time");
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogServer(plugin, clickEvent.getPlayer(), table, server, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,204 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: VoteLog entries for a given service/site (paged).
*
* Uses VoteLogMysqlTable instance passed in from CommandLoader.
*/
public class AdminVoteLogService extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int page;
private final String service;
private final int days;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogService.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param service the service name
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogService(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, String service,
int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.service = service;
this.days = days;
this.page = Math.max(0, page);
if (sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
/**
* Constructor for AdminVoteLogService with user.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param service the service name
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogService(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, String service, int days, int page) {
this(plugin, sender, table, service, days, page);
this.user = user;
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
try {
BInventory inv = new BInventory("&aVoteLog - Service &7(" + AdminVoteLogHelpers.safe(service) + ")");
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
int needed = (page + 1) * PAGE_SIZE;
List<VoteLogMysqlTable.VoteLogEntry> rows = table.getByService(service, days, needed);
int start = page * PAGE_SIZE;
int end = Math.min(rows.size(), start + PAGE_SIZE);
if (start >= rows.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo entries")
.addLoreLine("&7No vote log entries on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
} else {
for (int i = start; i < end; i++) {
inv.addButton(makeEntryButton(rows.get(i)));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makeEntryButton(final VoteLogMysqlTable.VoteLogEntry entry) {
VoteLogMysqlTable.VoteLogEvent event = entry.event != null ? VoteLogMysqlTable.VoteLogEvent.valueOf(entry.event)
: null;
Material mat = AdminVoteLogHelpers.getMaterialForEvent(event, entry.status);
String eventColor = AdminVoteLogHelpers.getEventColor(event);
String timeStr = AdminVoteLogHelpers.formatTime(entry.voteTime);
String uuidShort = AdminVoteLogHelpers.shortUuid(entry.playerUuid);
String voteShort = AdminVoteLogHelpers.shortUuid(entry.voteId);
ItemBuilder item = new ItemBuilder(mat)
.setName(eventColor + AdminVoteLogHelpers.safe(entry.event) + " &7(" + voteShort + ")");
if (AdminVoteLogHelpers.notEmpty(entry.playerName) || AdminVoteLogHelpers.notEmpty(entry.playerUuid)) {
item.addLoreLine("&7Player: &f" + AdminVoteLogHelpers.safe(entry.playerName) + " &7(" + uuidShort + ")");
}
if (entry.voteTime > 0) {
item.addLoreLine("&7Time: &f" + timeStr);
}
if (AdminVoteLogHelpers.notEmpty(entry.service)) {
item.addLoreLine("&7Service: &f" + AdminVoteLogHelpers.safe(entry.service));
}
if (AdminVoteLogHelpers.notEmpty(entry.context)) {
item.addLoreLine("&7Context: &f" + AdminVoteLogHelpers.safe(entry.context));
}
if (AdminVoteLogHelpers.notEmpty(entry.status)) {
item.addLoreLine("&7Status: &f" + AdminVoteLogHelpers.safe(entry.status));
}
if (entry.proxyCachedTotal != 0) {
item.addLoreLine("&7Cached Total: &f" + entry.proxyCachedTotal);
}
if (AdminVoteLogHelpers.notEmpty(entry.voteId)) {
item.addLoreLine("&7VoteId: &f" + AdminVoteLogHelpers.safe(entry.voteId));
}
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
// vote_id is the correlation id
new AdminVoteLogVoteId(plugin, clickEvent.getPlayer(), table, entry.voteId, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,152 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: Select a service (distinct list), then open AdminVoteLogService.
*/
public class AdminVoteLogServiceSelect extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int days;
private final int page;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogServiceSelect.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogServiceSelect(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
this.days = Math.max(0, days);
this.page = Math.max(0, page);
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Services";
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
int needed = (page + 1) * PAGE_SIZE;
List<String> services = table.getDistinctServices(days, needed);
int start = page * PAGE_SIZE;
int end = Math.min(services.size(), start + PAGE_SIZE);
if (start >= services.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo services")
.addLoreLine("&7No services on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
} else {
for (int i = start; i < end; i++) {
String svc = services.get(i);
inv.addButton(makeServiceButton(svc));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makeServiceButton(final String service) {
ItemBuilder item = new ItemBuilder(Material.PAPER).setName("&a" + AdminVoteLogHelpers.safe(service))
.addLoreLine("&7Click to open votelog for this service")
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time");
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogService(plugin, clickEvent.getPlayer(), table, service, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,150 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: Select a service from DB (distinct services), then open
* AdminVoteLogService.
*/
public class AdminVoteLogServiceSelectAll extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int days;
private final int page;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogServiceSelectAll.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogServiceSelectAll(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
this.days = days;
this.page = Math.max(0, page);
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Services";
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
int needed = (page + 1) * PAGE_SIZE;
List<String> services = table.getDistinctServices(days, needed);
int start = page * PAGE_SIZE;
int end = Math.min(services.size(), start + PAGE_SIZE);
if (start >= services.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo services")
.addLoreLine("&7No services on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
}
});
} else {
for (int i = start; i < end; i++) {
String svc = services.get(i);
inv.addButton(makeServiceButton(svc));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makeServiceButton(String service) {
ItemBuilder item = new ItemBuilder(Material.PAPER).setName("&a" + AdminVoteLogHelpers.safe(service))
.addLoreLine("&7Click to open votelog for this service")
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time");
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
new AdminVoteLogService(plugin, clickEvent.getPlayer(), table, service, days, 0).open();
}
};
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,239 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.util.ArrayList;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: VoteLog stats overview.
*
* Uses VoteLogMysqlTable instance passed in from CommandLoader.
*
* Shows: - total / immediate / cached counts (VoteLogCounts) - unique voters -
* top services - top servers
*/
public class AdminVoteLogStats extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int days;
private final int topServicesLimit;
private final int topServersLimit;
/**
* Constructor for AdminVoteLogStats.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param days the number of days to filter
*/
public AdminVoteLogStats(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, int days) {
this(plugin, sender, table, null, days, 10, 10);
}
/**
* Constructor for AdminVoteLogStats with user and top services limit.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
* @param topServicesLimit the maximum number of top services to display
*/
public AdminVoteLogStats(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int topServicesLimit) {
this(plugin, sender, table, user, days, topServicesLimit, 10);
}
/**
* Constructor for AdminVoteLogStats with full parameters.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param days the number of days to filter
* @param topServicesLimit the maximum number of top services to display
* @param topServersLimit the maximum number of top servers to display
*/
public AdminVoteLogStats(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int topServicesLimit, int topServersLimit) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.user = user;
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
this.days = Math.max(0, days);
this.topServicesLimit = topServicesLimit <= 0 ? 10 : topServicesLimit;
this.topServersLimit = topServersLimit <= 0 ? 10 : topServersLimit;
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - Stats";
if (days > 0) {
title += " &7(" + days + "d)";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
// counts
VoteLogMysqlTable.VoteLogCounts counts = table.getCounts(days);
long uniques = table.getUniqueVoters(days);
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BOOK).setName("&aOverview")
.addLoreLine("&7Total Votes: &f" + counts.total).addLoreLine("&7Immediate: &f" + counts.immediate)
.addLoreLine("&7Cached: &f" + counts.cached).addLoreLine("&7Unique Voters: &f" + uniques)
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
// top services header
inv.addButton(new BInventoryButton(new ItemBuilder(Material.MAP).setName("&aTop Services")
.addLoreLine("&7Top " + topServicesLimit + " services/sites:")
.addLoreLine("&8(Click does nothing)")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
int shownServices = 0;
for (VoteLogMysqlTable.ServiceCount sc : table.getTopServices(days, topServicesLimit)) {
shownServices++;
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER)
.setName("&a#" + shownServices + " &f" + AdminVoteLogHelpers.safe(sc.service))
.addLoreLine("&7Votes: &f" + sc.votes)
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op (could open AdminVoteLogService in future)
}
});
}
if (shownServices == 0) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo service data")
.addLoreLine("&7No services found in the log.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
}
// top servers header
inv.addButton(new BInventoryButton(new ItemBuilder(Material.COMPASS).setName("&aTop Servers")
.addLoreLine("&7Top " + topServersLimit + " servers:").addLoreLine("&8(Click does nothing)")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
int shownServers = 0;
for (VoteLogMysqlTable.ServerCount sc : table.getTopServers(days, topServersLimit)) {
shownServers++;
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER)
.setName("&a#" + shownServers + " &f" + AdminVoteLogHelpers.safe(sc.server))
.addLoreLine("&7Votes: &f" + sc.votes)
.addLoreLine(days > 0 ? "&7Window: &fLast " + days + " days" : "&7Window: &fAll time")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op (could open AdminVoteLogServer in future)
}
});
}
if (shownServers == 0) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo server data")
.addLoreLine("&7No servers found in the log.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,257 +0,0 @@
package com.bencodez.votingplugin.commands.gui.admin.votelog;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable;
/**
* Admin GUI: VoteLog entries for a given voteId (paged).
*
* vote_id is NOT unique; this shows ALL rows for the correlation id.
*/
public class AdminVoteLogVoteId extends GUIHandler {
private final VotingPluginMain plugin;
private final VoteLogMysqlTable table;
private VotingPluginUser user;
private final int page;
private final String voteId;
private final int days;
private static final int PAGE_SIZE = 45;
/**
* Constructor for AdminVoteLogVoteId.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param voteId the vote ID to filter
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogVoteId(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, String voteId,
int days, int page) {
super(plugin, sender);
this.plugin = plugin;
this.table = table;
this.voteId = voteId;
this.days = days;
this.page = Math.max(0, page);
if (sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
/**
* Constructor for AdminVoteLogVoteId with user.
*
* @param plugin the VotingPluginMain instance
* @param sender the command sender
* @param table the vote log table
* @param user the voting plugin user
* @param voteId the vote ID to filter
* @param days the number of days to filter
* @param page the page number
*/
public AdminVoteLogVoteId(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, String voteId, int days, int page) {
this(plugin, sender, table, voteId, days, page);
this.user = user;
if (this.user == null && sender instanceof Player) {
this.user = plugin.getVotingPluginUserManager().getVotingPluginUser((Player) sender);
}
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
try {
String title = "&aVoteLog - VoteId";
if (voteId != null && !voteId.isEmpty()) {
title += " &7(" + shortId(voteId) + ")";
}
BInventory inv = new BInventory(title);
inv.requirePermission("VotingPlugin.Commands.AdminVote.VoteLog");
if (!plugin.getConfigFile().isAlwaysCloseInventory()) {
inv.dontClose();
}
if (table == null) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.BARRIER).setName("&cVoteLog unavailable")
.addLoreLine("&7VoteLog MySQL table is not loaded/enabled.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
if (voteId == null || voteId.isEmpty()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo voteId provided")
.addLoreLine("&7Missing voteId.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
return;
}
int needed = (page + 1) * PAGE_SIZE;
// vote_id is not unique -> show ALL rows for it (paged)
List<VoteLogMysqlTable.VoteLogEntry> rows = table.getByVoteIdAll(voteId, days, needed);
int start = page * PAGE_SIZE;
int end = Math.min(rows.size(), start + PAGE_SIZE);
if (start >= rows.size()) {
inv.addButton(new BInventoryButton(new ItemBuilder(Material.PAPER).setName("&eNo entries")
.addLoreLine("&7No vote log entries on this page.").addLoreLine("&7Try going back a page.")) {
@Override
public void onClick(ClickEvent clickEvent) {
// no-op
}
});
} else {
for (int i = start; i < end; i++) {
inv.addButton(makeEntryButton(rows.get(i)));
}
}
inv.setPages(true);
inv.setMaxInvSize(54);
inv.openInventory(player);
} catch (Exception e) {
e.printStackTrace();
}
}
private BInventoryButton makeEntryButton(VoteLogMysqlTable.VoteLogEntry entry) {
VoteLogMysqlTable.VoteLogEvent event = entry.event != null ? VoteLogMysqlTable.VoteLogEvent.valueOf(entry.event)
: null;
Material mat = AdminVoteLogHelpers.getMaterialForEvent(event, entry.status);
String eventColor = AdminVoteLogHelpers.getEventColor(event);
String timeStr = formatTime(entry.voteTime);
String uuidShort = shortId(entry.playerUuid);
String voteShort = shortId(entry.voteId);
ItemBuilder item = new ItemBuilder(mat)
.setName(eventColor + AdminVoteLogHelpers.safe(entry.event) + " &7(" + voteShort + ")");
// Only add lore lines when the value is actually set / meaningful
// (prevents blank/0/default noise)
if (notEmpty(entry.playerName) || notEmpty(entry.playerUuid)) {
item.addLoreLine("&7Player: &f" + safe(entry.playerName) + " &7(" + uuidShort + ")");
}
if (entry.voteTime > 0) {
item.addLoreLine("&7Time: &f" + timeStr);
}
if (notEmpty(entry.service)) {
item.addLoreLine("&7Service: &f" + safe(entry.service));
}
if (notEmpty(entry.context)) {
item.addLoreLine("&7Context: &f" + safe(entry.context));
}
if (notEmpty(entry.status)) {
item.addLoreLine("&7Status: &f" + safe(entry.status));
}
if (entry.proxyCachedTotal != 0) {
item.addLoreLine("&7Cached Total: &f" + entry.proxyCachedTotal);
}
if (notEmpty(entry.voteId)) {
item.addLoreLine("&7VoteId: &f" + safe(entry.voteId));
}
return new BInventoryButton(item) {
@Override
public void onClick(ClickEvent clickEvent) {
// read-only
}
};
}
private String formatTime(long millis) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date(millis));
} catch (Exception e) {
return String.valueOf(millis);
}
}
private boolean notEmpty(String s) {
return s != null && !s.trim().isEmpty();
}
private String shortId(String s) {
if (s == null || s.isEmpty()) {
return "unknown";
}
return s.length() > 8 ? s.substring(0, 8) : s;
}
private String safe(String s) {
return s == null ? "" : s;
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,260 +0,0 @@
package com.bencodez.votingplugin.commands.gui.player;
import java.util.ArrayList;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.advancedcore.api.rewards.RewardBuilder;
import com.bencodez.simpleapi.messages.MessageAPI;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.voteshop.VoteShopManager;
import com.bencodez.votingplugin.voteshop.service.VoteShopPurchaseResult;
import com.bencodez.votingplugin.voteshop.shop.VoteShopCategory;
import com.bencodez.votingplugin.voteshop.shop.VoteShopCategoryButton;
import com.bencodez.votingplugin.voteshop.shop.VoteShopDefinition;
import com.bencodez.votingplugin.voteshop.shop.VoteShopEntry;
import com.bencodez.votingplugin.voteshop.shop.VoteShopExtraItem;
import com.bencodez.votingplugin.voteshop.shop.VoteShopItem;
/**
* Main vote shop GUI.
*/
public class VoteShop extends GUIHandler {
private VotingPluginMain plugin;
private VotingPluginUser user;
/**
* Creates the GUI.
*
* @param plugin the plugin
* @param player the sender
* @param user the user
*/
public VoteShop(VotingPluginMain plugin, CommandSender player, VotingPluginUser user) {
super(plugin, player);
this.plugin = plugin;
this.user = user;
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
VoteShopManager manager = plugin.getVoteShopManager();
VoteShopDefinition definition = manager.getDefinition();
if (!definition.isEnabled()) {
player.sendMessage(MessageAPI.colorize(definition.getDisabledMessage()));
return;
}
VotingPluginUser currentUser = getUser(player);
BInventory inv = new BInventory(definition.getTitle());
inv.addPlaceholder("points", String.valueOf(currentUser.getPoints()));
inv.addPlaceholder("sitesavailable", String.valueOf(currentUser.getSitesNotVotedOn()));
inv.dontClose();
for (VoteShopEntry entry : definition.getMainEntries().values()) {
if (entry instanceof VoteShopItem) {
addItemButton(inv, player, currentUser, (VoteShopItem) entry, null);
} else if (entry instanceof VoteShopCategoryButton) {
addCategoryButton(inv, player, currentUser, (VoteShopCategoryButton) entry);
}
}
for (VoteShopExtraItem extraItem : definition.getExtraItems().values()) {
addExtraItemButton(inv, player, currentUser, extraItem);
}
if (definition.isBackButton()) {
inv.addButton(plugin.getCommandLoader().getBackButton(currentUser));
}
inv.openInventory(player);
}
/**
* Adds an item button.
*
* @param inv the inventory
* @param player the player
* @param currentUser the user
* @param item the item
* @param category the category, null for main shop
*/
protected void addItemButton(BInventory inv, final Player player, final VotingPluginUser currentUser,
final VoteShopItem item, final VoteShopCategory category) {
VoteShopPurchaseResult validation = plugin.getVoteShopManager().getPurchaseService().validatePurchase(player,
currentUser, item);
boolean hasPermission = plugin.getVoteShopManager().getPurchaseService().hasPermission(player,
item.getPermission());
if ((!hasPermission && item.isHideOnNoPermission()) || (validation == VoteShopPurchaseResult.LIMIT_REACHED
&& plugin.getVoteShopManager().getDefinition().isHideLimitedReached())) {
return;
}
inv.addButton(new BInventoryButton(new ItemBuilder(item.getDisplaySection())) {
@Override
public void onClick(ClickEvent event) {
VotingPluginUser clickedUser = getUser(event.getPlayer());
if (plugin.getConfigFile().isExtraVoteShopCheck()) {
clickedUser.cache();
}
if (item.isNotBuyable()) {
clickedUser.sendMessage(plugin.getConfigFile().getFormatShopNotPurchasable());
return;
}
VoteShopPurchaseResult clickValidation = plugin.getVoteShopManager().getPurchaseService()
.validatePurchase(event.getPlayer(), clickedUser, item);
if (clickValidation != VoteShopPurchaseResult.SUCCESS) {
plugin.getVoteShopManager().getPurchaseService().sendFailureMessage(event.getPlayer(), clickedUser,
item, clickValidation);
return;
}
if (item.isRequireConfirmation()) {
if (plugin.getShopFile().isShopConfirmPurchaseUseDialog()) {
new VoteShopConfirm(plugin, event.getPlayer(), clickedUser, item, category)
.open(GUIMethod.DIALOG);
return;
} else {
new VoteShopConfirm(plugin, event.getPlayer(), clickedUser, item, category)
.open(GUIMethod.CHEST);
return;
}
}
handlePurchase(event.getPlayer(), clickedUser, item, category);
}
}.addData("identifier", item.getIdentifier()));
}
/**
* Adds a category button.
*
* @param inv the inventory
* @param player the player
* @param currentUser the user
* @param button the button
*/
protected void addCategoryButton(BInventory inv, final Player player, final VotingPluginUser currentUser,
final VoteShopCategoryButton button) {
boolean hasPermission = plugin.getVoteShopManager().getPurchaseService().hasPermission(player,
button.getPermission());
if (!hasPermission && button.isHideOnNoPermission()) {
return;
}
final VoteShopCategory category = plugin.getVoteShopManager().getCategory(button.getCategoryId());
if (category == null) {
plugin.extraDebug(
"VoteShop: missing category '" + button.getCategoryId() + "' for entry " + button.getIdentifier());
return;
}
inv.addButton(new BInventoryButton(new ItemBuilder(button.getDisplaySection())) {
@Override
public void onClick(ClickEvent event) {
new VoteShopCategoryMenu(plugin, event.getPlayer(), currentUser, category).open(GUIMethod.CHEST);
}
}.addData("identifier", button.getIdentifier()));
}
/**
* Adds an extra item button.
*
* @param inv the inventory
* @param player the player
* @param currentUser the user
* @param extraItem the extra item
*/
protected void addExtraItemButton(BInventory inv, final Player player, final VotingPluginUser currentUser,
final VoteShopExtraItem extraItem) {
inv.addButton(new BInventoryButton(new ItemBuilder(extraItem.getDisplaySection())) {
@Override
public void onClick(ClickEvent clickEvent) {
plugin.getCommandLoader().processSlotClick(player, currentUser, extraItem.getIdentifier());
new RewardBuilder(plugin.getShopFile().getData(),
"ExtraItems." + extraItem.getIdentifier() + "."
+ clickEvent.getButton().getLastRewardsPath(player))
.setGiveOffline(false).send(clickEvent.getPlayer());
}
});
}
/**
* Handles a completed purchase.
*
* @param player the player
* @param currentUser the user
* @param item the item
* @param category the category or null
*/
protected void handlePurchase(Player player, VotingPluginUser currentUser, VoteShopItem item,
VoteShopCategory category) {
VoteShopPurchaseResult result = plugin.getVoteShopManager().purchase(player, currentUser, item);
if (result != VoteShopPurchaseResult.SUCCESS) {
plugin.getVoteShopManager().getPurchaseService().sendFailureMessage(player, currentUser, item, result);
return;
}
plugin.getCommandLoader().processSlotClick(player, currentUser, item.getIdentifier());
if (plugin.getVoteShopManager().getDefinition().isReopenGuiOnPurchase()) {
if (category != null) {
new VoteShopCategoryMenu(plugin, player, currentUser, category).open(GUIMethod.CHEST);
} else {
plugin.getCommandLoader().processSlotClick(player, currentUser, "shop");
}
}
}
/**
* Gets the current user.
*
* @param player the player
* @return the user
*/
protected VotingPluginUser getUser(Player player) {
if (user == null) {
user = plugin.getVotingPluginUserManager().getVotingPluginUser(player);
}
return plugin.getVotingPluginUserManager().getVotingPluginUser(player);
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,167 +0,0 @@
package com.bencodez.votingplugin.commands.gui.player;
import java.util.ArrayList;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.simpleapi.messages.MessageAPI;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.voteshop.shop.VoteShopCategory;
import com.bencodez.votingplugin.voteshop.shop.VoteShopCategoryButton;
import com.bencodez.votingplugin.voteshop.shop.VoteShopEntry;
import com.bencodez.votingplugin.voteshop.shop.VoteShopItem;
/**
* Vote shop category GUI.
*/
public class VoteShopCategoryMenu extends GUIHandler {
private VoteShopCategory category;
private VotingPluginMain plugin;
@SuppressWarnings("unused")
private VotingPluginUser user;
/**
* Creates the GUI.
*
* @param plugin the plugin
* @param player the sender
* @param user the user
* @param category the category
*/
public VoteShopCategoryMenu(VotingPluginMain plugin, CommandSender player, VotingPluginUser user,
VoteShopCategory category) {
super(plugin, player);
this.plugin = plugin;
this.user = user;
this.category = category;
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onDialog(Player player) {
}
@Override
public void onChest(Player player) {
if (!plugin.getVoteShopManager().getDefinition().isEnabled()) {
player.sendMessage(MessageAPI.colorize(plugin.getVoteShopManager().getDefinition().getDisabledMessage()));
return;
}
VotingPluginUser currentUser = plugin.getVotingPluginUserManager().getVotingPluginUser(player);
BInventory inv = new BInventory(category.getName());
inv.addPlaceholder("points", String.valueOf(currentUser.getPoints()));
inv.addPlaceholder("sitesavailable", String.valueOf(currentUser.getSitesNotVotedOn()));
inv.dontClose();
for (VoteShopEntry entry : category.getEntries().values()) {
if (entry instanceof VoteShopItem) {
addItemButton(inv, player, currentUser, (VoteShopItem) entry);
} else if (entry instanceof VoteShopCategoryButton) {
addCategoryButton(inv, player, currentUser, (VoteShopCategoryButton) entry);
}
}
if (category.isBackButton()) {
addBackButton(inv, player, currentUser, category.getId());
}
inv.openInventory(player);
}
private void addBackButton(BInventory inv, Player player, VotingPluginUser user, String category) {
if (!plugin.getShopFile().isCategoryBackButtonEnabled(category)) {
return;
}
ConfigurationSection section = plugin.getShopFile().getCategoryBackButtonItem(category);
if (section == null) {
section = plugin.getShopFile().getDefaultBackButtonItem();
}
ItemBuilder builder;
if (section != null) {
builder = new ItemBuilder(section);
} else {
builder = new ItemBuilder(Material.ARROW);
builder.setName("&cBack");
}
inv.addButton(new BInventoryButton(builder) {
@Override
public void onClick(ClickEvent event) {
new VoteShop(plugin, player, user).open();
}
});
}
/**
* Adds an item button.
*
* @param inv the inventory
* @param player the player
* @param currentUser the user
* @param item the item
*/
protected void addItemButton(BInventory inv, final Player player, final VotingPluginUser currentUser,
final VoteShopItem item) {
new VoteShop(plugin, player, currentUser).addItemButton(inv, player, currentUser, item, category);
}
/**
* Adds a category button.
*
* @param inv the inventory
* @param player the player
* @param currentUser the user
* @param button the button
*/
protected void addCategoryButton(BInventory inv, final Player player, final VotingPluginUser currentUser,
final VoteShopCategoryButton button) {
inv.addButton(new BInventoryButton(new ItemBuilder(button.getDisplaySection())) {
@Override
public void onClick(ClickEvent event) {
VoteShopCategory target = plugin.getVoteShopManager().getCategory(button.getCategoryId());
if (target != null) {
new VoteShopCategoryMenu(plugin, event.getPlayer(), currentUser, target).open(GUIMethod.CHEST);
}
}
});
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,158 +0,0 @@
package com.bencodez.votingplugin.commands.gui.player;
import java.util.ArrayList;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.gui.GUIHandler;
import com.bencodez.advancedcore.api.gui.GUIMethod;
import com.bencodez.advancedcore.api.inventory.BInventory;
import com.bencodez.advancedcore.api.inventory.BInventory.ClickEvent;
import com.bencodez.advancedcore.api.inventory.BInventoryButton;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.simpleapi.player.PlayerUtils;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.voteshop.service.VoteShopPurchaseResult;
import com.bencodez.votingplugin.voteshop.shop.VoteShopCategory;
import com.bencodez.votingplugin.voteshop.shop.VoteShopItem;
/**
* Confirmation GUI for vote shop purchases.
*/
public class VoteShopConfirm extends GUIHandler {
private VoteShopCategory category;
private VoteShopItem item;
private VotingPluginMain plugin;
private VotingPluginUser user;
/**
* Creates the GUI.
*
* @param plugin the plugin
* @param player the sender
* @param user the user
* @param item the item
* @param category the category or null
*/
public VoteShopConfirm(VotingPluginMain plugin, CommandSender player, VotingPluginUser user, VoteShopItem item,
VoteShopCategory category) {
super(plugin, player);
this.plugin = plugin;
this.user = user;
this.item = item;
this.category = category;
}
@Override
public ArrayList<String> getChat(CommandSender sender) {
return null;
}
@Override
public void onBook(Player player) {
}
@Override
public void onChat(CommandSender sender) {
}
@Override
public void onChest(final Player player) {
PlayerUtils.setPlayerMeta(plugin, player, "ident", item.getIdentifier());
BInventory inv = new BInventory(plugin.getShopFile().getShopConfirmPurchaseTitle());
inv.dontClose();
inv.addButton(new BInventoryButton(new ItemBuilder(plugin.getShopFile().getShopConfirmPurchaseYesItem())) {
@Override
public void onClick(ClickEvent event) {
user.cache();
VoteShopPurchaseResult result = plugin.getVoteShopManager().purchase(player, user, item);
if (result != VoteShopPurchaseResult.SUCCESS) {
plugin.getVoteShopManager().getPurchaseService().sendFailureMessage(player, user, item, result);
returnToPrevious(event.getPlayer());
return;
}
plugin.getCommandLoader().processSlotClick(player, user, item.getIdentifier());
if (item.isCloseGUI()) {
event.closeInventory();
} else {
returnToPrevious(event.getPlayer());
}
}
});
inv.addButton(new BInventoryButton(new ItemBuilder(plugin.getShopFile().getShopConfirmPurchaseNoItem())) {
@Override
public void onClick(ClickEvent event) {
if (item.isCloseGUI()) {
event.closeInventory();
} else {
returnToPrevious(event.getPlayer());
}
}
});
inv.openInventory(player);
}
@Override
public void onDialog(Player player) {
PlayerUtils.setPlayerMeta(plugin, player, "ident", item.getIdentifier());
plugin.getDialogService().confirmation(player).placeholder("player", user.getPlayerName())
.placeholder("identifier", item.getIdentifier())
.title(plugin.getShopFile().getShopConfirmPurchaseTitle())
.body("&7Confirm purchase of &e%identifier%&7?")
.yesText(new ItemBuilder(plugin.getShopFile().getShopConfirmPurchaseYesItem()).getName())
.noText(new ItemBuilder(plugin.getShopFile().getShopConfirmPurchaseNoItem()).getName())
.onYes(payload -> {
Player clicked = player.getServer().getPlayer(payload.owner());
if (clicked == null) {
return;
}
user.cache();
VoteShopPurchaseResult result = plugin.getVoteShopManager().purchase(clicked, user, item);
if (result != VoteShopPurchaseResult.SUCCESS) {
plugin.getVoteShopManager().getPurchaseService().sendFailureMessage(clicked, user, item,
result);
returnToPrevious(clicked);
return;
}
plugin.getCommandLoader().processSlotClick(clicked, user, item.getIdentifier());
if (!item.isCloseGUI()) {
returnToPrevious(clicked);
}
}).onNo(payload -> {
Player clicked = player.getServer().getPlayer(payload.owner());
if (clicked != null) {
returnToPrevious(clicked);
}
}).open();
}
/**
* Returns to the previous GUI.
*
* @param player the player
*/
protected void returnToPrevious(Player player) {
if (category != null) {
new VoteShopCategoryMenu(plugin, player, user, category).open(GUIMethod.CHEST);
} else {
new VoteShop(plugin, player, user).open(GUIMethod.CHEST);
}
}
@Override
public void open() {
open(GUIMethod.CHEST);
}
}
@@ -1,141 +0,0 @@
package com.bencodez.votingplugin.commands.tabcompleter;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandSendEvent;
import com.bencodez.votingplugin.VotingPluginMain;
/**
* Filters VotingPlugin command aliases from tab completion when aliases are
* disabled.
* <p>
* This listener ONLY hides aliases that are explicitly listed in VotingPlugin's
* plugin.yml under the "aliases:" section for the "vote" and "adminvote"
* commands.
* <p>
* It will NOT hide unrelated commands such as "/votechallenge" unless that
* exact command name is an alias defined in VotingPlugin's plugin.yml.
*/
public class AliasCommandFilterListener implements Listener {
private final VotingPluginMain plugin;
/**
* Cached lowercase alias names (without '/') that should be hidden when alias
* loading is disabled.
*/
private final Set<String> aliasesToHide;
/**
* Creates a new listener and caches aliases from VotingPlugin's plugin.yml.
*
* @param plugin VotingPlugin main instance
*/
public AliasCommandFilterListener(VotingPluginMain plugin) {
this.plugin = plugin;
this.aliasesToHide = buildAliasesToHide(plugin);
}
/**
* Removes VotingPlugin aliases from the player's available command list when
* alias loading is disabled.
*
* @param event Bukkit command send event
*/
@EventHandler
public void onPlayerCommandSend(PlayerCommandSendEvent event) {
// Only hide when aliases feature is disabled
if (plugin.getConfigFile().isLoadCommandAliases()) {
return;
}
// Remove ONLY exact alias command names (not "vote*" prefixes)
event.getCommands().removeIf(this::shouldHideCommand);
}
/**
* Determines whether a specific command should be hidden.
*
* @param cmd Command name (without '/'), possibly namespaced (e.g.
* "otherplugin:vote")
* @return true if the command should be hidden
*/
private boolean shouldHideCommand(String cmd) {
String name = cmd.toLowerCase(Locale.ROOT);
// Never touch namespaced commands (otherplugin:vote)
if (name.contains(":")) {
return false;
}
// Always keep the primary commands visible
if (name.equals("vote") || name.equals("adminvote")) {
return false;
}
// Hide only exact aliases defined in VotingPlugin plugin.yml
return aliasesToHide.contains(name);
}
/**
* Builds the set of aliases to hide from VotingPlugin's plugin.yml.
* <p>
* Bukkit exposes the plugin.yml "commands" section via
* {@code plugin.getDescription().getCommands()}. Each command entry is a map
* that may contain an "aliases" key.
*
* @param plugin VotingPlugin main instance
* @return a lowercase set of alias command names
*/
private Set<String> buildAliasesToHide(VotingPluginMain plugin) {
Set<String> aliases = new HashSet<>();
Map<String, Map<String, Object>> commands = plugin.getDescription().getCommands();
if (commands == null || commands.isEmpty()) {
return Collections.emptySet();
}
// Only consider aliases for these main commands
addAliasesFor(commands, "vote", aliases);
addAliasesFor(commands, "adminvote", aliases);
return Collections.unmodifiableSet(aliases);
}
/**
* Adds aliases for a given command from the plugin.yml commands map into the
* provided set.
*
* @param commandsMap full commands map from PluginDescriptionFile
* @param commandName the primary command to read aliases for (e.g. "vote")
* @param out output set to add lowercase alias names into
*/
@SuppressWarnings("unchecked")
private static void addAliasesFor(Map<String, Map<String, Object>> commandsMap, String commandName,
Set<String> out) {
Map<String, Object> cmdInfo = commandsMap.get(commandName);
if (cmdInfo == null) {
return;
}
Object aliasesObj = cmdInfo.get("aliases");
if (aliasesObj instanceof String) {
// plugin.yml also allows a single alias as a string
out.add(((String) aliasesObj).toLowerCase(Locale.ROOT));
} else if (aliasesObj instanceof Collection) {
for (Object o : (Collection<Object>) aliasesObj) {
if (o != null) {
out.add(o.toString().toLowerCase(Locale.ROOT));
}
}
}
}
}
@@ -1,435 +0,0 @@
package com.bencodez.votingplugin.cooldown;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import com.bencodez.advancedcore.api.rewards.RewardOptions;
import com.bencodez.advancedcore.api.user.AdvancedCoreUser;
import com.bencodez.advancedcore.api.user.UserDataFetchMode;
import com.bencodez.advancedcore.api.user.UserStartup;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.events.PlayerVoteCoolDownEndEvent;
import com.bencodez.votingplugin.events.PlayerVoteSiteCoolDownEndEvent;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votesites.NextSite;
import com.bencodez.votingplugin.votesites.VoteSite;
import lombok.Getter;
/**
* Manages cooldown checks for vote rewards.
*/
public class CoolDownCheck implements Listener {
private final VotingPluginMain plugin;
private final Map<UUID, ScheduledFuture<?>> perSiteTasks = new ConcurrentHashMap<>();
private final Map<UUID, ScheduledFuture<?>> allSiteTasks = new ConcurrentHashMap<>();
@Getter
private final ScheduledExecutorService timer;
private volatile boolean cooldownCheckEnabled = false;
/**
* Constructs a new cooldown check manager.
*
* @param plugin the plugin instance
*/
public CoolDownCheck(VotingPluginMain plugin) {
this.plugin = plugin;
// Better behavior when you cancel+reschedule a lot (removes cancelled tasks from queue)
ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1);
exec.setRemoveOnCancelPolicy(true);
this.timer = exec;
}
/**
* Checks the cooldown for a player by UUID.
*
* @param uuid the player UUID
*/
public void check(UUID uuid) {
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
if (!user.isOnline()) {
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
}
check(user);
}
/**
* Shuts down the cooldown check timer.
*/
public void shutdown() {
timer.shutdownNow();
for (ScheduledFuture<?> f : perSiteTasks.values()) {
if (f != null) {
f.cancel(false);
}
}
for (ScheduledFuture<?> f : allSiteTasks.values()) {
if (f != null) {
f.cancel(false);
}
}
perSiteTasks.clear();
allSiteTasks.clear();
}
/**
* Checks the cooldown for a user.
*
* @param user the voting plugin user
*/
public synchronized void check(VotingPluginUser user) {
if (!user.getCoolDownCheck()) {
return;
}
if (user.canVoteAll()) {
user.setCoolDownCheck(false);
PlayerVoteCoolDownEndEvent event = new PlayerVoteCoolDownEndEvent(user);
plugin.getServer().getPluginManager().callEvent(event);
ScheduledFuture<?> f = allSiteTasks.remove(user.getJavaUUID());
if (f != null) {
f.cancel(false);
}
return;
}
// Keep per-site scheduling going while waiting for "all sites available"
schedulePerSite(user, false);
}
/**
* Check ONLY a specific votesite for all users (uses streaming keys/cols).
*
* @param site the vote site to check
*/
public void checkAllVoteSite(VoteSite site) {
if (site == null) {
return;
}
plugin.getLogger().info("Checking vote cooldown rewards for all players: " + site.getKey());
plugin.getUserManager().forEachUserKeys((uuid, cols) -> {
if (!plugin.isEnabled() || uuid == null) {
return;
}
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, false);
if (user == null) {
return;
}
user.userDataFetechMode(UserDataFetchMode.TEMP_ONLY);
try {
if (cols != null) {
user.updateTempCacheWithColumns(cols);
cols.clear(); // help GC (only useful if reused)
}
checkPerSite(site, user);
} finally {
user.clearTempCache();
}
}, (count) -> plugin.getLogger().info("Finished checking vote cooldown rewards for all players: " + site.getKey()
+ " (processed: " + count + ")"));
}
/**
* Checks if cooldown check is enabled.
*/
public void checkEnabled() {
cooldownCheckEnabled = !plugin.getConfigFile().isDisableCoolDownCheck()
&& plugin.getRewardHandler().hasRewards(plugin.getSpecialRewardsConfig().getData(),
"VoteCoolDownEndedReward");
}
/**
* Checks per-site cooldown for a player by UUID.
*
* @param uuid the player UUID
*/
public void checkPerSite(UUID uuid) {
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
if (!user.isOnline()) {
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
}
checkPerSite(user);
}
/**
* Checks per-site cooldown for a user.
*
* @param user the voting plugin user
*/
public synchronized void checkPerSite(VotingPluginUser user) {
if (!plugin.getConfigFile().isPerSiteCoolDownEvents()) {
return;
}
HashMap<String, Boolean> coolDownChecks = user.getCoolDownCheckSiteList();
boolean changed = false;
for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) {
if (site == null) {
continue;
}
if (!Boolean.TRUE.equals(coolDownChecks.get(site.getKey()))) {
if (user.canVoteSite(site)) {
coolDownChecks.put(site.getKey(), Boolean.TRUE);
changed = true;
plugin.extraDebug("Triggering votesitecooldownend event for " + user.getUUID());
PlayerVoteSiteCoolDownEndEvent event = new PlayerVoteSiteCoolDownEndEvent(user, site);
plugin.getServer().getPluginManager().callEvent(event);
}
}
}
if (changed) {
user.setCoolDownCheckSite(coolDownChecks);
}
schedulePerSite(user, false);
}
/**
* Site-specific version used by batch checks / checkAllVoteSite.
*/
private void checkPerSite(VoteSite site, VotingPluginUser user) {
if (!plugin.getConfigFile().isPerSiteCoolDownEvents() || site == null) {
return;
}
HashMap<String, Boolean> coolDownChecks = user.getCoolDownCheckSiteList();
if (Boolean.TRUE.equals(coolDownChecks.get(site.getKey()))) {
return;
}
if (user.canVoteSite(site)) {
coolDownChecks.put(site.getKey(), Boolean.TRUE);
user.setCoolDownCheckSite(coolDownChecks);
plugin.extraDebug(
"Triggering votesitecooldownend event on site " + site.getKey() + " for " + user.getUUID());
PlayerVoteSiteCoolDownEndEvent event = new PlayerVoteSiteCoolDownEndEvent(user, site);
plugin.getServer().getPluginManager().callEvent(event);
}
}
/**
* Loads the cooldown check system.
*/
public void load() {
plugin.addUserStartup(new UserStartup() {
@Override
public void onFinish() {
checkAllVoteSites();
}
@Override
public void onStart() {
}
@Override
public void onStartUp(AdvancedCoreUser advancedcoreUser) {
}
});
}
/**
* Handles the cooldown end event.
*
* @param event the cooldown end event
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onCoolDownEnd(PlayerVoteCoolDownEndEvent event) {
plugin.getRewardHandler().giveReward(event.getPlayer(), plugin.getSpecialRewardsConfig().getData(),
"VoteCoolDownEndedReward", new RewardOptions());
}
/**
* Handles the per-site cooldown end event.
*
* @param event the per-site cooldown end event
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onCoolDownEnd(PlayerVoteSiteCoolDownEndEvent event) {
plugin.getRewardHandler().giveReward(event.getPlayer(), event.getSite().getSiteData(), "CoolDownEndRewards",
new RewardOptions().addPlaceholder("sitename", event.getSite().getDisplayName())
.addPlaceholder("url", event.getSite().getVoteURL(false)));
}
/**
* Schedules a cooldown check for a user.
*
* @param user the voting plugin user
* @param force whether to force the schedule
*/
public void schedule(VotingPluginUser user, boolean force) {
if (user.getSitesVotedOn() <= 0 && !force) {
return;
}
final UUID uuid = user.getJavaUUID();
final long time = user.getNextTimeAllSitesAvailable();
allSiteTasks.compute(uuid, (k, existing) -> {
if (existing != null) {
existing.cancel(false);
}
if (time <= 0) {
plugin.extraDebug(user.getUUID() + "/" + user.getPlayerName() + " not scheduling cooldown check");
return null; // remove entry
}
user.setCoolDownCheck(true);
return timer.schedule(() -> {
if (plugin.isEnabled()) {
check(uuid);
}
}, time + 2, TimeUnit.SECONDS);
});
}
/**
* Schedules per-site cooldown checks for a user.
*
* @param user the voting plugin user
* @param force whether to force the schedule
*/
public void schedulePerSite(VotingPluginUser user, boolean force) {
if (!plugin.getConfigFile().isPerSiteCoolDownEvents()) {
return;
}
if (user.getSitesVotedOn() <= 0 && !force) {
return;
}
final UUID uuid = user.getJavaUUID();
final NextSite next = user.getNextSiteAvailable();
perSiteTasks.compute(uuid, (k, existing) -> {
if (existing != null) {
existing.cancel(false);
}
if (next == null || next.getSite() == null || next.getSecondsUntilAvailable() <= 0) {
plugin.extraDebug(user.getUUID() + "/" + user.getPlayerName() + " not scheduling per-site cooldown check");
return null; // remove entry
}
plugin.devDebug("PerSiteCoolDownEvent schedule: site=" + next.getSite().getKey() + " time="
+ next.getSecondsUntilAvailable() + " seconds");
return timer.schedule(() -> {
if (!plugin.isEnabled()) {
return;
}
// Only check the next site; this will set the cooldown-end flag + fire event if ready
checkPerSite(next.getSite(), user);
// Schedule the following site (if any)
schedulePerSite(user, false);
}, next.getSecondsUntilAvailable() + 2, TimeUnit.SECONDS);
});
}
/**
* Handles a vote event for cooldown scheduling.
*
* @param user the voting plugin user
* @param site the vote site
*/
public void vote(VotingPluginUser user, VoteSite site) {
if (cooldownCheckEnabled) {
schedule(user, true);
}
if (plugin.getConfigFile().isPerSiteCoolDownEvents()) {
user.setCoolDownCheckSite(site, Boolean.FALSE);
schedulePerSite(user, true);
}
}
/**
* Performance-optimized: scan users ONCE, process all daily-delay sites.
*/
public void checkAllVoteSites() {
final List<VoteSite> sites = plugin.getVoteSiteManager().getVoteSites();
if (sites == null || sites.isEmpty()) {
return;
}
final ArrayList<VoteSite> dailySites = new ArrayList<>();
for (int i = 0; i < sites.size(); i++) {
VoteSite site = sites.get(i);
if (site != null && site.isVoteDelayDaily()) {
dailySites.add(site);
}
}
if (dailySites.isEmpty()) {
return;
}
plugin.debug("Checking vote cooldown rewards for all players across " + dailySites.size() + " sites...");
plugin.getUserManager().forEachUserKeys((uuid, cols) -> {
if (!plugin.isEnabled() || uuid == null) {
return;
}
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, false);
if (user == null) {
return;
}
user.userDataFetechMode(UserDataFetchMode.TEMP_ONLY);
try {
if (cols != null) {
user.updateTempCacheWithColumns(cols);
cols.clear();
}
for (int i = 0; i < dailySites.size(); i++) {
checkPerSite(dailySites.get(i), user);
}
check(user);
} finally {
user.clearTempCache();
}
}, (count) -> plugin.debug("Finished checking vote cooldown rewards (processed: " + count + " users)"));
}
}
@@ -1,84 +0,0 @@
package com.bencodez.votingplugin.events;
import lombok.Getter;
/**
* Enumeration for different special reward types.
*/
public enum SpecialRewardType {
/** @deprecated All site reward. */
@Deprecated
ALLSITE,
/** @deprecated Cumulative reward. */
@Deprecated
CUMMULATIVE,
/** @deprecated First vote reward. */
@Deprecated
FIRSTVOTE,
/** @deprecated First vote today reward. */
@Deprecated
FIRSTVOTETODAY,
/** @deprecated Milestone reward. */
@Deprecated
MILESTONE,
/** Vote streak reward. */
VOTESTREAK,
/** @deprecated Almost all sites reward. */
@Deprecated
ALMOSTALLSITES,
/** Top voter reward. */
TOPVOTER,
/** Vote streaks reward. */
VOTESTREAKS;
@Getter
private int amount = -1;
@Getter
private String type = "";
/**
* Sets the amount for this special reward.
*
* @param num the amount
* @return this special reward type
*/
public SpecialRewardType setAmount(int num) {
amount = num;
return this;
}
/**
* Sets the type for this special reward.
*
* @param type the type
* @return this special reward type
*/
public SpecialRewardType setType(String type) {
this.type = type;
return this;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(name());
if (amount >= 0) {
sb.append("(amount=").append(amount);
}
if (type != null && !type.isEmpty()) {
if (amount >= 0) {
sb.append(", ");
} else {
sb.append("(");
}
sb.append("type=").append(type).append(")");
} else if (amount >= 0) {
sb.append(")");
}
return sb.toString();
}
}
@@ -1,159 +0,0 @@
package com.bencodez.votingplugin.events;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import com.bencodez.votingplugin.specialrewards.votemilestones.VoteMilestone;
import com.bencodez.votingplugin.user.VotingPluginUser;
import lombok.Getter;
/**
* Fired after a VoteMilestone reward has been executed successfully.
*/
public class VoteMilestoneRewardEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final VotingPluginUser user;
private final UUID uuid;
private final String playerName;
private final VoteMilestone milestone;
private final long value;
private final String groupId;
private final String rewardPath;
@Getter
private final UUID voteUUID;
private final Map<String, String> placeholders;
/**
* Constructs a new VoteMilestoneRewardEvent.
*
* @param user VotingPluginUser who received the reward
* @param milestone milestone that triggered
* @param value resolved total value used for matching
* @param groupId resolved group id used for selection
* @param rewardPath path used to execute rewards
* @param placeholders placeholders passed to the reward handler
* @param voteUUID UUID of the vote that triggered this reward (may be null)
*/
public VoteMilestoneRewardEvent(VotingPluginUser user, VoteMilestone milestone, long value, String groupId,
String rewardPath, Map<String, String> placeholders, UUID voteUUID) {
super(true);
this.user = user;
this.uuid = user == null ? null : user.getJavaUUID();
this.playerName = user == null ? null : user.getPlayerName();
this.milestone = milestone;
this.value = value;
this.groupId = groupId;
this.rewardPath = rewardPath;
this.voteUUID = voteUUID;
if (placeholders == null || placeholders.isEmpty()) {
this.placeholders = Collections.emptyMap();
} else {
this.placeholders = Collections.unmodifiableMap(new HashMap<>(placeholders));
}
}
/**
* Gets the voting plugin user associated with this event.
*
* @return the voting plugin user
*/
public VotingPluginUser getUser() {
return user;
}
/**
* Gets the player UUID.
*
* @return the player UUID
*/
public UUID getUuid() {
return uuid;
}
/**
* Gets the player name.
*
* @return the player name
*/
public String getPlayerName() {
return playerName;
}
/**
* Gets the milestone that triggered this event.
*
* @return milestone instance
*/
public VoteMilestone getMilestone() {
return milestone;
}
/**
* Gets the milestone ID.
*
* @return milestone id string
*/
public String getMilestoneId() {
return milestone == null ? null : milestone.getId();
}
/**
* Gets the value that triggered the milestone.
*
* @return the milestone value
*/
public long getValue() {
return value;
}
/**
* Gets the group ID used for selection.
*
* @return group id string
*/
public String getGroupId() {
return groupId;
}
/**
* Gets the configuration path for the rewards.
*
* @return the reward path
*/
public String getRewardPath() {
return rewardPath;
}
/**
* Gets the placeholders map for this milestone reward.
*
* @return map of placeholder names to values
*/
public Map<String, String> getPlaceholders() {
return placeholders;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
/**
* Gets the handler list for this event.
*
* @return handler list
*/
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,51 +0,0 @@
package com.bencodez.votingplugin.events;
import java.util.UUID;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import com.bencodez.votingplugin.user.VotingPluginUser;
import lombok.Getter;
/**
* Fired when a player attempts to purchase an item from VotingPlugin's
* VoteShop.
*/
public class VoteShopPurchaseEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
@Getter
private UUID playerUuid;
@Getter
private String playerName;
@Getter
private VotingPluginUser user;
@Getter
private String identifier;
@Getter
private int cost;
public VoteShopPurchaseEvent(UUID playerUUID, String playerName, VotingPluginUser user, String identifier,
int cost) {
super(true);
this.playerName = playerName;
this.user = user;
this.playerUuid = playerUUID;
this.identifier = identifier;
this.cost = cost;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,127 +0,0 @@
package com.bencodez.votingplugin.listeners;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import com.bencodez.advancedcore.api.player.UuidLookup;
import com.bencodez.advancedcore.api.user.UserDataFetchMode;
import com.bencodez.advancedcore.listeners.AdvancedCoreLoginEvent;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
import com.bencodez.votingplugin.user.VotingPluginUser;
/**
* The Class PlayerJoinEvent.
*/
public class PlayerJoinEvent implements Listener {
/** The plugin. */
private final VotingPluginMain plugin;
/**
* Instantiates a new player join event.
*
* @param plugin the plugin
*/
public PlayerJoinEvent(VotingPluginMain plugin) {
this.plugin = plugin;
}
private static boolean isBlank(String s) {
return s == null || s.trim().isEmpty() || "null".equalsIgnoreCase(s.trim());
}
/**
* On AdvancedCore login event (post-auth / delayed login).
*
* @param event the event
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerLogin(AdvancedCoreLoginEvent event) {
if (event == null || !plugin.isMySQLOkay() || event.isCancelled() || event.getUser() == null) {
return;
}
// UUID is authoritative here (String)
String uuid = event.getUuid();
if (isBlank(uuid)) {
return;
}
// "Has data" should come from AdvancedCore event (storage presence)
boolean hasData = event.isUserInStorage();
// Resolve VotingPluginUser by the storage UUID string (offline-mode
// name-derived UUID)
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
if (user == null) {
return;
}
Player player = event.getPlayer();
if (player != null && player.isOp() && plugin.isYmlError()) {
user.sendMessage("&cVotingPlugin: Detected yml error, please check console for details");
}
if (hasData) {
// give offline vote (if they voted offline)
user.offVote();
} else {
plugin.debug("No data detected for " + user.getUUID() + "/" + user.getPlayerName());
}
user.loginRewards();
plugin.getPlaceholders().onUpdate(user, true);
if (plugin.getBungeeSettings().isUseBungeecoord()) {
plugin.getBungeeHandler().getGlobalMessageHandler().sendMessage(VotingPluginWire.login(user.getPlayerName(),
user.getUUID(), plugin.getBungeeSettings().getServer()));
}
}
/**
* Handles player quit events.
*
* @param event the player quit event
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
if (plugin == null || !plugin.isEnabled() || event == null) {
return;
}
final Player player = event.getPlayer();
if (player == null) {
return;
}
plugin.getLoginTimer().execute(new Runnable() {
@Override
public void run() {
VotingPluginMain.plugin.getAdvancedTab().remove(player.getUniqueId());
// Use AdvancedCore UUIDLookup to derive the correct UUID (online/offline aware)
String uuid = UuidLookup.getInstance().getUUID(player.getName());
if (isBlank(uuid)) {
// Fallback to online UUID if lookup fails
uuid = player.getUniqueId().toString();
}
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
if (user == null) {
return;
}
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
user.logoutRewards();
plugin.getPlaceholders().onLogout(user);
}
});
}
}
@@ -1,75 +0,0 @@
package com.bencodez.votingplugin.placeholders;
/**
* Enum for placeholder cache levels.
*/
public enum PlaceholderCacheLevel {
/** No caching. */
NONE,
/** Specific placeholders cached. */
SPECIFIC,
/** Auto-cache for online players. */
AUTO,
/** Auto-cache for all players. */
AUTOALL,
/** Specific cache for all players. */
SPECIFICALL;
/**
* Get cache level from string.
* @param str the string to parse
* @return the cache level
*/
public static PlaceholderCacheLevel getCache(String str) {
for (PlaceholderCacheLevel v : PlaceholderCacheLevel.values()) {
if (v.toString().equalsIgnoreCase(str)) {
return v;
}
}
return AUTO;
}
/**
* Check if cache is for online players only.
* @return true if online only
*/
public boolean onlineOnly() {
if ((this == AUTOALL) || (this == SPECIFICALL)) {
return false;
}
return true;
}
/**
* Check if caching should be enabled.
* @return true if should cache
*/
public boolean shouldCache() {
switch (this) {
case AUTO:
return true;
case NONE:
return false;
case SPECIFIC:
return false;
case SPECIFICALL:
return false;
case AUTOALL:
return true;
default:
return false;
}
}
/**
* Check if cache should always be used.
* @return true if cache always
*/
public boolean isCacheAlways() {
if ((this == AUTOALL) || (this == SPECIFICALL)) {
return true;
}
return false;
}
}
@@ -1,26 +0,0 @@
package com.bencodez.votingplugin.presets;
import lombok.Getter;
import lombok.Setter;
/**
* Represents display information for a preset. Display information
* includes a name and a description used to describe the preset to
* users. This class is used when parsing meta JSON files.
*/
@Getter
@Setter
public class DisplayInfo {
/**
* The human‑readable name of the preset. This name will appear in
* preset selection UIs.
*/
private String name;
/**
* A brief description of the preset. This description helps users
* understand what the preset is intended for.
*/
private String description;
}
@@ -1,30 +0,0 @@
package com.bencodez.votingplugin.presets;
import lombok.Getter;
import lombok.Setter;
/**
* Represents a fragment defined in a preset meta JSON file. A
* fragment contains the path to a YAML file and a path indicating
* where the fragment should be merged into when applying the preset.
* In the context of using only values from JSON (without YAML
* fragments), this class is unused but included for completeness.
*/
@Getter
@Setter
public class Fragment {
/**
* The relative path to the fragment file. This file typically
* contains a YAML snippet defining a portion of the configuration.
*/
private String path;
/**
* The dot‑separated path within the configuration to merge the
* fragment into. For example, {@code VoteSites} indicates the
* fragment should be merged into the {@code VoteSites} section of
* {@code VoteSites.yml}.
*/
private String mergeInto;
}
@@ -1,199 +0,0 @@
package com.bencodez.votingplugin.presets;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* Utility class for loading vote site presets from GitHub.
*/
public class GitHubVoteSitePresetLoader {
private final String owner;
private final String repository;
private final String branch;
private final String token;
private final Gson gson;
/**
* Shared HTTP client.
*/
private final HttpClient httpClient = HttpClient.newBuilder()
.followRedirects(Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(5))
.build();
public GitHubVoteSitePresetLoader(String owner, String repository, String branch) {
this(owner, repository, branch, null);
}
public GitHubVoteSitePresetLoader(String owner, String repository, String branch, String token) {
this.owner = Objects.requireNonNull(owner);
this.repository = Objects.requireNonNull(repository);
this.branch = Objects.requireNonNull(branch);
this.token = token;
this.gson = new Gson();
}
public List<String> listPresetPaths() throws IOException, InterruptedException {
String apiUrl = String.format(
"https://api.github.com/repos/%s/%s/contents/presets/votesites?ref=%s",
owner, repository, branch);
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.GET()
.timeout(Duration.ofSeconds(7))
.header("User-Agent", "VotingPlugin-PresetLoader");
if (token != null && !token.isEmpty()) {
builder.header("Authorization", "token " + token);
}
HttpResponse<String> response = httpClient.send(builder.build(),
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() != 200) {
return Collections.emptyList();
}
JsonElement root = JsonParser.parseString(response.body());
if (!root.isJsonArray()) {
return Collections.emptyList();
}
JsonArray array = root.getAsJsonArray();
List<String> paths = new ArrayList<>();
for (JsonElement element : array) {
if (!element.isJsonObject()) continue;
JsonObject obj = element.getAsJsonObject();
String type = obj.has("type") ? obj.get("type").getAsString() : null;
String path = obj.has("path") ? obj.get("path").getAsString() : null;
if ("file".equals(type) && path != null && path.endsWith(".meta.json")) {
paths.add(path);
}
}
return paths;
}
public VoteSitePreset loadPreset(String path) throws IOException, InterruptedException {
Objects.requireNonNull(path, "path must not be null");
String rawUrl = String.format(
"https://raw.githubusercontent.com/%s/%s/%s/%s",
owner, repository, branch, path);
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(rawUrl))
.GET()
.timeout(Duration.ofSeconds(7))
.header("User-Agent", "VotingPlugin-PresetLoader");
if (token != null && !token.isEmpty()) {
builder.header("Authorization", "token " + token);
}
HttpResponse<String> response = httpClient.send(builder.build(),
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() != 200) {
return null;
}
try (Reader reader = new InputStreamReader(
new java.io.ByteArrayInputStream(response.body().getBytes(StandardCharsets.UTF_8)),
StandardCharsets.UTF_8)) {
return gson.fromJson(reader, VoteSitePreset.class);
}
}
public List<VoteSitePreset> listAllVoteSitePresets() throws IOException, InterruptedException {
List<String> paths = listPresetPaths();
if (paths.isEmpty()) {
return Collections.emptyList();
}
List<VoteSitePreset> result = new ArrayList<>();
for (String path : paths) {
try {
VoteSitePreset preset = loadPreset(path);
if (preset != null) {
result.add(preset);
}
} catch (Exception ignored) {
}
}
return result;
}
public VoteSitePreset findVoteSitePresetForURL(String voteURL) throws IOException, InterruptedException {
if (voteURL == null || voteURL.trim().isEmpty()) {
return null;
}
String host;
try {
java.net.URI uri = new java.net.URI(voteURL.trim());
host = uri.getHost();
} catch (Exception e) {
return null;
}
if (host == null || host.isEmpty()) {
return null;
}
host = host.toLowerCase();
if (host.startsWith("www.")) {
host = host.substring(4);
}
List<VoteSitePreset> presets = listAllVoteSitePresets();
for (VoteSitePreset preset : presets) {
if (preset == null || preset.getMatch() == null || preset.getMatch().getDomains() == null) {
continue;
}
for (String domain : preset.getMatch().getDomains()) {
if (domain == null || domain.isEmpty()) {
continue;
}
String normalizedDomain = domain.toLowerCase();
if (normalizedDomain.startsWith("www.")) {
normalizedDomain = normalizedDomain.substring(4);
}
if (host.equals(normalizedDomain) || host.endsWith("." + normalizedDomain)) {
return preset;
}
}
}
return null;
}
}
@@ -1,28 +0,0 @@
package com.bencodez.votingplugin.presets;
import lombok.Getter;
import lombok.Setter;
/**
* Represents matching information for a preset. Match information
* identifies which domains or keywords should match this preset. This
* class is used for deserializing preset meta JSON files but is
* otherwise optional when programmatically applying the preset.
*/
@Getter
@Setter
public class MatchInfo {
/**
* Domains associated with the preset. These are used by the preset
* generator to automatically select a preset based on a vote site
* hostname.
*/
private java.util.List<String> domains;
/**
* Keywords associated with the preset. These can be used to search
* presets in user interfaces.
*/
private java.util.List<String> keywords;
}
@@ -1,46 +0,0 @@
package com.bencodez.votingplugin.presets;
import lombok.Getter;
import lombok.Setter;
import com.google.gson.annotations.SerializedName;
/**
* Represents a placeholder definition loaded from a preset meta JSON file.
*
* The VotingPlugin presets define a set of placeholders that may be
* substituted when creating vote site entries. Each placeholder has a
* type (for example, {@code string}), a human‑readable label, and a
* default value. This class is a simple container for these fields so
* they can be deserialized via Gson and accessed via Lombok‑generated
* getters and setters.
*/
@Getter
@Setter
public class PlaceholderDef {
/**
* The data type of this placeholder (e.g. {@code string}). The type
* information is primarily for user interfaces and may be ignored when
* programmatically applying the preset.
*/
private String type;
/**
* A human‑readable label describing the placeholder. This is used by
* external tools (such as the web preset generator) to prompt for
* values.
*/
private String label;
/**
* The default value for the placeholder. If the user does not supply a
* value when applying the preset, this value will be used instead.
* <p>
* The JSON meta files use the key {@code "default"} for this value. Because
* {@code default} is a reserved keyword in Java, we cannot name this
* field {@code default}. Instead, we annotate this field with
* {@link SerializedName @SerializedName("default")} so that Gson
* will map the {@code "default"} property in the JSON to this field.
*/
@SerializedName("default")
private String defaultValue;
}
@@ -1,234 +0,0 @@
package com.bencodez.votingplugin.presets;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.config.ConfigVoteSites;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Manager responsible for loading vote site presets defined in JSON and
* applying them to the VotingPlugin configuration. Unlike the web preset
* generator, this manager ignores YAML fragments and relies solely on values
* defined in the meta JSON file.
*/
public class VoteSiteJsonPresetManager {
/**
* The main plugin instance used to access the configuration.
*/
private final VotingPluginMain plugin;
/**
* Cached reference to the vote sites configuration helper.
*/
private final ConfigVoteSites configVoteSites;
/**
* Constructs a new manager for applying vote site presets.
*
* @param plugin the main plugin instance
*/
public VoteSiteJsonPresetManager(VotingPluginMain plugin) {
this.plugin = Objects.requireNonNull(plugin, "plugin must not be null");
this.configVoteSites = plugin.getConfigVoteSites();
}
/**
* Loads a vote site preset from the specified resource path.
*
* @param resourcePath the path to the meta JSON file within the JAR
* @return the deserialized preset
* @throws IOException if the resource cannot be read or parsed
*/
public VoteSitePreset loadPreset(String resourcePath) throws IOException {
InputStream in = plugin.getResource(resourcePath);
if (in == null) {
throw new IOException("Preset resource not found: " + resourcePath);
}
try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
Gson gson = new GsonBuilder().create();
return gson.fromJson(reader, VoteSitePreset.class);
}
}
/**
* Applies the given vote site preset to the VotingPlugin configuration.
* Placeholder values will be populated using the defaults defined in the
* preset and overridden by any values provided in the {@code overrides} map.
*
* @param preset the preset to apply
* @param overrides a map of placeholder names to override values
*/
public void applyPreset(VoteSitePreset preset, Map<String, Object> overrides) {
if (preset == null) {
return;
}
Map<String, Object> values = new HashMap<String, Object>();
if (preset.getPlaceholders() != null) {
for (Map.Entry<String, PlaceholderDef> entry : preset.getPlaceholders().entrySet()) {
PlaceholderDef def = entry.getValue();
if ((def != null) && (def.getDefaultValue() != null)) {
values.put(entry.getKey(), def.getDefaultValue());
}
}
}
if (overrides != null) {
for (Map.Entry<String, Object> entry : overrides.entrySet()) {
if (entry.getValue() != null) {
values.put(entry.getKey(), entry.getValue());
}
}
}
String siteKey = getString(values.get("siteKey")).trim();
String displayName = getString(values.get("displayName"));
String serviceSite = getString(values.get("serviceSite"));
String voteURL = getString(values.get("voteURL"));
String voteDelay = getString(values.get("voteDelay"));
Boolean waitUntilDelay = getBoolean(values.get("WaitUnitVoteDelay"));
if (waitUntilDelay == null) {
waitUntilDelay = getBoolean(values.get("waitUntilVoteDelay"));
}
Boolean voteDelayDaily = getBoolean(values.get("VoteDelayDaily"));
if (voteDelayDaily == null) {
voteDelayDaily = getBoolean(values.get("voteDelayDaily"));
}
Integer voteDelayDailyHour = getInteger(values.get("voteDelayDailyHour"));
if (voteDelayDailyHour == null) {
voteDelayDailyHour = getInteger(values.get("VoteDelayDailyHour"));
}
if (siteKey.isEmpty()) {
plugin.getLogger().warning("Cannot apply preset: siteKey is missing");
return;
}
configVoteSites.setEnabled(siteKey, true);
if (!displayName.isEmpty()) {
configVoteSites.setDisplayName(siteKey, displayName);
}
if (!serviceSite.isEmpty()) {
configVoteSites.setServiceSite(siteKey, serviceSite);
}
if (!voteURL.isEmpty()) {
configVoteSites.setVoteURL(siteKey, voteURL);
}
if (!voteDelay.isEmpty()) {
configVoteSites.setVoteDelay(siteKey, voteDelay);
}
if (waitUntilDelay != null) {
configVoteSites.set(siteKey, "WaitUntilVoteDelay", waitUntilDelay.booleanValue());
}
if (voteDelayDaily != null) {
configVoteSites.setVoteDelayDaily(siteKey, voteDelayDaily.booleanValue());
}
if (voteDelayDailyHour != null) {
configVoteSites.setVoteDelayDailyHour(siteKey, voteDelayDailyHour.intValue());
}
if (configVoteSites.getItem(siteKey) == null) {
configVoteSites.set(siteKey, "DisplayItem.Material", "STONE");
configVoteSites.set(siteKey, "DisplayItem.Amount", 1);
}
if (configVoteSites.getData(siteKey).getConfigurationSection("Rewards") == null) {
configVoteSites.set(siteKey, "Rewards.Messages.Player", "&aThanks for voting on %ServiceSite%!");
}
configVoteSites.saveData();
plugin.loadVoteSites();
}
/**
* Converts an object to a string.
*
* @param value the value
* @return the string form, or an empty string if null
*/
private String getString(Object value) {
return value != null ? String.valueOf(value) : "";
}
/**
* Converts an object to a boolean if possible.
*
* @param value the value
* @return the parsed boolean, or null if not set
*/
private Boolean getBoolean(Object value) {
if (value == null) {
return null;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
String text = String.valueOf(value).trim().toLowerCase();
if (text.isEmpty()) {
return null;
}
if (text.equals("true") || text.equals("yes") || text.equals("1")) {
return Boolean.TRUE;
}
if (text.equals("false") || text.equals("no") || text.equals("0")) {
return Boolean.FALSE;
}
return null;
}
/**
* Converts an object to an integer if possible.
*
* @param value the value
* @return the parsed integer, or null if not set or invalid
*/
private Integer getInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number) {
return Integer.valueOf(((Number) value).intValue());
}
String text = String.valueOf(value).trim();
if (text.isEmpty()) {
return null;
}
try {
return Integer.valueOf(Integer.parseInt(text));
} catch (NumberFormatException e) {
plugin.getLogger().warning("Invalid integer value: " + text);
return null;
}
}
}
@@ -1,62 +0,0 @@
package com.bencodez.votingplugin.presets;
import lombok.Getter;
import lombok.Setter;
import java.util.Map;
import java.util.List;
/**
* Represents a vote site preset as defined in a meta JSON file.
*
* The meta JSON file defines metadata about the preset such as display
* information, matching criteria, placeholder definitions and optional YAML
* fragments. This class provides a simple POJO for deserialization via
* Gson. Only the {@code placeholders} field is strictly necessary when
* applying the preset programmatically; the other fields are included
* for completeness.
*/
@Getter
@Setter
public class VoteSitePreset {
/**
* The schema version of the preset definition. This may be used to
* handle future format changes.
*/
private int schemaVersion;
/**
* The unique identifier of the preset (e.g. {@code votesite:generic}).
*/
private String id;
/**
* Human‑friendly display information for the preset.
*/
private DisplayInfo display;
/**
* Matching information that can be used to automatically select a
* preset based on domain or keywords. Optional when applying
* programmatically.
*/
private MatchInfo match;
/**
* Definitions of placeholders used by this preset. Each entry in
* the map corresponds to a placeholder name and defines its type,
* label and default value.
*/
private Map<String, PlaceholderDef> placeholders;
/**
* A list of YAML fragments referenced by this preset. When not using
* YAML, this field is unused but retained for completeness.
*/
private List<Fragment> fragments;
/**
* Whether the preset has been verified by the project maintainers.
*/
private boolean verified;
}
@@ -1,210 +0,0 @@
package com.bencodez.votingplugin.presets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.bukkit.entity.Player;
import com.bencodez.simpleapi.valuerequest.MultiValueField;
import com.bencodez.simpleapi.valuerequest.MultiValueField.FieldType;
import com.bencodez.simpleapi.valuerequest.MultiValueListener;
import com.bencodez.simpleapi.valuerequest.MultiValueResult;
import com.bencodez.simpleapi.valuerequest.StringListener;
import com.bencodez.simpleapi.valuerequest.ValueRequest;
import com.bencodez.votingplugin.VotingPluginMain;
import lombok.Getter;
/**
* Handler class that guides a player through selecting a vote site preset and
* configuring its placeholder values using the SimpleAPI {@link ValueRequest}
* system.
*/
public class VoteSitePresetSetupHandler {
/**
* Main plugin instance.
*/
private final VotingPluginMain plugin;
/**
* Loader used to fetch vote site presets.
*/
@Getter
private final GitHubVoteSitePresetLoader loader;
/**
* Creates a new setup handler.
*
* @param plugin plugin instance
*/
public VoteSitePresetSetupHandler(VotingPluginMain plugin) {
this.plugin = Objects.requireNonNull(plugin, "plugin must not be null");
this.loader = new GitHubVoteSitePresetLoader("BenCodez", "VotingPlugin-Presets", "main");
}
/**
* Starts the preset setup process.
*
* @param player player running setup
*/
public void startSetup(Player player) {
List<VoteSitePreset> presets;
try {
presets = loader.listAllVoteSitePresets();
} catch (Exception e) {
player.sendMessage("§cUnable to load vote site presets.");
plugin.getLogger().warning("Failed to load vote site presets: " + e.getMessage());
return;
}
if (presets == null || presets.isEmpty()) {
player.sendMessage("§cNo vote site presets are available.");
return;
}
List<String> options = new ArrayList<String>();
for (VoteSitePreset preset : presets) {
options.add(preset.getId());
}
ValueRequest request = new ValueRequest(plugin, plugin.getDialogService());
request.requestString(player, null, options, false, "Select a vote site preset", new StringListener() {
@Override
public void onInput(Player p, String value) {
VoteSitePreset selected = null;
for (VoteSitePreset preset : presets) {
if (preset.getId() != null && preset.getId().equalsIgnoreCase(value)) {
selected = preset;
break;
}
}
if (selected == null) {
p.sendMessage("§cInvalid preset: " + value);
return;
}
promptPlaceholders(p, selected);
}
});
}
/**
* Prompts placeholder values using the multi-value request system.
*
* @param player player
* @param preset preset
*/
public void promptPlaceholders(Player player, VoteSitePreset preset) {
Map<String, PlaceholderDef> defs = preset.getPlaceholders();
if (defs == null || defs.isEmpty()) {
new VoteSiteJsonPresetManager(plugin).applyPreset(preset, new HashMap<String, Object>());
player.sendMessage("§aPreset applied successfully.");
return;
}
List<MultiValueField> fields = new ArrayList<MultiValueField>();
List<String> keys = new ArrayList<String>();
for (Map.Entry<String, PlaceholderDef> entry : defs.entrySet()) {
String key = entry.getKey();
if (key == null) {
continue;
}
// skip auto values
if (key.equalsIgnoreCase("siteKey") || key.equalsIgnoreCase("serviceSite")) {
continue;
}
PlaceholderDef def = entry.getValue();
String label = def != null && def.getLabel() != null ? def.getLabel() : key;
String defaultValue = def != null && def.getDefaultValue() != null ? def.getDefaultValue() : "";
MultiValueField field = createField(key, label, defaultValue);
fields.add(field);
keys.add(key);
}
if (fields.isEmpty()) {
new VoteSiteJsonPresetManager(plugin).applyPreset(preset, new HashMap<String, Object>());
player.sendMessage("§aPreset applied successfully.");
return;
}
ValueRequest request = new ValueRequest(plugin, plugin.getDialogService());
request.requestMultipleValues(player, fields, new MultiValueListener() {
@Override
public void onInput(Player p, MultiValueResult result) {
Map<String, Object> overrides = new HashMap<String, Object>();
for (String key : keys) {
if (isBooleanField(key)) {
Boolean value = result.getBoolean(key);
overrides.put(key, value != null ? value : Boolean.FALSE);
} else {
String value = result.getString(key);
overrides.put(key, value != null ? value : "");
}
}
new VoteSiteJsonPresetManager(plugin).applyPreset(preset, overrides);
p.sendMessage("§aVote site configured and preset applied.");
}
});
}
/**
* Creates a field based on placeholder key.
*
* @param key key
* @param label label
* @param defaultValue default value
* @return field
*/
private MultiValueField createField(String key, String label, String defaultValue) {
if (isBooleanField(key)) {
boolean boolValue = Boolean.parseBoolean(defaultValue);
return new MultiValueField(key, label + " (default: " + boolValue + ")", FieldType.BOOLEAN)
.booleanValue(Boolean.valueOf(boolValue))
.required(false);
}
return new MultiValueField(key, label + " (default: " + defaultValue + ")", FieldType.STRING)
.stringValue(defaultValue)
.required(true);
}
/**
* Determines if the placeholder should be treated as a boolean.
*
* @param key placeholder key
* @return true if boolean field
*/
private boolean isBooleanField(String key) {
return key.equalsIgnoreCase("WaitUntilVoteDelay")
|| key.equalsIgnoreCase("VoteDelayDaily");
}
}
@@ -1,41 +0,0 @@
package com.bencodez.votingplugin.proxy;
/**
* Enumeration of available Bungee connection methods.
*/
public enum BungeeMethod {
/** MySQL database connection. */
MYSQL,
/** Plugin messaging channel. */
PLUGINMESSAGING,
/** Socket connection. */
SOCKETS,
/** Redis connection. */
REDIS,
/** MQTT message broker. */
MQTT;
/**
* Checks if this method requires a player to be online.
*
* @return true if player must be online
*/
public boolean requiresPlayerOnline() {
return this == PLUGINMESSAGING;
}
/**
* Gets a BungeeMethod by name.
*
* @param str the method name
* @return the matching BungeeMethod, or PLUGINMESSAGING if not found
*/
public static BungeeMethod getByName(String str) {
for (BungeeMethod method : values()) {
if (method.toString().equalsIgnoreCase(str)) {
return method;
}
}
return PLUGINMESSAGING;
}
}
@@ -1,81 +0,0 @@
package com.bencodez.votingplugin.proxy;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
/**
* Represents an offline bungee vote.
*/
public class OfflineBungeeVote {
@Getter
private String playerName;
@Getter
private boolean realVote;
@Getter
private String service;
@Getter
@Setter
private String text;
@Getter
private long time;
@Getter
private String uuid;
@Getter
private UUID voteId;
/**
* Constructor with UUID voteId.
* @param voteId the vote ID
* @param playerName the player name
* @param uuid the player UUID
* @param service the vote service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text additional text
*/
public OfflineBungeeVote(UUID voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text) {
this.playerName = playerName;
this.uuid = uuid;
this.service = service;
this.time = time;
this.realVote = realVote;
this.text = text;
this.voteId = voteId;
}
/**
* Constructor with String voteId.
* @param voteId the vote ID as string
* @param playerName the player name
* @param uuid the player UUID
* @param service the vote service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text additional text
*/
public OfflineBungeeVote(String voteId, String playerName, String uuid, String service, long time, boolean realVote,
String text) {
this.playerName = playerName;
this.uuid = uuid;
this.service = service;
this.time = time;
this.realVote = realVote;
this.text = text;
if (voteId != null && !voteId.isEmpty()) {
this.voteId = UUID.fromString(voteId);
} else {
this.voteId = null;
}
}
@Override
public String toString() {
return "VoteCache:" + playerName + "/" + uuid + "/" + service + "/" + time + "/" + realVote + "/" + text + "/"
+ voteId;
}
}
@@ -1,580 +0,0 @@
package com.bencodez.votingplugin.proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import com.bencodez.simpleapi.sql.Column;
import com.bencodez.simpleapi.sql.DataType;
import com.bencodez.simpleapi.sql.data.DataValue;
import com.bencodez.simpleapi.sql.data.DataValueInt;
import com.bencodez.simpleapi.sql.data.DataValueString;
import com.bencodez.simpleapi.sql.mysql.AbstractSqlTable;
import com.bencodez.simpleapi.sql.mysql.DbType;
import com.bencodez.simpleapi.sql.mysql.MySQL;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
import com.bencodez.simpleapi.sql.mysql.queries.Query;
/**
* Proxy MySQL user table for managing voting plugin user data.
*/
public abstract class ProxyMysqlUserTable extends AbstractSqlTable {
// Keep this so existing callers relying on getName() still work
private final String name;
// For int-vs-string decode in getExactQuery (same pattern as before)
private final List<String> intColumns = new ArrayList<>();
// Backwards-compat wrapper (your old code expects getUuids())
private final Set<String> uuidsView;
// ---- Required subclass hooks ----
@Override
public String getPrimaryKeyColumn() {
return "uuid";
}
@Override
public String buildCreateTableSql(DbType dbType) {
// NOTE: AbstractSqlTable.qi(...) already quotes identifiers properly for
// DbType.
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE IF NOT EXISTS ").append(qi(getTableName())).append(" (");
// Best-case uuid type:
// - Postgres: UUID
// - MySQL/MariaDB: VARCHAR(37) (matches existing behavior)
sb.append(qi("uuid")).append(" ").append(bestUuidType()).append(", ");
sb.append("PRIMARY KEY ( ").append(qi("uuid")).append(" )");
sb.append(");");
return sb.toString();
}
// ---- Logging hooks required by AbstractSqlTable ----
@Override
public abstract void logSevere(String msg);
@Override
public abstract void logInfo(String msg);
@Override
public abstract void debug(Throwable t);
// Keep these existing abstract methods for compatibility with your old
// subclasses
/**
* Debug log for SQL exceptions.
* @param e the SQL exception
*/
public abstract void debug(SQLException e);
// ---- Constructors ----
/**
* Constructor with new MySQL connection.
* @param tableName the table name
* @param config the MySQL configuration
* @param debug enable debug mode
*/
public ProxyMysqlUserTable(String tableName, MysqlConfig config, boolean debug) {
super(tableName, config, debug);
this.name = super.getTableName();
// keep uuids view
this.uuidsView = super.getPrimaryKeys();
// keep old "uuid column type tweak" behavior (but now uses base helper)
alterColumnType("uuid", bestUuidType());
// load intColumns behavior if you rely on it elsewhere
// (no-op: it's only used if you add to it)
}
/**
* Use an already-connected pool (shared).
* @param tableName the table name
* @param existingMysql existing MySQL connection
* @param debug enable debug mode
*/
public ProxyMysqlUserTable(String tableName, MySQL existingMysql, boolean debug) {
super(tableName, existingMysql, debug);
this.name = super.getTableName();
this.uuidsView = super.getPrimaryKeys();
alterColumnType("uuid", bestUuidType());
}
// ---- Backwards-compatible API ----
/**
* Get table name.
* @return the table name
*/
public String getName() {
return name;
}
/**
* Keep old method name and behavior.
* @return set of UUIDs
*/
public Set<String> getUuids() {
return getPrimaryKeys();
}
/**
* Keep old behavior: query uuids fresh.
* @return list of UUIDs from database
*/
public ArrayList<String> getUuidsQuery() {
return new ArrayList<>(getPrimaryKeysQuery());
}
/**
* Load data from database.
*/
public void loadData() {
// old name -> new behavior
loadBasicCaches();
}
/**
* Clear cache.
*/
public void clearCache() {
clearCacheBasic();
}
/**
* Clear basic cache.
*/
public void clearCacheBasic() {
clearCaches();
}
/**
* Shutdown and close connections.
*/
public void shutdown() {
close();
}
// Keep old containsKeyQuery signature
public boolean containsKeyQuery(String index) {
return super.containsKeyQuery(index);
}
// ---- Required old utility methods (kept) ----
/**
* Get columns from query.
* @return list of column names
*/
public ArrayList<String> getColumnsQueury() {
return new ArrayList<>(getColumnsQuery());
}
/**
* Get rows with player names from query.
* @return list of columns with player names
*/
public ArrayList<Column> getRowsNameQuery() {
ArrayList<Column> result = new ArrayList<>();
checkColumn("PlayerName", DataType.STRING);
String sqlStr = "SELECT " + qi("PlayerName") + " FROM " + qi(getTableName()) + ";";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sqlStr);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
result.add(new Column("PlayerName", new DataValueString(rs.getString(1))));
}
} catch (SQLException ignored) {
}
return result;
}
/**
* Get player names from query.
* @return list of player names
*/
public ArrayList<String> getNamesQuery() {
ArrayList<String> names = new ArrayList<>();
checkColumn("PlayerName", DataType.STRING);
ArrayList<Column> rows = getRowsNameQuery();
if (rows != null) {
for (Column c : rows) {
if (c.getValue() != null && c.getValue().isString()) {
String v = c.getValue().getString();
if (v != null) {
names.add(v);
}
}
}
}
return names;
}
/**
* Get all rows from query.
* @return list of columns with UUID data
*/
public ArrayList<Column> getRowsQuery() {
ArrayList<Column> result = new ArrayList<>();
String sqlStr = "SELECT " + qi("uuid") + " FROM " + qi(getTableName()) + ";";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sqlStr);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
result.add(new Column("uuid", new DataValueString(rs.getString(1))));
}
} catch (SQLException e) {
return null;
}
return result;
}
/**
* Get UUID to name mapping from query.
* @return map of UUID to player name
*/
public ConcurrentHashMap<UUID, String> getRowsUUIDNameQuery() {
ConcurrentHashMap<UUID, String> uuidNames = new ConcurrentHashMap<>();
checkColumn("PlayerName", DataType.STRING);
String sqlStr = "SELECT " + qi("uuid") + ", " + qi("PlayerName") + " FROM " + qi(getTableName()) + ";";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sqlStr);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String uuidStr = rs.getString(1);
String playerName = rs.getString(2);
if (uuidStr != null && !uuidStr.isEmpty() && !"null".equalsIgnoreCase(uuidStr) && playerName != null) {
try {
uuidNames.put(UUID.fromString(uuidStr), playerName);
} catch (IllegalArgumentException ignored) {
}
}
}
} catch (SQLException ignored) {
}
return uuidNames;
}
/**
* Get UUID by player name.
* @param playerName the player name
* @return the UUID or null if not found
*/
public String getUUID(String playerName) {
checkColumn("PlayerName", DataType.STRING);
String query = "SELECT " + qi("uuid") + " FROM " + qi(getTableName()) + " WHERE " + qi("PlayerName") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(query)) {
ps.setString(1, playerName);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
String uuid = rs.getString(1);
if (uuid != null && !uuid.isEmpty()) {
return uuid;
}
}
}
} catch (SQLException e) {
debug(e);
}
return null;
}
/**
* Get exact match query.
* @param column the column to match
* @return list of matching columns
*/
public ArrayList<Column> getExactQuery(Column column) {
ArrayList<Column> result = new ArrayList<>();
String query = "SELECT * FROM " + qi(getTableName()) + " WHERE " + qi(column.getName()) + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(query)) {
String v = column.getValue() == null ? null : column.getValue().getString();
if (getDbType() == DbType.POSTGRESQL && "uuid".equalsIgnoreCase(column.getName()) && v != null) {
ps.setObject(1, UUID.fromString(v));
} else {
ps.setString(1, v);
}
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) {
String columnName = rs.getMetaData().getColumnLabel(i);
Column rCol;
if (intColumns.contains(columnName)) {
rCol = new Column(columnName, DataType.INTEGER);
rCol.setValue(new DataValueInt(rs.getInt(i)));
} else {
rCol = new Column(columnName, DataType.STRING);
rCol.setValue(new DataValueString(rs.getString(i)));
}
result.add(rCol);
}
return result;
}
}
} catch (SQLException e) {
debug(e);
} catch (ArrayIndexOutOfBoundsException ignored) {
}
for (String col : getColumns()) {
result.add(new Column(col, DataType.STRING));
}
return result;
}
/**
* Check if column is integer type.
* @param key the column name
* @return true if integer column
*/
public boolean isIntColumn(String key) {
return intColumns.contains(key);
}
// ---- INSERT/UPDATE/WIPE (kept similar to your existing behavior) ----
/**
* Insert a value.
* @param index the UUID
* @param column the column name
* @param value the value to insert
*/
public void insert(String index, String column, DataValue value) {
insertQuery(index, Arrays.asList(new Column(column, value)));
}
/**
* Insert multiple columns.
* @param index the UUID
* @param cols the columns to insert
*/
public void insertQuery(String index, List<Column> cols) {
for (Column c : cols) {
checkColumn(c.getName(), c.getDataType());
}
// Keep your previous cross-db behavior:
// - Postgres: ON CONFLICT upsert
// - MySQL/MariaDB: INSERT IGNORE ... SET ...
if (getDbType() == DbType.POSTGRESQL) {
StringBuilder sb = new StringBuilder();
sb.append("INSERT INTO ").append(qi(getTableName())).append(" (").append(qi("uuid"));
for (Column col : cols) {
sb.append(", ").append(qi(col.getName()));
}
sb.append(") VALUES (?");
for (int i = 0; i < cols.size(); i++) {
sb.append(", ?");
}
sb.append(") ON CONFLICT (").append(qi("uuid")).append(") DO UPDATE SET ");
for (int i = 0; i < cols.size(); i++) {
Column col = cols.get(i);
String c = qi(col.getName());
sb.append(c).append(" = EXCLUDED.").append(c);
if (i != cols.size() - 1) {
sb.append(", ");
}
}
sb.append(";");
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sb.toString())) {
ps.setObject(1, UUID.fromString(index));
int p = 2;
for (Column col : cols) {
ps.setString(p++, col.getValue().toString());
}
ps.executeUpdate();
uuidsView.add(index);
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
return;
}
StringBuilder query = new StringBuilder();
query.append("INSERT IGNORE ").append(qi(getTableName())).append(" SET ").append(qi("uuid")).append("=?, ");
for (int i = 0; i < cols.size(); i++) {
Column col = cols.get(i);
query.append(qi(col.getName())).append("=?");
query.append(i == cols.size() - 1 ? ";" : ", ");
}
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(query.toString())) {
ps.setString(1, index);
int p = 2;
for (Column col : cols) {
ps.setString(p++, col.getValue().toString());
}
ps.executeUpdate();
uuidsView.add(index);
} catch (SQLException e) {
debug(e);
}
}
/**
* Update multiple columns.
* @param index the UUID
* @param cols the columns to update
*/
public void update(String index, List<Column> cols) {
for (Column col : cols) {
checkColumn(col.getName(), col.getDataType());
}
// keep old behavior: if missing, insert
if (!getUuids().contains(index) && !containsKeyQuery(index)) {
insertQuery(index, cols);
return;
}
synchronized (updateLock) {
StringBuilder sb = new StringBuilder();
sb.append("UPDATE ").append(qi(getTableName())).append(" SET ");
for (int i = 0; i < cols.size(); i++) {
Column col = cols.get(i);
sb.append(qi(col.getName())).append("=?");
if (i != cols.size() - 1) {
sb.append(", ");
}
}
sb.append(" WHERE ").append(qi("uuid")).append("=?;");
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sb.toString())) {
int p = 1;
for (Column col : cols) {
bindValue(ps, p++, col);
}
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(p, UUID.fromString(index));
} else {
ps.setString(p, index);
}
int updated = ps.executeUpdate();
if (updated == 0) {
// row missing (wiped) even though uuid cache thought it existed
insertQuery(index, cols);
}
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
}
private void bindValue(PreparedStatement ps, int index, Column col) throws SQLException {
DataValue v = col.getValue();
if (getDbType() == DbType.POSTGRESQL && v != null && v.isInt()) {
ps.setInt(index, v.getInt());
return;
}
ps.setString(index, v == null ? null : v.toString());
}
/**
* Update a single column value.
* @param index the UUID
* @param column the column name
* @param value the value to update
*/
public void update(String index, String column, DataValue value) {
checkColumn(column, value.getType());
if (!getUuids().contains(index) && !containsKeyQuery(index)) {
insert(index, column, value);
return;
}
synchronized (updateLock) {
String sql = "UPDATE " + qi(getTableName()) + " SET " + qi(column) + "=? WHERE " + qi("uuid") + "=?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (getDbType() == DbType.POSTGRESQL && value != null && value.isInt()) {
ps.setInt(1, value.getInt());
} else {
ps.setString(1, value == null ? null : value.toString());
}
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(2, UUID.fromString(index));
} else {
ps.setString(2, index);
}
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
}
/**
* Wipe all data from a column.
* @param columnName the column name
* @param dataType the data type
*/
public void wipeColumnData(String columnName, DataType dataType) {
checkColumn(columnName, dataType);
String sql = "UPDATE " + qi(getTableName()) + " SET " + qi(columnName) + " = " + dataType.getNoValue() + ";";
try {
new Query(mysql, sql).executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
}
@@ -1,178 +0,0 @@
package com.bencodez.votingplugin.proxy;
import java.util.UUID;
import java.util.regex.Pattern;
import lombok.Getter;
import lombok.Setter;
public class VoteTotalsSnapshot {
private static final String SEP = "//";
private static final Pattern SEP_PATTERN = Pattern.compile(Pattern.quote(SEP));
// Current storage version
private static final int CURRENT_VERSION = 2;
@Getter
@Setter
private int allTimeTotal;
@Getter
@Setter
private int monthTotal;
@Getter
@Setter
private int weeklyTotal;
@Getter
@Setter
private int dailyTotal;
@Getter
@Setter
private int points;
@Getter
@Setter
private int votePartyCurrent;
@Getter
@Setter
private int votePartyRequired;
@Getter
@Setter
private int dateMonthTotal;
@Getter
@Setter
private UUID voteUUID;
public VoteTotalsSnapshot() {
}
public VoteTotalsSnapshot(int allTimeTotal, int monthTotal, int weeklyTotal, int dailyTotal, int points,
int votePartyCurrent, int votePartyRequired, int dateMonthTotal, UUID voteUUID) {
this.allTimeTotal = allTimeTotal;
this.monthTotal = monthTotal;
this.weeklyTotal = weeklyTotal;
this.dailyTotal = dailyTotal;
this.points = points;
this.votePartyCurrent = votePartyCurrent;
this.votePartyRequired = votePartyRequired;
this.dateMonthTotal = dateMonthTotal;
this.voteUUID = voteUUID;
}
public static VoteTotalsSnapshot parseStorage(String raw) {
VoteTotalsSnapshot out = new VoteTotalsSnapshot();
if (raw == null || raw.isEmpty()) {
return out;
}
String[] t = SEP_PATTERN.split(raw, -1);
if (t.length < 5) {
return out;
}
int idx = 0;
int version = 1; // legacy default
// Versioned?
if (t[0].startsWith("v")) {
version = parseInt(t[0].substring(1), 1);
idx = 1;
}
switch (version) {
case 1:
parseV1(out, t, idx);
break;
case 2:
parseV2(out, t, idx);
break;
default:
// Unknown future version → best-effort using v2 layout
parseV2(out, t, idx);
break;
}
return out;
}
/*
* ========================= Writers (always v2) =========================
*/
public String toStorageString() {
return "v" + CURRENT_VERSION + SEP + allTimeTotal + SEP + monthTotal + SEP + weeklyTotal + SEP + dailyTotal
+ SEP + points + SEP + votePartyCurrent + SEP + votePartyRequired + SEP + dateMonthTotal + SEP
+ (voteUUID != null ? voteUUID.toString() : "");
}
@Override
public String toString() {
return toStorageString();
}
// Legacy (milestoneCount exists, but ignored)
private static void parseV1(VoteTotalsSnapshot out, String[] t, int i) {
out.allTimeTotal = parseInt(t, i++, 0);
out.monthTotal = parseInt(t, i++, 0);
out.weeklyTotal = parseInt(t, i++, 0);
out.dailyTotal = parseInt(t, i++, 0);
out.points = parseInt(t, i++, 0);
i++; // skip milestoneCount
out.votePartyCurrent = parseInt(t, i++, 0);
out.votePartyRequired = parseInt(t, i++, 0);
out.dateMonthTotal = parseInt(t, i++, 0);
out.voteUUID = parseUUID(t, i);
}
// Current (no milestoneCount)
private static void parseV2(VoteTotalsSnapshot out, String[] t, int i) {
out.allTimeTotal = parseInt(t, i++, 0);
out.monthTotal = parseInt(t, i++, 0);
out.weeklyTotal = parseInt(t, i++, 0);
out.dailyTotal = parseInt(t, i++, 0);
out.points = parseInt(t, i++, 0);
out.votePartyCurrent = parseInt(t, i++, 0);
out.votePartyRequired = parseInt(t, i++, 0);
out.dateMonthTotal = parseInt(t, i++, 0);
out.voteUUID = parseUUID(t, i);
}
/*
* ========================= Helpers =========================
*/
private static int parseInt(String[] t, int idx, int def) {
if (idx < 0 || idx >= t.length)
return def;
try {
return Integer.parseInt(t[idx].trim());
} catch (Exception e) {
return def;
}
}
private static int parseInt(String s, int def) {
try {
return Integer.parseInt(s);
} catch (Exception e) {
return def;
}
}
private static UUID parseUUID(String[] t, int idx) {
if (idx < 0 || idx >= t.length)
return null;
String s = t[idx].trim();
if (s.isEmpty() || "null".equalsIgnoreCase(s))
return null;
try {
return UUID.fromString(s);
} catch (Exception e) {
return null;
}
}
}
@@ -1,628 +0,0 @@
package com.bencodez.votingplugin.proxy;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Configuration interface for proxy server integration.
*/
public interface VotingPluginProxyConfig {
/**
* Gets whether proxy broadcast is enabled.
*
* @return true if proxy broadcast is enabled
*/
default boolean getProxyBroadcastEnabled() {
return false;
}
/**
* Routing scope mode.
*
* Expected values: PLAYER_SERVER | ALL_SERVERS | SERVERS | ALL_EXCEPT
*
* @return the scope mode
*/
default String getProxyBroadcastScopeMode() {
return "ALL_SERVERS";
}
/**
* Optional server list used by scope modes SERVERS and ALL_EXCEPT.
*
* @return the list of scope servers
*/
default List<String> getProxyBroadcastScopeServers() {
return Collections.emptyList();
}
/**
* What to do when the voting player is offline and scope is PLAYER_SERVER.
*
* Expected values: NONE | QUEUE | FORWARD
*
* @return the offline mode
*/
default String getProxyBroadcastOfflineMode() {
return "QUEUE";
}
/**
* Backend server list used when OfflineMode is FORWARD and Scope.Mode is
* PLAYER_SERVER.
*
* Ignored when Scope.Mode is ALL_SERVERS or ALL_EXCEPT.
*
* @return the list of offline forward servers
*/
default List<String> getProxyBroadcastOfflineForwardServers() {
return Collections.emptyList();
}
/**
* Gets whether unjoined players are allowed.
*
* @return true if unjoined players are allowed
*/
public boolean getAllowUnJoined();
/**
* Gets the Bedrock player prefix.
*
* @return the Bedrock player prefix
*/
public String getBedrockPlayerPrefix();
/**
* Gets the list of blocked servers.
*
* @return the list of blocked servers
*/
public List<String> getBlockedServers();
/**
* Gets the Bungee host.
*
* @return the Bungee host
*/
public String getBungeeHost();
/**
* Gets whether Bungee manages totals.
*
* @return true if Bungee manages totals
*/
public boolean getBungeeManageTotals();
/**
* Gets the Bungee method.
*
* @return the Bungee method
*/
public String getBungeeMethod();
/**
* Gets the Bungee port.
*
* @return the Bungee port
*/
public int getBungeePort();
/**
* Gets the plugin message channel.
*
* @return the plugin message channel
*/
public String getPluginMessageChannel();
/**
* Gets whether plugin message encryption is enabled.
*
* @return true if plugin message encryption is enabled
*/
public boolean getPluginMessageEncryption();
/**
* Gets whether debug mode is enabled.
*
* @return true if debug mode is enabled
*/
public boolean getDebug();
/**
* Gets whether vote cache uses MySQL.
*
* @return true if vote cache uses MySQL
*/
public boolean getVoteCacheUseMySQL();
/**
* Gets whether vote cache uses main MySQL.
*
* @return true if vote cache uses main MySQL
*/
public boolean getVoteCacheUseMainMySQL();
/**
* Gets whether non-voted cache uses MySQL.
*
* @return true if non-voted cache uses MySQL
*/
public boolean getNonVotedCacheUseMySQL();
/**
* Gets whether non-voted cache uses main MySQL.
*
* @return true if non-voted cache uses main MySQL
*/
public boolean getNonVotedCacheUseMainMySQL();
/**
* Gets the MQTT client ID.
*
* @return the MQTT client ID
*/
public String getMqttClientID();
/**
* Gets the MQTT broker URL.
*
* @return the MQTT broker URL
*/
public String getMqttBrokerURL();
/**
* Gets the MQTT username.
*
* @return the MQTT username
*/
public String getMqttUsername();
/**
* Gets the MQTT password.
*
* @return the MQTT password
*/
public String getMqttPassword();
/**
* Gets the MQTT prefix.
*
* @return the MQTT prefix
*/
public String getMqttPrefix();
/**
* Gets the fallback server.
*
* @return the fallback server
*/
public String getFallBack();
/**
* Gets whether global data is enabled.
*
* @return true if global data is enabled
*/
public boolean getGlobalDataEnabled();
/**
* Gets whether global data uses main MySQL.
*
* @return true if global data uses main MySQL
*/
public boolean getGlobalDataUseMainMySQL();
/**
* Gets the limit for vote points.
*
* @return the vote points limit
*/
public int getLimitVotePoints();
/**
* Gets the maximum amount of votes per day.
*
* @return the maximum votes per day
*/
public int getMaxAmountOfVotesPerDay();
/**
* Gets the multi-proxy method.
*
* @return the multi-proxy method
*/
public String getMultiProxyMethod();
/**
* Gets whether multi-proxy uses one global reward.
*
* @return true if multi-proxy uses one global reward
*/
public boolean getMultiProxyOneGlobalReward();
/**
* Gets the multi-proxy Redis host.
*
* @return the Redis host
*/
public String getMultiProxyRedisHost();
/**
* Gets the multi-proxy Redis password.
*
* @return the Redis password
*/
public String getMultiProxyRedisPassword();
/**
* Gets the multi-proxy Redis port.
*
* @return the Redis port
*/
public int getMultiProxyRedisPort();
/**
* Gets whether multi-proxy Redis uses existing connection.
*
* @return true if using existing connection
*/
public boolean getMultiProxyRedisUseExistingConnection();
/**
* Gets the multi-proxy Redis username.
*
* @return the Redis username
*/
public String getMultiProxyRedisUsername();
/**
* Gets the collection of multi-proxy servers.
*
* @return the collection of server names
*/
public Collection<String> getMultiProxyServers();
/**
* Gets the configuration for a specific multi-proxy server.
*
* @param s the server name
* @return the server configuration as a map
*/
public Map<String, Object> getMultiProxyServersConfiguration(String s);
/**
* Gets the multi-proxy socket host.
*
* @return the socket host
*/
public String getMultiProxySocketHostHost();
/**
* Gets the multi-proxy socket host port.
*
* @return the socket port
*/
public int getMultiProxySocketHostPort();
/**
* Gets whether multi-proxy support is enabled.
*
* @return true if multi-proxy support is enabled
*/
public boolean getMultiProxySupport();
/**
* Gets whether online mode is enabled.
*
* @return true if online mode is enabled
*/
public boolean getOnlineMode();
/**
* Gets the points awarded per vote.
*
* @return the points per vote
*/
public int getPointsOnVote();
/**
* Gets whether this is the primary server.
*
* @return true if this is the primary server
*/
public boolean getPrimaryServer();
/**
* Gets the proxy server name.
*
* @return the proxy server name
*/
public String getProxyServerName();
/**
* Gets the list of proxy servers.
*
* @return the list of proxy servers
*/
public List<String> getProxyServers();
/**
* Gets the Redis host.
*
* @return the Redis host
*/
public String getRedisHost();
/**
* Gets the Redis password.
*
* @return the Redis password
*/
public String getRedisPassword();
/**
* Gets the Redis port.
*
* @return the Redis port
*/
public int getRedisPort();
/**
* Gets the Redis database index.
*
* @return the Redis database index
*/
public int getRedisDbIndex();
/**
* Gets the Redis prefix.
*
* @return the Redis prefix
*/
public String getRedisPrefix();
/**
* Gets the Redis username.
*
* @return the Redis username
*/
public String getRedisUsername();
/**
* Gets whether votes should be sent to all servers.
*
* @return true if votes should be sent to all servers
*/
public boolean getSendVotesToAllServers();
/**
* Gets the configuration for a specific Spigot server.
*
* @param s the server name
* @return the server configuration as a map
*/
public Map<String, Object> getSpigotServerConfiguration(String s);
/**
* Gets the collection of Spigot servers.
*
* @return the collection of server names
*/
public Collection<String> getSpigotServers();
/**
* Gets whether to store month totals with date.
*
* @return true if month totals are stored with date
*/
public boolean getStoreMonthTotalsWithDate();
/**
* Gets whether time change fail-safe bypass is enabled.
*
* @return true if time change fail-safe bypass is enabled
*/
public boolean getTimeChangeFailSafeBypass();
/**
* Gets the time hour offset.
*
* @return the hour offset
*/
public int getTimeHourOffSet();
/**
* Gets the time week offset.
*
* @return the week offset
*/
public int getTimeWeekOffSet();
/**
* Gets the time zone.
*
* @return the time zone
*/
public String getTimeZone();
/**
* Gets whether to use month date totals as primary total.
*
* @return true if month date totals are used as primary total
*/
public boolean getUseMonthDateTotalsAsPrimaryTotal();
/**
* Gets whether UUID lookup is enabled.
*
* @return true if UUID lookup is enabled
*/
public boolean getUUIDLookup();
/**
* Gets the vote cache time.
*
* @return the vote cache time
*/
public int getVoteCacheTime();
/**
* Gets the vote party broadcast message.
*
* @return the vote party broadcast message
*/
public String getVotePartyBroadcast();
/**
* Gets the list of vote party Bungee commands.
*
* @return the list of commands
*/
public List<String> getVotePartyBungeeCommands();
/**
* Gets whether vote party is enabled.
*
* @return true if vote party is enabled
*/
public boolean getVotePartyEnabled();
/**
* Gets the vote party increase votes required.
*
* @return the increase in votes required
*/
public int getVotePartyIncreaseVotesRequired();
/**
* Gets whether to send vote party to all servers.
*
* @return true if vote party is sent to all servers
*/
public boolean getVotePartySendToAllServers();
/**
* Gets the list of servers to send vote party to.
*
* @return the list of servers
*/
public List<String> getVotePartyServersToSend();
/**
* Gets the number of votes required for vote party.
*
* @return the votes required
*/
public int getVotePartyVotesRequired();
/**
* Gets whether to wait for user to be online.
*
* @return true if waiting for user to be online
*/
public boolean getWaitForUserOnline();
/**
* Gets the list of whitelisted servers.
*
* @return the list of whitelisted servers
*/
public List<String> getWhiteListedServers();
/**
* Gets the collection of sites with vote delay configured.
*
* @return the collection of site names
*/
public Collection<String> getWaitUntilVoteDelaySites();
/**
* Gets the vote delay service for a specific site.
*
* @param site the site name
* @return the service name
*/
public String getWaitUntilVoteDelayService(String site);
/**
* Gets the vote delay in seconds for a specific site.
*
* @param site the site name
* @return the vote delay in seconds
*/
public int getWaitUntilVoteDelayVoteDelay(String site);
/**
* Gets the minimum vote delay for a specific site.
*
* @param site the site name
* @return the minimum vote delay
*/
public int getWaitUntilVoteDelayVoteDelayMin(String site);
/**
* Gets the hourly vote delay for a specific site.
*
* @param site the site name
* @return the hourly vote delay
*/
public int getWaitUntilVoteDelayVoteDelayHour(String site);
/**
* Gets whether the vote delay is daily for a specific site.
*
* @param site the site name
* @return true if the delay is daily
*/
public boolean getWaitUntilVoteDelayVoteDelayDaily(String site);
/**
* Loads the configuration.
*/
public void load();
/**
* Saves the configuration.
*/
public void save();
/**
* Gets whether vote logging is enabled.
*
* @return true if vote logging is enabled
*/
public boolean getVoteLoggingEnabled();
/**
* Gets the number of days to keep vote logs.
*
* @return the purge days
*/
public int getVoteLoggingPurgeDays();
/**
* Gets whether vote logging uses main MySQL.
*
* @return true if vote logging uses main MySQL
*/
public boolean getVoteLoggingUseMainMySQL();
/**
* Gets whether a database is configured.
*
* @return true if a database is configured
*/
public boolean hasDatabaseConfigured();
/**
* Gets the multi-proxy Redis database index.
*
* @return the database index
*/
public int getMultiProxyRedisDbIndex();
}
@@ -1,308 +0,0 @@
// File: com/bencodez/votingplugin/proxy/VotingPluginWire.java
package com.bencodez.votingplugin.proxy;
import java.util.Map;
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
/**
* Canonical wire format for proxy - backend messages.
*
* Rules: - Only JsonEnvelope is sent over the wire (no legacy String[] /
* delimiters). - All fields are named keys (no numeric indices). - Versioning
* uses the JsonEnvelope schema as the source of truth. - A "bungeeVersion"
* field is also included for legacy compatibility checks. - Values are written
* as native types where possible, but readers tolerate Strings.
*
* Note: Some keys are public because other classes (e.g. MultiProxyHandler)
* reference them directly.
*/
public final class VotingPluginWire {
private VotingPluginWire() {
}
public static final int SCHEMA_VERSION = 1;
// =========================
// Subchannels (canonical)
// =========================
public static final String SUB_VOTE = "Vote";
public static final String SUB_VOTE_ONLINE = "VoteOnline";
public static final String SUB_VOTE_UPDATE = "VoteUpdate";
public static final String SUB_VOTE_BROADCAST = "VoteBroadcast";
public static final String SUB_BUNGEE_TIME_CHANGE = "BungeeTimeChange";
public static final String SUB_STATUS = "Status";
public static final String SUB_STATUS_OKAY = "statusokay";
public static final String SUB_SERVER_NAME = "ServerName";
public static final String SUB_VOTE_PARTY = "VotePartyBungee";
public static final String SUB_LOGIN = "login";
public static final String SUB_VOTEUPDATE_RELAY = "voteupdate";
// =========================
// Multi-proxy subchannels (REQUIRED by MultiProxyHandler)
// =========================
public static final String SUB_CLEAR_VOTE = "ClearVote";
public static final String SUB_CLEAR_VOTE_PRIMARY = "ClearVotePrimary";
// =========================
// Field keys (public where referenced externally)
// =========================
public static final String K_VERSION = "version";
public static final String K_PLAYER = "player";
public static final String K_UUID = "uuid";
public static final String K_SERVER = "server";
public static final String K_SERVICE = "service";
public static final String K_TIME = "time";
// Vote/VoteOnline extras
public static final String K_WAS_ONLINE = "wasOnline";
public static final String K_REAL_VOTE = "realVote";
public static final String K_TOTALS = "totals";
public static final String K_SET_TOTALS = "setTotals";
public static final String K_MANAGE_TOTALS = "manageTotals";
public static final String K_BUNGEE_BROADCAST = "bungeeBroadcast";
public static final String K_NUM = "num";
public static final String K_NUMBER_OF_VOTES = "numberOfVotes";
// VoteUpdate extras
public static final String K_PLAYER_UUID = "playerUuid";
public static final String K_VOTE_PARTY_CURRENT = "votePartyCurrent";
public static final String K_VOTE_PARTY_REQUIRED = "votePartyRequired";
public static final String K_LAST_VOTE_TIME = "lastVoteTime";
// =========================
// Builders
// =========================
public static JsonEnvelope bungeeTimeChange() {
return base(SUB_BUNGEE_TIME_CHANGE).build();
}
public static JsonEnvelope vote(String player, String uuid, String service, long time, boolean wasOnline,
boolean realVote, String totals, boolean manageTotals, boolean bungeeBroadcast, int num,
int numberOfVotes) {
return base(SUB_VOTE).put(K_PLAYER, safe(player)).put(K_UUID, safe(uuid)).put(K_SERVICE, safe(service))
.put(K_TIME, time).put(K_WAS_ONLINE, wasOnline).put(K_REAL_VOTE, realVote).put(K_TOTALS, safe(totals))
.put(K_SET_TOTALS, true) // backend historically defaulted to true
.put(K_MANAGE_TOTALS, manageTotals).put(K_BUNGEE_BROADCAST, bungeeBroadcast).put(K_NUM, num)
.put(K_NUMBER_OF_VOTES, numberOfVotes).build();
}
public static JsonEnvelope voteOnline(String player, String uuid, String service, long time, boolean wasOnline,
boolean realVote, String totals, boolean manageTotals, boolean bungeeBroadcast, int num,
int numberOfVotes) {
return base(SUB_VOTE_ONLINE).put(K_PLAYER, safe(player)).put(K_UUID, safe(uuid)).put(K_SERVICE, safe(service))
.put(K_TIME, time).put(K_WAS_ONLINE, wasOnline).put(K_REAL_VOTE, realVote).put(K_TOTALS, safe(totals))
.put(K_SET_TOTALS, true).put(K_MANAGE_TOTALS, manageTotals).put(K_BUNGEE_BROADCAST, bungeeBroadcast)
.put(K_NUM, num).put(K_NUMBER_OF_VOTES, numberOfVotes).build();
}
public static JsonEnvelope voteBroadcast(String uuid, String player, String service, long time, String totals) {
return base(SUB_VOTE_BROADCAST).put(K_UUID, safe(uuid)).put(K_PLAYER, safe(player))
.put(K_SERVICE, safe(service)).put(K_TIME, time).put(K_TOTALS, safe(totals)).build();
}
public static JsonEnvelope voteUpdate(String playerUuid, int votePartyCurrent, int votePartyRequired,
String service, long lastVoteTime, String totals) {
final String u = safe(playerUuid);
return base(SUB_VOTE_UPDATE).put(K_PLAYER_UUID, u).put(K_PLAYER, u) // legacy fallback key used by older
// handlers
.put(K_VOTE_PARTY_CURRENT, votePartyCurrent).put(K_VOTE_PARTY_REQUIRED, votePartyRequired)
.put(K_SERVICE, safe(service)).put(K_LAST_VOTE_TIME, lastVoteTime).put(K_TOTALS, safe(totals)).build();
}
public static JsonEnvelope votePartyBungee() {
return base(SUB_VOTE_PARTY).build();
}
public static JsonEnvelope status(String server) {
return base(SUB_STATUS).put(K_SERVER, safe(server)).build();
}
public static JsonEnvelope statusOkay(String server) {
return base(SUB_STATUS_OKAY).put(K_SERVER, safe(server)).build();
}
public static JsonEnvelope serverName(String server) {
return base(SUB_SERVER_NAME).put(K_SERVER, safe(server)).build();
}
public static JsonEnvelope login(String player, String uuid, String server) {
return base(SUB_LOGIN).put(K_PLAYER, safe(player)).put(K_UUID, safe(uuid)).put(K_SERVER, safe(server)).build();
}
// =========================
// Multi-proxy builders (REQUIRED by MultiProxyHandler)
// =========================
/**
* Sent by non-primary servers to ask the primary to clear vote state. Primary
* responds by broadcasting ClearVotePrimary.
*/
public static JsonEnvelope clearVote(String uuid, String player, String server) {
return base(SUB_CLEAR_VOTE).put(K_UUID, safe(uuid)).put(K_PLAYER, safe(player)).put(K_SERVER, safe(server))
.build();
}
/**
* Broadcast by the primary server so all proxies clear vote state.
*/
public static JsonEnvelope clearVotePrimary(String uuid, String player, String server) {
return base(SUB_CLEAR_VOTE_PRIMARY).put(K_UUID, safe(uuid)).put(K_PLAYER, safe(player))
.put(K_SERVER, safe(server)).build();
}
// =========================
// Readers (decode)
// =========================
public static final class Vote {
public final String subChannel;
public final String player;
public final String uuid;
public final String service;
public final long time;
public final boolean wasOnline;
public final boolean realVote;
public final String totals;
public final boolean setTotals;
public final boolean manageTotals;
public final boolean broadcast; // historically bungeeBroadcast
public final int num;
public final int numberOfVotes;
private Vote(String subChannel, String player, String uuid, String service, long time, boolean wasOnline,
boolean realVote, String totals, boolean setTotals, boolean manageTotals, boolean broadcast, int num,
int numberOfVotes) {
this.subChannel = subChannel;
this.player = player;
this.uuid = uuid;
this.service = service;
this.time = time;
this.wasOnline = wasOnline;
this.realVote = realVote;
this.totals = totals;
this.setTotals = setTotals;
this.manageTotals = manageTotals;
this.broadcast = broadcast;
this.num = num;
this.numberOfVotes = numberOfVotes;
}
}
public static Vote readVote(JsonEnvelope env) {
Map<String, String> f = env.getFields();
final String sub = safe(env.getSubChannel());
final String player = safe(f.get(K_PLAYER));
final String uuid = safe(f.get(K_UUID));
final String service = safe(f.get(K_SERVICE));
final long time = readLong(f, K_TIME, 0L);
final boolean wasOnline = readBool(f, K_WAS_ONLINE, false);
final boolean realVote = readBool(f, K_REAL_VOTE, false);
final String totals = safe(f.get(K_TOTALS));
final boolean setTotals = readBool(f, K_SET_TOTALS, true);
final boolean manageTotals = readBool(f, K_MANAGE_TOTALS, false);
final boolean broadcast = readBool(f, K_BUNGEE_BROADCAST, false);
final int num = readInt(f, K_NUM, 1);
final int numberOfVotes = readInt(f, K_NUMBER_OF_VOTES, 1);
return new Vote(sub, player, uuid, service, time, wasOnline, realVote, totals, setTotals, manageTotals,
broadcast, num, numberOfVotes);
}
public static final class VoteUpdate {
public final String uuid;
public final int votePartyCurrent;
public final int votePartyRequired;
public final String service;
public final long time; // lastVoteTime
private VoteUpdate(String uuid, int votePartyCurrent, int votePartyRequired, String service, long time) {
this.uuid = uuid;
this.votePartyCurrent = votePartyCurrent;
this.votePartyRequired = votePartyRequired;
this.service = service;
this.time = time;
}
}
public static VoteUpdate readVoteUpdate(JsonEnvelope env) {
Map<String, String> f = env.getFields();
String uuid = safe(f.get(K_PLAYER_UUID));
if (uuid.isEmpty()) {
uuid = safe(f.get(K_PLAYER));
}
int cur = readInt(f, K_VOTE_PARTY_CURRENT, 0);
int req = readInt(f, K_VOTE_PARTY_REQUIRED, 0);
String service = safe(f.get(K_SERVICE));
long t = readLong(f, K_LAST_VOTE_TIME, 0L);
if (t == 0L) {
t = readLong(f, K_TIME, 0L);
}
return new VoteUpdate(uuid, cur, req, service, t);
}
// =========================
// Internals
// =========================
private static JsonEnvelope.Builder base(String subChannel) {
int ver = SCHEMA_VERSION;
return JsonEnvelope.builder(subChannel).schema(ver);
}
private static String safe(String s) {
return s == null ? "" : s;
}
private static boolean readBool(Map<String, String> f, String key, boolean def) {
String v = f.get(key);
if (v == null) {
return def;
}
return Boolean.parseBoolean(v);
}
private static int readInt(Map<String, String> f, String key, int def) {
String v = f.get(key);
if (v == null) {
return def;
}
try {
return Integer.parseInt(v);
} catch (Exception ignored) {
return def;
}
}
private static long readLong(Map<String, String> f, String key, long def) {
String v = f.get(key);
if (v == null) {
return def;
}
try {
return Long.parseLong(v);
} catch (Exception ignored) {
return def;
}
}
}
@@ -1,34 +0,0 @@
package com.bencodez.votingplugin.proxy.broadcast;
import java.util.Locale;
/**
* Enum for offline broadcast modes.
*/
public enum OfflineMode {
/** No offline handling. */
NONE,
/** Queue votes for offline players. */
QUEUE,
/** Forward to other servers. */
FORWARD;
/**
* Parse offline mode from string.
* @param value the string to parse
* @param def the default value
* @return the offline mode
*/
public static OfflineMode parse(String value, OfflineMode def) {
if (value == null)
return def;
String v = value.trim();
if (v.isEmpty())
return def;
try {
return OfflineMode.valueOf(v.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
return def;
}
}
}
@@ -1,161 +0,0 @@
package com.bencodez.votingplugin.proxy.broadcast;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import com.bencodez.votingplugin.proxy.VotingPluginProxyConfig;
/**
* Decides where vote broadcasts should execute (proxy-side routing only).
*
* Backend servers still decide WHAT to broadcast (VoteBroadcast.Type/Format).
*
* QUEUE behavior:
* - This class returns no immediate targets when player is offline and OfflineMode=QUEUE.
* - The "queue" is the existing online vote cache (addOnlineVote + checkOnlineVotes on login).
*/
public final class ProxyBroadcastDecider {
private final Supplier<VotingPluginProxyConfig> config;
private final Supplier<Set<String>> allServers;
private final Predicate<String> isServerValid;
private final Predicate<String> isBlockedServer;
/**
* Constructor for ProxyBroadcastDecider.
* @param config configuration supplier
* @param allServers all servers supplier
* @param isServerValid server validation predicate
* @param isBlockedServer blocked server predicate
*/
public ProxyBroadcastDecider(Supplier<VotingPluginProxyConfig> config,
Supplier<Set<String>> allServers,
Predicate<String> isServerValid,
Predicate<String> isBlockedServer) {
this.config = config;
this.allServers = allServers;
this.isServerValid = isServerValid;
this.isBlockedServer = isBlockedServer;
}
/**
* Compute which backend servers should broadcast for this vote.
*
* @param playerOnline whether the voting player is currently online
* @param currentPlayerServer the server the player is on (only used when playerOnline=true)
* @return set of target servers
*/
public Set<String> resolveTargets(boolean playerOnline, String currentPlayerServer) {
VotingPluginProxyConfig cfg = config.get();
if (cfg == null || !cfg.getProxyBroadcastEnabled()) {
return Collections.emptySet();
}
ScopeMode scope = ScopeMode.parse(cfg.getProxyBroadcastScopeMode(), ScopeMode.ALL_SERVERS);
List<String> scopeServers = cfg.getProxyBroadcastScopeServers();
Set<String> eligible = eligibleServers();
switch (scope) {
case ALL_SERVERS:
return eligible;
case SERVERS:
return intersect(scopeServers, eligible);
case ALL_EXCEPT:
return subtract(scopeServers, eligible);
case PLAYER_SERVER:
default:
if (playerOnline) {
if (currentPlayerServer != null && eligible.contains(currentPlayerServer)) {
Set<String> out = new LinkedHashSet<>();
out.add(currentPlayerServer);
return out;
}
return Collections.emptySet();
}
// player offline + PLAYER_SERVER scope
OfflineMode offlineMode = OfflineMode.parse(cfg.getProxyBroadcastOfflineMode(), OfflineMode.QUEUE);
if (offlineMode == OfflineMode.FORWARD) {
// FORWARD uses OfflineForward.Servers (list only; mode is implicit SERVERS)
return intersect(cfg.getProxyBroadcastOfflineForwardServers(), eligible);
}
// NONE or QUEUE => no immediate targets
// QUEUE is handled by existing vote cache + checkOnlineVotes() on login
return Collections.emptySet();
}
}
/**
* Convenience check for "should this specific server broadcast?"
* @param server the server name
* @param targets the target servers
* @return true if should broadcast
*/
public boolean shouldBroadcast(String server, Set<String> targets) {
return targets != null && server != null && targets.contains(server);
}
private Set<String> eligibleServers() {
Set<String> out = new LinkedHashSet<>();
for (String s : allServers.get()) {
if (s == null || s.isEmpty()) {
continue;
}
if (!isServerValid.test(s)) {
continue;
}
if (isBlockedServer.test(s)) {
continue;
}
out.add(s);
}
return out;
}
private static Set<String> intersect(List<String> list, Set<String> eligible) {
if (list == null || list.isEmpty() || eligible.isEmpty()) {
return Collections.emptySet();
}
Set<String> out = new LinkedHashSet<>();
for (String s : list) {
if (s == null) {
continue;
}
String v = s.trim();
if (!v.isEmpty() && eligible.contains(v)) {
out.add(v);
}
}
return out;
}
private static Set<String> subtract(List<String> list, Set<String> eligible) {
if (eligible.isEmpty()) {
return Collections.emptySet();
}
Set<String> out = new LinkedHashSet<>(eligible);
if (list == null || list.isEmpty()) {
return out;
}
for (String s : list) {
if (s == null) {
continue;
}
String v = s.trim();
if (!v.isEmpty()) {
out.remove(v);
}
}
return out;
}
}
@@ -1,70 +0,0 @@
package com.bencodez.votingplugin.proxy.broadcast;
import java.util.Locale;
/**
* Proxy-side routing scope for broadcast triggers.
*/
public enum ScopeMode {
/**
* Send only to the backend server the player is currently on.
*/
PLAYER_SERVER,
/**
* Send to all connected backend servers.
*/
ALL_SERVERS,
/**
* Send only to the configured server list.
*/
SERVERS,
/**
* Send to all servers except the configured server list.
*/
ALL_EXCEPT;
/**
* Parses a string value to a ScopeMode with a default fallback.
*
* @param value the string value to parse
* @param def the default value if parsing fails
* @return the parsed ScopeMode or the default
*/
public static ScopeMode parse(String value, ScopeMode def) {
if (value == null) {
return def;
}
String v = value.trim();
if (v.isEmpty()) {
return def;
}
v = v.toUpperCase(Locale.ROOT);
// allow a couple common aliases
if (v.equals("ALL")) {
return ALL_SERVERS;
}
if (v.equals("PLAYER")) {
return PLAYER_SERVER;
}
try {
return ScopeMode.valueOf(v);
} catch (IllegalArgumentException e) {
return def;
}
}
/**
* Parses a string value to a ScopeMode with ALL_SERVERS as default.
*
* @param value the string value to parse
* @return the parsed ScopeMode or ALL_SERVERS
*/
public static ScopeMode parse(String value) {
return parse(value, ALL_SERVERS);
}
}
@@ -1,55 +0,0 @@
package com.bencodez.votingplugin.proxy.bungee;
import java.io.File;
import java.util.function.Consumer;
import com.bencodez.simpleapi.file.BungeeJsonFile;
import com.bencodez.votingplugin.proxy.cache.nonvoted.INonVotedPlayersStorage;
/**
* JSON file-based storage for non-voted players cache on Bungee.
*/
public class BungeeJsonNonVotedPlayersCache extends BungeeJsonFile
implements INonVotedPlayersStorage {
/**
* Constructs a new BungeeJsonNonVotedPlayersCache.
*
* @param file the file to store data in
*/
public BungeeJsonNonVotedPlayersCache(File file) {
super(file);
}
@Override
public void upsertPlayer(String uuid, String playerName, long lastTime) {
setString("NonVotedPlayers." + playerName + ".UUID", uuid);
setLong("NonVotedPlayers." + playerName + ".LastTime", lastTime);
save();
}
@Override
public String getUuidByPlayerName(String playerName) {
String uuid = getString("NonVotedPlayers." + playerName + ".UUID", "");
return uuid != null ? uuid : "";
}
@Override
public void removeByPlayerName(String playerName) {
remove("NonVotedPlayers." + playerName);
save();
}
@Override
public void forEach(Consumer<NonVotedPlayerEntry> consumer) {
for (String player : getKeys("NonVotedPlayers")) {
long time = getLong("NonVotedPlayers." + player + ".LastTime", 0);
String uuid = getString("NonVotedPlayers." + player + ".UUID", "");
consumer.accept(new NonVotedPlayerEntry(player, uuid, time));
}
}
@Override
public void close() {
}
}
@@ -1,887 +0,0 @@
package com.bencodez.votingplugin.proxy.bungee;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.security.CodeSource;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigBungee;
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
import com.bencodez.votingplugin.proxy.VotingPluginProxyConfig;
import lombok.Getter;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.event.EventHandler;
/**
* VotingPlugin proxy implementation for BungeeCord.
*
* Supports a true hot reload (no proxy restart required) by:
* <ul>
* <li>Stopping the old {@link VotingPluginProxy} runtime</li>
* <li>Reloading configuration</li>
* <li>Re-registering the plugin message channel if it changed</li>
* <li>Creating a fresh {@link VotingPluginProxy} instance (constructor-time
* state refreshed)</li>
* <li>Re-initializing caches and rescheduling tasks</li>
* </ul>
*
*/
public class VotingPluginBungee extends Plugin implements Listener {
@Getter
private BungeeConfig config;
private BungeeJsonNonVotedPlayersCache nonVotedPlayersCache;
private BungeeJsonVoteCache voteCacheFile;
private String buildNumber = "NOTSET";
/**
* Votifier listener (registered only once).
*/
private VoteEventBungee voteEventBungee;
@Getter
private volatile VotingPluginProxy votingPluginProxy;
@Getter
private ScheduledExecutorService timer;
private ScheduledTask voteCheckTask;
private ScheduledTask cacheSaveTask;
/**
* Reload synchronization lock.
*/
private final Object reloadLock = new Object();
/**
* True while reload is in progress.
*/
private volatile boolean reloading = false;
/**
* Plugin messages received during reload are queued and replayed after reload.
*/
private final Queue<byte[]> queuedPluginMessages = new ConcurrentLinkedQueue<byte[]>();
/**
* Logs debug output if enabled in config.
*
* @param msg message to log
*/
public void debug(String msg) {
if (config != null && config.getDebug()) {
getLogger().info("Debug: " + msg);
}
}
/**
* Alias for debug for existing call sites.
*
* @param msg message to log
*/
public void debug2(String msg) {
debug(msg);
}
/**
* Gets servers eligible for proxy operations.
*
* @return server names
*/
public Set<String> getAvailableAllServers() {
Set<String> servers = new HashSet<String>();
if (config.getWhiteListedServers().isEmpty()) {
for (String s : getProxy().getServers().keySet()) {
if (!config.getBlockedServers().contains(s)) {
servers.add(s);
}
}
} else {
servers.addAll(config.getWhiteListedServers());
}
return servers;
}
/**
* Gets proper player name if online, otherwise returns stored name.
*
* @param uuid uuid string
* @param currentName current name
* @return proper name if online
*/
public String getProperPlayerName(String uuid, String currentName) {
ProxiedPlayer p = getProxy().getPlayer(UUID.fromString(uuid));
if (p != null && p.isConnected()) {
return p.getName();
}
return currentName;
}
/**
* Loads votingpluginversion.yml from jar if present.
*
* @return configuration or null
*/
private Configuration getVersionFile() {
try {
CodeSource src = this.getClass().getProtectionDomain().getCodeSource();
if (src != null) {
URL jar = src.getLocation();
ZipInputStream zip = new ZipInputStream(jar.openStream());
while (true) {
ZipEntry e = zip.getNextEntry();
if (e != null) {
if ("votingpluginversion.yml".equals(e.getName())) {
Reader defConfigStream = new InputStreamReader(zip);
if (defConfigStream != null) {
Configuration conf = ConfigurationProvider
.getProvider(net.md_5.bungee.config.YamlConfiguration.class)
.load(defConfigStream);
defConfigStream.close();
return conf;
}
}
}
}
}
} catch (Exception e) {
// ignore
}
return null;
}
/**
* Loads build number from embedded version file.
*/
public void loadVersionFile() {
Configuration conf = getVersionFile();
if (conf != null) {
buildNumber = conf.getString("buildnumber", "NOTSET");
}
}
@Override
public void onEnable() {
timer = Executors.newScheduledThreadPool(1);
getProxy().getPluginManager().registerListener(this, this);
config = new BungeeConfig(this);
config.load();
getProxy().getPluginManager().registerCommand(this, new VotingPluginBungeeCommand(this));
// Ensure channel registered
try {
getProxy().registerChannel(config.getPluginMessageChannel());
} catch (Exception ignored) {
}
// Create initial runtime (fresh instance)
votingPluginProxy = createProxyRuntime();
// Full init using the same pathway as reloadall
reloadPlugin(true);
loadVersionFile();
getLogger().info("VotingPlugin loaded, using method: " + getVotingPluginProxy().getMethod().toString());
if (!"NOTSET".equals(buildNumber)) {
getLogger().info("Detected using dev build number: " + buildNumber);
}
}
@Override
public void onDisable() {
synchronized (reloadLock) {
reloading = true;
cancelPlatformTasks();
try {
if (votingPluginProxy != null) {
votingPluginProxy.onDisable();
}
} catch (Exception ignored) {
}
try {
if (voteCacheFile != null) {
voteCacheFile.save();
}
} catch (Exception ignored) {
}
try {
if (nonVotedPlayersCache != null) {
nonVotedPlayersCache.save();
}
} catch (Exception ignored) {
}
try {
if (timer != null) {
timer.shutdownNow();
}
} catch (Exception ignored) {
}
reloading = false;
}
getLogger().info("VotingPlugin disabled");
}
/**
* Handles plugin messages from backend servers.
*
* @param ev plugin message event
*/
@EventHandler
public void onPluginMessage(PluginMessageEvent ev) {
if (config == null) {
return;
}
if (!ev.getTag().equalsIgnoreCase(getConfig().getPluginMessageChannel())) {
return;
}
ev.setCancelled(true);
if (!(ev.getSender() instanceof Server)) {
debug("Ignore plugin message (not from server)");
return;
}
// During reload, queue and replay later to avoid calling into disposed runtime
if (reloading) {
byte[] copy = new byte[ev.getData().length];
System.arraycopy(ev.getData(), 0, copy, 0, ev.getData().length);
queuedPluginMessages.add(copy);
return;
}
handlePluginMessageBytes(ev.getData());
}
/**
* Hot-reloads the proxy runtime.
*
* <p>
* Two modes:
* </p>
* <ul>
* <li><b>Soft reload</b> ({@code loadMysql=false}): keeps the existing
* {@link VotingPluginProxy} instance (and its MySQL pool) alive. Only reloads
* config/channel and applies runtime-only changes via
* {@link VotingPluginProxy#reload()}.</li>
* <li><b>Full reload</b> ({@code loadMysql=true}): fully tears down the old
* runtime (including MySQL), recreates the proxy instance, reconnects MySQL
* from config, then calls
* {@link VotingPluginProxy#load(com.bencodez.votingplugin.proxy.cache.IVoteCache, com.bencodez.votingplugin.proxy.cache.nonvoted.INonVotedPlayersStorage)}.</li>
* </ul>
*
* @param loadMysql true to fully reinitialize MySQL and rebuild the proxy
* runtime, false for a config/runtime-only reload
*/
public void reloadPlugin(boolean loadMysql) {
synchronized (reloadLock) {
reloading = true;
final String oldChannel = (config != null) ? config.getPluginMessageChannel() : null;
// Always stop platform tasks while we touch state
cancelPlatformTasks();
// Always reload config first
try {
config.load();
} catch (Exception e) {
getLogger().severe("Failed to reload bungeeconfig.yml");
e.printStackTrace();
}
// Update channel registration if changed (safe for both modes)
try {
final String newChannel = config.getPluginMessageChannel();
if (oldChannel != null && newChannel != null && !oldChannel.equalsIgnoreCase(newChannel)) {
try {
getProxy().unregisterChannel(oldChannel);
} catch (Exception ignored) {
}
}
try {
if (newChannel != null && !newChannel.isEmpty()) {
getProxy().registerChannel(newChannel);
}
} catch (Exception ignored) {
}
} catch (Exception e) {
// keep going; channel mismatch should not kill reload
e.printStackTrace();
}
// =========================
// SOFT RELOAD (NO MYSQL)
// =========================
if (!loadMysql) {
try {
if (votingPluginProxy != null) {
// Applies runtime-only config (method selection, multiproxy settings, vote
// party settings, etc.)
votingPluginProxy.reload();
}
} catch (Throwable t) {
getLogger().severe("Error while applying soft reload");
t.printStackTrace();
}
// Restart tasks (they rely on already-loaded handlers on the existing proxy
// instance)
try {
schedulePlatformTasks();
} catch (Exception e) {
e.printStackTrace();
}
reloading = false;
return;
}
// =========================
// FULL RELOAD (WITH MYSQL)
// =========================
// Save caches best-effort before teardown
try {
if (voteCacheFile != null) {
voteCacheFile.save();
}
} catch (Exception ignored) {
}
try {
if (nonVotedPlayersCache != null) {
nonVotedPlayersCache.save();
}
} catch (Exception ignored) {
}
// Tear down old runtime (this WILL shutdown MySQL because proxy owns it)
try {
if (votingPluginProxy != null) {
votingPluginProxy.onDisable();
}
} catch (Exception ignored) {
}
// Recreate runtime
votingPluginProxy = createProxyRuntime();
// Initialize MySQL BEFORE calling proxy.load(...)
try {
if (config.hasDatabaseConfigured()) {
votingPluginProxy.loadMysql(getMysqlConfig(), getGlobalDataMysqlConfig());
} else {
getLogger().severe("MySQL settings not set in bungeeconfig.yml");
votingPluginProxy.setProxyMySQL(null);
}
} catch (Throwable t) {
getLogger().severe("Failed to initialize MySQL during reload");
t.printStackTrace();
votingPluginProxy.setProxyMySQL(null);
}
// Abort cleanly if MySQL did not initialize (prevents NPE inside
// VotingPluginProxy.load)
if (votingPluginProxy.getProxyMySQL() == null) {
getLogger().severe("Reload aborted: Proxy MySQL is not initialized (see logs above).");
reloading = false;
return;
}
// Ensure caches exist and load into runtime
try {
if (voteCacheFile == null) {
voteCacheFile = new BungeeJsonVoteCache(this);
}
if (nonVotedPlayersCache == null) {
nonVotedPlayersCache = new BungeeJsonNonVotedPlayersCache(
new File(getDataFolder(), "nonvotedplayerscache.json"));
}
// Load proxy state (requires MySQL)
votingPluginProxy.load(voteCacheFile, nonVotedPlayersCache);
// Apply runtime-only config too
votingPluginProxy.reload();
} catch (Throwable t) {
getLogger().severe("Reload aborted while loading proxy state");
t.printStackTrace();
reloading = false;
return;
}
// Restart tasks after a successful swap
try {
schedulePlatformTasks();
} catch (Exception e) {
e.printStackTrace();
}
reloading = false;
}
// Replay queued plugin messages after swap
drainQueuedPluginMessages();
initVotifierListenerIfNeeded();
// Send server name message again (safe)
try {
getVotingPluginProxy().sendServerNameMessage();
} catch (Exception ignored) {
}
}
/**
* Creates a new VotingPluginProxy instance wired to this platform.
*
* @return new proxy runtime
*/
private VotingPluginProxy createProxyRuntime() {
return new VotingPluginProxy() {
@Override
public void broadcast(String message) {
getProxy().broadcast(new TextComponent(ChatColor.translateAlternateColorCodes('&', message)));
}
@Override
public void debug(String str) {
debug2(str);
}
@Override
public Set<String> getAllAvailableServers() {
return getAvailableAllServers();
}
@Override
public VotingPluginProxyConfig getConfig() {
return config;
}
@Override
public String getCurrentPlayerServer(String player) {
ProxiedPlayer p = getProxy().getPlayer(player);
if (p != null && p.getServer() != null) {
return p.getServer().getInfo().getName();
}
return "";
}
@Override
public File getDataFolderPlugin() {
return getDataFolder();
}
@Override
public String getProperName(String uuid, String playerName) {
return getProperPlayerName(uuid, playerName);
}
@Override
public String getUUID(String playerName) {
if (playerName == null || playerName.isEmpty() || "null".equalsIgnoreCase(playerName)) {
return "";
}
if (!config.getOnlineMode()) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.toLowerCase(Locale.ROOT).trim())
.getBytes(java.nio.charset.StandardCharsets.UTF_8)).toString();
}
ProxiedPlayer p = getProxy().getPlayer(playerName);
if (p != null && p.isConnected()) {
playerName = p.getName();
}
for (Entry<UUID, String> entry : getVotingPluginProxy().getUuidPlayerNameCache().entrySet()) {
String cachedName = entry.getValue();
if (cachedName != null && cachedName.equalsIgnoreCase(playerName)) {
playerName = cachedName;
break;
}
}
if (p != null && p.isConnected()) {
return p.getUniqueId().toString();
}
for (Entry<UUID, String> entry : getVotingPluginProxy().getUuidPlayerNameCache().entrySet()) {
if (entry.getValue() != null && entry.getValue().equalsIgnoreCase(playerName)) {
return entry.getKey().toString();
}
}
if (getVotingPluginProxy().getProxyMySQL() != null) {
String str = getVotingPluginProxy().getProxyMySQL().getUUID(playerName);
if (str != null) {
return str;
}
}
return getVotingPluginProxy().getNonVotedPlayersCache().getUUID(playerName);
}
@Override
public String getPluginVersion() {
return getDescription().getVersion();
}
@Override
public int getVoteCacheCurrentVotePartyVotes() {
return voteCacheFile.getVotePartyCurrentVotes();
}
@Override
public long getVoteCacheLastUpdated() {
return voteCacheFile.getLong("Time.LastUpdated", 0L);
}
@Override
public int getVoteCachePrevDay() {
return voteCacheFile.getInt("Time.Day", 0);
}
@Override
public String getVoteCachePrevMonth() {
return voteCacheFile.getString("Time.Month", "");
}
@Override
public int getVoteCachePrevWeek() {
return voteCacheFile.getInt("Time.Week", 0);
}
@Override
public int getVoteCacheVotePartyIncreaseVotesRequired() {
return voteCacheFile.getVotePartyInreaseVotesRequired();
}
@Override
public boolean isPlayerOnline(String playerName) {
ProxiedPlayer player = getProxy().getPlayer(playerName);
return player != null && player.isConnected();
}
@Override
public boolean isServerValid(String server) {
return getProxy().getServerInfo(server) != null;
}
@Override
public boolean isSomeoneOnlineServer(String server) {
return getProxy().getServerInfo(server) != null
&& !getProxy().getServerInfo(server).getPlayers().isEmpty();
}
@Override
public boolean isVoteCacheIgnoreTime() {
return voteCacheFile.getBoolean("Time.IgnoreTime", false);
}
@Override
public void log(String message) {
getLogger().info(message);
}
@Override
public void logSevere(String message) {
getLogger().severe(message);
}
@Override
public void runAsync(Runnable run) {
runAsyncNow(run);
}
@Override
public void runConsoleCommand(String command) {
getProxy().getPluginManager().dispatchCommand(getProxy().getConsole(), command);
}
@Override
public void saveVoteCacheFile() {
voteCacheFile.save();
}
@Override
public void sendPluginMessageData(String server, String channel, byte[] data, boolean queue) {
if (getProxy().getServerInfo(server) != null) {
getProxy().getServerInfo(server).sendData(channel, data, queue);
}
}
@Override
public void setVoteCacheLastUpdated() {
voteCacheFile.setLong("Time.LastUpdated", System.currentTimeMillis());
voteCacheFile.save();
}
@Override
public void setVoteCachePrevDay(int day) {
voteCacheFile.setInt("Time.Day", day);
voteCacheFile.save();
}
@Override
public void setVoteCachePrevMonth(String text) {
voteCacheFile.setString("Time.Month", text);
voteCacheFile.save();
}
@Override
public void setVoteCachePrevWeek(int week) {
voteCacheFile.setInt("Time.Week", week);
voteCacheFile.save();
}
@Override
public void setVoteCacheVoteCacheIgnoreTime(boolean ignore) {
voteCacheFile.setBoolean("Time.IgnoreTime", ignore);
voteCacheFile.save();
}
@Override
public void setVoteCacheVotePartyCurrentVotes(int votes) {
voteCacheFile.setVotePartyCurrentVotes(votes);
}
@Override
public void setVoteCacheVotePartyIncreaseVotesRequired(int votes) {
voteCacheFile.setVotePartyInreaseVotesRequired(votes);
}
@Override
public void warn(String message) {
getLogger().warning(message);
}
@Override
public void reloadCore(boolean mysql) {
// mysql==true should do full reloadall behavior on the platform
reloadPlugin(mysql);
}
@Override
public ScheduledExecutorService getScheduler() {
return timer;
}
@Override
public MysqlConfig getVoteCacheMySQLConfig() {
return new MysqlConfigBungee(config.getData().getSection("VoteCache"));
}
@Override
public MysqlConfig getNonVotedCacheMySQLConfig() {
return new MysqlConfigBungee(config.getData().getSection("NonVotedCache"));
}
@Override
public MysqlConfig getVoteLoggingMySQLConfig() {
return new MysqlConfigBungee(config.getData().getSection("VoteLogging"));
}
@Override
public void loadTaskTimer(Runnable runnable, long delaySeconds, long repeatSeconds) {
timer.scheduleAtFixedRate(runnable, delaySeconds, repeatSeconds, TimeUnit.SECONDS);
}
};
}
/**
* Gets the MySQL configuration for Bungee.
*
* <p>
* Prefers the "Database" section if it exists and contains keys; otherwise
* falls back to legacy root keys.
* </p>
*
* @return mysql config (never null)
*/
public MysqlConfig getMysqlConfig() {
Configuration data = config.getData();
Configuration db = config.sectionOrNull(data, "Database");
if (isMissingOrEmptySection(db)) {
// legacy root keys
return new MysqlConfigBungee(data);
}
return new MysqlConfigBungee(db);
}
/**
* Checks whether a configuration section is missing or empty.
*
* @param section config section (may be null)
* @return true if null or contains no keys
*/
private static boolean isMissingOrEmptySection(Configuration section) {
if (section == null) {
return true;
}
return section.getKeys() == null || section.getKeys().isEmpty();
}
public MysqlConfig getGlobalDataMysqlConfig() {
return new MysqlConfigBungee(config.getData().getSection("GlobalData"));
}
/**
* Initializes votifier listener if enabled and available. Registers the
* listener only once.
*/
private void initVotifierListenerIfNeeded() {
boolean eventFound = true;
try {
Class.forName("com.vexsoftware.votifier.bungee.events.VotifierEvent");
} catch (ClassNotFoundException e) {
eventFound = false;
}
if (!eventFound) {
getVotingPluginProxy().setVotifierEnabled(false);
return;
}
if (getVotingPluginProxy().isVotifierEnabled()) {
if (voteEventBungee == null) {
try {
voteEventBungee = new VoteEventBungee(this);
getProxy().getPluginManager().registerListener(this, voteEventBungee);
} catch (Exception e) {
getVotingPluginProxy().setVotifierEnabled(false);
}
}
}
}
/**
* Cancels Bungee scheduled tasks owned by this plugin.
*/
private void cancelPlatformTasks() {
try {
if (voteCheckTask != null) {
voteCheckTask.cancel();
voteCheckTask = null;
}
} catch (Exception ignored) {
}
try {
if (cacheSaveTask != null) {
cacheSaveTask.cancel();
cacheSaveTask = null;
}
} catch (Exception ignored) {
}
}
/**
* Schedules platform periodic tasks.
*/
private void schedulePlatformTasks() {
voteCheckTask = getProxy().getScheduler().schedule(this, new Runnable() {
@Override
public void run() {
for (String server : getVotingPluginProxy().getVoteCacheHandler().getCachedVotesServers()) {
getVotingPluginProxy().checkCachedVotes(server);
}
for (ProxiedPlayer player : getProxy().getPlayers()) {
getVotingPluginProxy().checkOnlineVotes(player.getName(), player.getUniqueId().toString(), null);
}
}
}, 120L, 60L, TimeUnit.SECONDS);
cacheSaveTask = getProxy().getScheduler().schedule(this, new Runnable() {
@Override
public void run() {
if (nonVotedPlayersCache != null) {
debug("Checking nonvotedplayerscache...");
getVotingPluginProxy().getNonVotedPlayersCache().check();
}
if (voteCacheFile != null) {
voteCacheFile.save();
}
}
}, 1L, 60L, TimeUnit.MINUTES);
}
/**
* Runs a task asynchronously on the Bungee scheduler.
*
* @param runnable task
*/
private void runAsyncNow(Runnable runnable) {
getProxy().getScheduler().runAsync(this, runnable);
}
/**
* Parses and handles a plugin message payload.
*
* @param data payload bytes
*/
private void handlePluginMessageBytes(byte[] data) {
ByteArrayInputStream instream = new ByteArrayInputStream(data);
DataInputStream in = new DataInputStream(instream);
try {
getVotingPluginProxy().onPluginMessageReceived(in);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Replays queued plugin messages after reload completes.
*/
private void drainQueuedPluginMessages() {
byte[] msg;
while ((msg = queuedPluginMessages.poll()) != null) {
try {
handlePluginMessageBytes(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@@ -1,117 +0,0 @@
package com.bencodez.votingplugin.proxy.cache;
/**
* Interface for data node operations.
*/
public interface DataNode {
/**
* Checks if this node is an object.
*
* @return true if this is an object node
*/
boolean isObject();
/**
* Checks if this node is an array.
*
* @return true if this is an array node
*/
boolean isArray();
/**
* Checks if this node is a primitive value.
*
* @return true if this is a primitive node
*/
boolean isPrimitive();
/**
* Gets a child node by key.
*
* @param key the key
* @return the child data node
*/
DataNode get(String key);
/**
* Gets a child node by index.
*
* @param index the index
* @return the child data node
*/
DataNode get(int index);
/**
* Sets a value for a key.
*
* @param key the key
* @param value the value to set
*/
void set(String key, Object value);
/**
* Sets a value at an index.
*
* @param index the index
* @param value the value to set
*/
void set(int index, Object value);
/**
* Gets the primitive value.
*
* @return the primitive object
*/
Object asPrimitive();
/**
* Gets the value as a string.
*
* @return the string value
*/
String asString();
/**
* Gets the value as a long.
*
* @return the long value
*/
long asLong();
/**
* Checks if a key exists.
*
* @param key the key to check
* @return true if the key exists
*/
boolean has(String key);
/**
* Checks if an index exists.
*
* @param index the index to check
* @return true if the index exists
*/
boolean has(int index);
/**
* Gets the value as an int.
*
* @return the int value
*/
int asInt();
/**
* Gets the value as a boolean.
*
* @return the boolean value
*/
boolean asBoolean();
/**
* Returns the internal representation (JsonElement or ConfigurationNode).
*
* @return the internal object
*/
Object toInternal();
}
@@ -1,197 +0,0 @@
package com.bencodez.votingplugin.proxy.cache;
import java.util.Collection;
import com.bencodez.votingplugin.proxy.OfflineBungeeVote;
import com.bencodez.votingplugin.timequeue.VoteTimeQueue;
/**
* Interface for vote caching operations.
*/
public interface IVoteCache {
/**
* Adds a timed vote to the cache.
*
* @param num the vote number
* @param voteTimedQueue the vote time queue
*/
void addTimedVote(int num, VoteTimeQueue voteTimedQueue);
/**
* Adds a vote for a specific server.
*
* @param server the server name
* @param num the vote number
* @param voteData the vote data
*/
void addVote(String server, int num, OfflineBungeeVote voteData);
/**
* Adds a vote for an online player.
*
* @param player the player name
* @param num the vote number
* @param voteData the vote data
*/
void addVoteOnline(String player, int num, OfflineBungeeVote voteData);
/**
* Clears all cached data.
*/
void clearData();
/**
* Gets all online votes for a player.
*
* @param name the player name
* @return collection of vote identifiers
*/
Collection<String> getOnlineVotes(String name);
/**
* Gets a specific online vote for a player.
*
* @param name the player name
* @param num the vote number
* @return the vote data node
*/
DataNode getOnlineVotes(String name, String num);
/**
* Gets all cached players.
*
* @return collection of player names
*/
Collection<String> getPlayers();
/**
* Gets all cached servers.
*
* @return collection of server names
*/
Collection<String> getServers();
/**
* Gets all votes for a server.
*
* @param server the server name
* @return collection of vote identifiers
*/
Collection<String> getServerVotes(String server);
/**
* Gets a specific vote for a server.
*
* @param server the server name
* @param num the vote number
* @return the vote data node
*/
DataNode getServerVotes(String server, String num);
/**
* Gets all timed vote cache entries.
*
* @return collection of cache keys
*/
Collection<String> getTimedVoteCache();
/**
* Gets a specific timed vote cache entry.
*
* @param key the cache key
* @return the vote data node
*/
DataNode getTimedVoteCache(String key);
/**
* Gets the vote party cache for a server.
*
* @param server the server name
* @return the cached vote count
*/
int getVotePartyCache(String server);
/**
* Gets the current vote party votes.
*
* @return the current vote count
*/
int getVotePartyCurrentVotes();
/**
* Gets the vote party increase votes required.
*
* @return the increase amount
*/
int getVotePartyInreaseVotesRequired();
/**
* Sets the vote party cache for a server.
*
* @param server the server name
* @param amount the vote amount
*/
void setVotePartyCache(String server, int amount);
/**
* Sets the current vote party votes.
*
* @param amount the vote amount
*/
void setVotePartyCurrentVotes(int amount);
/**
* Sets the vote party increase votes required.
*
* @param amount the increase amount
*/
void setVotePartyInreaseVotesRequired(int amount);
/**
* Saves the cache data.
*/
void save();
/**
* Reloads the cache data.
*/
void reload();
/**
* Removes all online votes for a player.
*
* @param player the player name
*/
void removeOnlineVotes(String player);
/**
* Removes all votes for a server.
*
* @param server the server name
*/
void removeServerVotes(String server);
/**
* Removes a specific vote for a server.
*
* @param server the server name
* @param uuid the vote UUID
*/
void removeServerVote(String server, String uuid);
/**
* Removes a vote from a server.
*
* @param server the server name
* @param vote the vote to remove
*/
void removeVote(String server, OfflineBungeeVote vote);
/**
* Removes an online vote.
*
* @param vote the vote to remove
*/
void removeOnlineVote(OfflineBungeeVote vote);
}
@@ -1,458 +0,0 @@
package com.bencodez.votingplugin.proxy.cache;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement; // NEW
import java.util.ArrayList;
import java.util.List;
import java.util.Set; // NEW
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; // NEW
import com.bencodez.simpleapi.sql.mysql.AbstractSqlTable;
import com.bencodez.simpleapi.sql.mysql.DbType;
import com.bencodez.simpleapi.sql.mysql.MySQL;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
import com.bencodez.simpleapi.sql.mysql.queries.Query;
import com.bencodez.votingplugin.proxy.OfflineBungeeVote;
/**
* Table for caching online votes in the proxy.
*/
public abstract class ProxyOnlineVoteCacheTable extends AbstractSqlTable {
// Prevent repeated startup DDL across multiple instances/subclasses
private static final Set<String> MIGRATED_VOTEID = ConcurrentHashMap.newKeySet();
private static final Set<String> ENSURED_INDEXES = ConcurrentHashMap.newKeySet();
@Override
public String getPrimaryKeyColumn() {
return "id";
}
@Override
public String buildCreateTableSql(DbType dbType) {
if (dbType == DbType.POSTGRESQL) {
return "CREATE TABLE IF NOT EXISTS " + qi(getTableName()) + " (" + qi("id") + " BIGSERIAL PRIMARY KEY, "
+ qi("uuid") + " " + bestUuidType() + ", " + qi("voteid") + " VARCHAR(36), " + qi("playerName")
+ " VARCHAR(100), " + qi("service") + " VARCHAR(100), " + qi("time") + " BIGINT, " + qi("realVote")
+ " BOOLEAN, " + qi("text") + " TEXT" + ");";
}
return "CREATE TABLE IF NOT EXISTS " + qi(getTableName()) + " (" + qi("id") + " INT AUTO_INCREMENT PRIMARY KEY,"
+ qi("uuid") + " VARCHAR(37)," + qi("voteid") + " VARCHAR(36)," + qi("playerName") + " VARCHAR(100),"
+ qi("service") + " VARCHAR(100)," + qi("time") + " BIGINT," + qi("realVote") + " TINYINT(1),"
+ qi("text") + " TEXT," + "INDEX idx_uuid (" + qi("uuid") + ")," + "INDEX idx_time (" + qi("time") + ")"
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
}
/**
* Removes all votes for a specific UUID.
* @param uuid the player UUID
*/
public void removeVotesByUuid(String uuid) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("uuid") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(1, UUID.fromString(uuid));
} else {
ps.setString(1, uuid);
}
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
@Override
public abstract void logSevere(String msg);
@Override
public abstract void logInfo(String msg);
@Override
public abstract void debug(Throwable t);
/**
* Constructor using an existing MySQL connection.
* @param existingMysql the existing MySQL instance
* @param tablePrefix the table prefix
* @param debug whether debug mode is enabled
*/
public ProxyOnlineVoteCacheTable(MySQL existingMysql, String tablePrefix, boolean debug) {
super((tablePrefix != null ? tablePrefix : "") + "votingplugin_onlinevotecache", existingMysql, debug);
// best-effort migrations (no nested connections)
alterColumnType("uuid", bestUuidType());
addVoteIdColumnIfMissingOnce();
ensureIndexesOnce();
}
/**
* Constructor using a MySQL configuration.
* @param config the MySQL configuration
* @param debug whether debug mode is enabled
*/
public ProxyOnlineVoteCacheTable(MysqlConfig config, boolean debug) {
super("votingplugin_onlinevotecache", config, debug);
alterColumnType("uuid", bestUuidType());
addVoteIdColumnIfMissingOnce();
ensureIndexesOnce();
}
/**
* Updates the text field of a vote.
* @param vote the vote to update
* @param newText the new text
*/
public void updateVoteText(OfflineBungeeVote vote, String newText) {
if (vote == null) {
return;
}
String sql = "UPDATE " + qi(getTableName()) + " SET " + qi("text") + " = ?" + " WHERE " + qi("uuid")
+ " = ? AND " + qi("service") + " = ? AND " + qi("time") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, newText);
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(2, UUID.fromString(vote.getUuid()));
} else {
ps.setString(2, vote.getUuid());
}
ps.setString(3, vote.getService());
ps.setLong(4, vote.getTime());
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
private void ensureIndexesOnce() {
if (getDbType() != DbType.POSTGRESQL) {
return;
}
String key = getDbType() + ":" + getTableName() + ":indexes";
if (!ENSURED_INDEXES.add(key)) {
return;
}
// Use ONE connection for all index DDL to avoid pool starvation (pool=1 safe)
try (Connection conn = mysql.getConnectionManager().getConnection(); Statement st = conn.createStatement()) {
st.executeUpdate("CREATE INDEX IF NOT EXISTS idx_uuid ON " + qi(getTableName()) + " (" + qi("uuid") + ");");
st.executeUpdate("CREATE INDEX IF NOT EXISTS idx_time ON " + qi(getTableName()) + " (" + qi("time") + ");");
} catch (SQLException e) {
debug(e);
}
}
private void addVoteIdColumnIfMissingOnce() {
String key = getDbType() + ":" + getTableName() + ":voteid";
if (!MIGRATED_VOTEID.add(key)) {
return;
}
addVoteIdColumnIfMissing();
}
private void addVoteIdColumnIfMissing() {
final boolean pg = getDbType() == DbType.POSTGRESQL;
final String schemaFilter = pg ? "table_schema = current_schema()" : "TABLE_SCHEMA = DATABASE()";
// Lowercase compare avoids casing issues on some setups
final String checkSql = "SELECT 1 FROM information_schema.columns WHERE " + schemaFilter
+ " AND LOWER(table_name) = LOWER(?) AND LOWER(column_name) = LOWER(?) LIMIT 1;";
// IMPORTANT: do check + alter on SAME connection to avoid deadlock when
// poolSize=1
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(checkSql)) {
ps.setString(1, getTableName());
ps.setString(2, "voteid");
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return; // exists
}
}
String alter = "ALTER TABLE " + qi(getTableName()) + " ADD COLUMN " + qi("voteid") + " VARCHAR(36);";
try (Statement st = conn.createStatement()) {
st.executeUpdate(alter);
}
} catch (SQLException e) {
logSevere("Failed to add voteid column to " + getTableName() + ": " + e.getMessage());
debug(e);
}
}
// --- INSERT ---
/**
* Inserts a new vote into the cache.
* @param voteId the vote ID
* @param uuid the player UUID
* @param playerName the player name
* @param service the voting service
* @param time the vote time
* @param real whether this is a real vote
* @param text the vote text/payload
*/
public void insertVote(UUID voteId, String uuid, String playerName, String service, long time, boolean real,
String text) {
String sql = "INSERT INTO " + qi(getTableName()) + " (" + qi("uuid") + ", " + qi("voteid") + ", "
+ qi("playerName") + ", " + qi("service") + ", " + qi("time") + ", " + qi("realVote") + ", "
+ qi("text") + ") VALUES (?, ?, ?, ?, ?, ?, ?);";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(1, UUID.fromString(uuid));
} else {
ps.setString(1, uuid);
}
ps.setString(2, voteId != null ? voteId.toString() : null);
ps.setString(3, playerName);
ps.setString(4, service);
ps.setLong(5, time);
if (getDbType() == DbType.POSTGRESQL) {
ps.setBoolean(6, real);
} else {
ps.setInt(6, real ? 1 : 0);
}
ps.setString(7, text);
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
// --- GET ---
/**
* Gets all votes from the table.
* @return list of all vote rows
*/
public List<VoteRow> getAllVotes() {
return selectVotes("SELECT * FROM " + qi(getTableName()) + ";", null);
}
/**
* Gets all votes for a specific UUID.
* @param uuid the player UUID
* @return list of vote rows
*/
public List<VoteRow> getVotesByUUID(String uuid) {
String sql = "SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("uuid") + " = ?;";
Object param = (getDbType() == DbType.POSTGRESQL) ? UUID.fromString(uuid) : uuid;
return selectVotes(sql, new Object[] { param });
}
// --- DELETE ---
/**
* Removes a vote by its ID.
* @param id the vote ID
*/
public void removeVoteById(int id) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("id") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
ps.executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Removes a specific vote.
* @param vote the vote to remove
*/
public void removeVote(OfflineBungeeVote vote) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("uuid") + " = ? AND " + qi("service")
+ " = ? AND " + qi("time") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(1, UUID.fromString(vote.getUuid()));
} else {
ps.setString(1, vote.getUuid());
}
ps.setString(2, vote.getService());
ps.setLong(3, vote.getTime());
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
/**
* Clears all votes from the table.
*/
public void clearTable() {
try {
if (getDbType() == DbType.POSTGRESQL) {
new Query(mysql, "TRUNCATE TABLE " + qi(getTableName()) + " RESTART IDENTITY;").executeUpdate();
} else {
new Query(mysql, "TRUNCATE TABLE " + qi(getTableName()) + ";").executeUpdate();
}
} catch (SQLException e) {
debug(e);
}
}
// --- Internals ---
private List<VoteRow> selectVotes(String sql, Object[] params) {
List<VoteRow> list = new ArrayList<>();
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (params != null) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
}
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(new VoteRow(rs.getInt("id"), rs.getString("voteid"), rs.getString("uuid"),
rs.getString("playerName"), rs.getString("service"), rs.getLong("time"),
(getDbType() == DbType.POSTGRESQL ? rs.getBoolean("realVote") : rs.getInt("realVote") == 1),
rs.getString("text")));
}
}
} catch (SQLException e) {
debug(e);
}
return list;
}
/**
* Represents a row in the online vote cache table.
*/
public static class VoteRow {
private final int id;
private final String voteId;
private final String uuid;
private final String playerName;
private final String service;
private final long time;
private final boolean realVote;
private final String text;
/**
* Constructor for VoteRow.
* @param id the row ID
* @param voteId the vote ID
* @param uuid the player UUID
* @param playerName the player name
* @param service the voting service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text the vote text
*/
public VoteRow(int id, String voteId, String uuid, String playerName, String service, long time,
boolean realVote, String text) {
this.id = id;
this.voteId = voteId;
this.uuid = uuid;
this.playerName = playerName;
this.service = service;
this.time = time;
this.realVote = realVote;
this.text = text;
}
/**
* Gets the row ID.
* @return the ID
*/
public int getId() {
return id;
}
/**
* Gets the vote ID.
* @return the vote ID
*/
public String getVoteId() {
return voteId;
}
/**
* Gets the player UUID.
* @return the UUID
*/
public String getUuid() {
return uuid;
}
/**
* Gets the player name.
* @return the player name
*/
public String getPlayerName() {
return playerName;
}
/**
* Gets the voting service.
* @return the service name
*/
public String getService() {
return service;
}
/**
* Gets the vote time.
* @return the time in milliseconds
*/
public long getTime() {
return time;
}
/**
* Checks if this is a real vote.
* @return true if real vote
*/
public boolean isRealVote() {
return realVote;
}
/**
* Gets the vote text.
* @return the text/payload
*/
public String getText() {
return text;
}
}
}
@@ -1,257 +0,0 @@
package com.bencodez.votingplugin.proxy.cache;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.bencodez.simpleapi.sql.mysql.AbstractSqlTable;
import com.bencodez.simpleapi.sql.mysql.DbType;
import com.bencodez.simpleapi.sql.mysql.MySQL;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
import com.bencodez.simpleapi.sql.mysql.queries.Query;
/**
* Table for caching timed votes in the proxy.
*/
public abstract class ProxyTimedVoteCacheTable extends AbstractSqlTable {
@Override
public String getPrimaryKeyColumn() {
return "id";
}
@Override
public String buildCreateTableSql(DbType dbType) {
if (dbType == DbType.POSTGRESQL) {
return "CREATE TABLE IF NOT EXISTS " + qi(getTableName()) + " ("
+ qi("id") + " BIGSERIAL PRIMARY KEY, "
+ qi("playerName") + " VARCHAR(100), "
+ qi("service") + " VARCHAR(100), "
+ qi("time") + " BIGINT"
+ ");";
}
return "CREATE TABLE IF NOT EXISTS " + qi(getTableName()) + " ("
+ qi("id") + " INT AUTO_INCREMENT PRIMARY KEY,"
+ qi("playerName") + " VARCHAR(100),"
+ qi("service") + " VARCHAR(100),"
+ qi("time") + " BIGINT,"
+ "INDEX idx_time (" + qi("time") + ")"
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
}
@Override
public abstract void logSevere(String msg);
@Override
public abstract void logInfo(String msg);
@Override
public abstract void debug(Throwable t);
/**
* Constructor using an existing MySQL connection.
* @param existingMysql the existing MySQL instance
* @param tablePrefix the table prefix
* @param debug whether debug mode is enabled
*/
public ProxyTimedVoteCacheTable(MySQL existingMysql, String tablePrefix, boolean debug) {
super((tablePrefix != null ? tablePrefix : "") + "votingplugin_timedvotecache",
existingMysql,
debug);
ensureIndexes();
}
/**
* Constructor using a MySQL configuration.
* @param config the MySQL configuration
* @param debug whether debug mode is enabled
*/
public ProxyTimedVoteCacheTable(MysqlConfig config, boolean debug) {
super("votingplugin_timedvotecache", config, debug);
ensureIndexes();
}
private void ensureIndexes() {
if (getDbType() == DbType.POSTGRESQL) {
try {
new Query(mysql, "CREATE INDEX IF NOT EXISTS idx_time ON " + qi(getTableName()) + " (" + qi("time")
+ ");").executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
}
// --- INSERT ---
/**
* Inserts a timed vote.
* @param playerName the player name
* @param service the voting service
* @param time the vote time
*/
public void insertTimedVote(String playerName, String service, long time) {
String sql = "INSERT INTO " + qi(getTableName()) + " (" + qi("playerName") + ", " + qi("service") + ", "
+ qi("time") + ") VALUES (?, ?, ?);";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, playerName);
ps.setString(2, service);
ps.setLong(3, time);
ps.executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
// --- GET ---
/**
* Gets all timed votes.
* @return list of all timed vote rows
*/
public List<TimedVoteRow> getAllVotes() {
return selectVotes("SELECT * FROM " + qi(getTableName()) + ";", null);
}
/**
* Gets expired votes before a given time.
* @param now the current time in milliseconds
* @return list of expired vote rows
*/
public List<TimedVoteRow> getExpiredVotes(long now) {
return selectVotes("SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("time") + " <= ?;",
new Object[] { now });
}
// --- DELETE ---
/**
* Removes a vote by its ID.
* @param id the vote ID
*/
public void removeVoteById(int id) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("id") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
ps.executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Removes expired votes before a given time.
* @param now the current time in milliseconds
*/
public void removeExpiredVotes(long now) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("time") + " <= ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, now);
ps.executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Clears all votes from the table.
*/
public void clearTable() {
try {
if (getDbType() == DbType.POSTGRESQL) {
new Query(mysql, "TRUNCATE TABLE " + qi(getTableName()) + " RESTART IDENTITY;").executeUpdate();
} else {
new Query(mysql, "TRUNCATE TABLE " + qi(getTableName()) + ";").executeUpdate();
}
} catch (SQLException e) {
debug(e);
}
}
private List<TimedVoteRow> selectVotes(String sql, Object[] params) {
List<TimedVoteRow> list = new ArrayList<>();
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (params != null) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
}
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(new TimedVoteRow(
rs.getInt("id"),
rs.getString("playerName"),
rs.getString("service"),
rs.getLong("time")
));
}
}
} catch (SQLException e) {
debug(e);
}
return list;
}
/**
* Represents a row in the timed vote cache table.
*/
public static class TimedVoteRow {
private final int id;
private final String playerName;
private final String service;
private final long time;
/**
* Constructor for TimedVoteRow.
* @param id the row ID
* @param playerName the player name
* @param service the voting service
* @param time the vote time
*/
public TimedVoteRow(int id, String playerName, String service, long time) {
this.id = id;
this.playerName = playerName;
this.service = service;
this.time = time;
}
/**
* Gets the row ID.
* @return the ID
*/
public int getId() {
return id;
}
/**
* Gets the player name.
* @return the player name
*/
public String getPlayerName() {
return playerName;
}
/**
* Gets the voting service.
* @return the service name
*/
public String getService() {
return service;
}
/**
* Gets the vote time.
* @return the time in milliseconds
*/
public long getTime() {
return time;
}
}
}
@@ -1,581 +0,0 @@
package com.bencodez.votingplugin.proxy.cache;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import com.bencodez.simpleapi.sql.mysql.AbstractSqlTable;
import com.bencodez.simpleapi.sql.mysql.DbType;
import com.bencodez.simpleapi.sql.mysql.MySQL;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
import com.bencodez.simpleapi.sql.mysql.queries.Query;
import com.bencodez.votingplugin.proxy.OfflineBungeeVote;
/**
* Proxy vote cache (per-server cached votes).
*
* Columns: - id (auto increment primary key) - uuid (player uuid) - voteid
* (vote uuid, nullable) - playerName - service - time (millis) - realVote
* (bool) - text (payload) - server
*
* Indexes: - server - uuid - time
*/
public abstract class ProxyVoteCacheTable extends AbstractSqlTable {
// Ensure we don't run the same migration repeatedly during startup (or for each
// subclass).
private static final Set<String> MIGRATED_VOTEID = ConcurrentHashMap.newKeySet();
private static final Set<String> ENSURED_INDEXES = ConcurrentHashMap.newKeySet();
// ---- Required hooks ----
@Override
public String getPrimaryKeyColumn() {
// This table's actual PK is numeric id; AbstractSqlTable caches keys as
// Strings.
return "id";
}
@Override
public String buildCreateTableSql(DbType dbType) {
StringBuilder sb = new StringBuilder();
if (dbType == DbType.POSTGRESQL) {
sb.append("CREATE TABLE IF NOT EXISTS ").append(qi(getTableName())).append(" (").append(qi("id"))
.append(" BIGSERIAL PRIMARY KEY, ").append(qi("uuid")).append(" ").append(bestUuidType())
.append(", ").append(qi("voteid")).append(" VARCHAR(36), ").append(qi("playerName"))
.append(" VARCHAR(100), ").append(qi("service")).append(" VARCHAR(100), ").append(qi("time"))
.append(" BIGINT, ").append(qi("realVote")).append(" BOOLEAN, ").append(qi("text"))
.append(" TEXT, ").append(qi("server")).append(" VARCHAR(100)").append(");");
} else {
sb.append("CREATE TABLE IF NOT EXISTS ").append(qi(getTableName())).append(" (").append(qi("id"))
.append(" INT AUTO_INCREMENT PRIMARY KEY,").append(qi("uuid")).append(" VARCHAR(37),")
.append(qi("voteid")).append(" VARCHAR(36),").append(qi("playerName")).append(" VARCHAR(100),")
.append(qi("service")).append(" VARCHAR(100),").append(qi("time")).append(" BIGINT,")
.append(qi("realVote")).append(" TINYINT(1),").append(qi("text")).append(" TEXT,")
.append(qi("server")).append(" VARCHAR(100),").append("INDEX idx_server (").append(qi("server"))
.append("),").append("INDEX idx_uuid (").append(qi("uuid")).append("),").append("INDEX idx_time (")
.append(qi("time")).append(")").append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
}
return sb.toString();
}
/**
* Updates the text field of a vote.
* @param vote the vote to update
* @param server the server name
* @param newText the new text
*/
public void updateVoteText(OfflineBungeeVote vote, String server, String newText) {
if (vote == null || server == null) {
return;
}
String sql = "UPDATE " + qi(getTableName()) + " SET " + qi("text") + " = ?" + " WHERE " + qi("uuid")
+ " = ? AND " + qi("service") + " = ? AND " + qi("time") + " = ? AND " + qi("server") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, newText);
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(2, UUID.fromString(vote.getUuid()));
} else {
ps.setString(2, vote.getUuid());
}
ps.setString(3, vote.getService());
ps.setLong(4, vote.getTime());
ps.setString(5, server);
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
@Override
public abstract void logSevere(String msg);
@Override
public abstract void logInfo(String msg);
@Override
public abstract void debug(Throwable t);
// ---- Constructors ----
/**
* Constructor using an existing MySQL connection.
* @param existingMysql the existing MySQL instance
* @param tablePrefix the table prefix
* @param debug whether debug mode is enabled
*/
public ProxyVoteCacheTable(MySQL existingMysql, String tablePrefix, boolean debug) {
super((tablePrefix != null ? tablePrefix : "") + "votingplugin_votecache", existingMysql, debug);
// best-effort migrations (safe for pool size 1; no nested connections)
alterColumnType("uuid", bestUuidType());
addVoteIdColumnIfMissingOnce();
ensureIndexesOnce();
}
/**
* Constructor using a MySQL configuration.
* @param config the MySQL configuration
* @param debug whether debug mode is enabled
*/
public ProxyVoteCacheTable(MysqlConfig config, boolean debug) {
super("votingplugin_votecache", config, debug);
alterColumnType("uuid", bestUuidType());
addVoteIdColumnIfMissingOnce();
ensureIndexesOnce();
}
// ---- Migrations / indexes ----
private void ensureIndexesOnce() {
if (getDbType() != DbType.POSTGRESQL) {
return; // MySQL indexes are already in CREATE TABLE
}
final String key = getDbType() + ":" + getTableName();
if (!ENSURED_INDEXES.add(key)) {
return;
}
// Use ONE connection for all index statements (no pool churn).
try (Connection conn = mysql.getConnectionManager().getConnection(); Statement st = conn.createStatement()) {
st.executeUpdate(
"CREATE INDEX IF NOT EXISTS idx_server ON " + qi(getTableName()) + " (" + qi("server") + ");");
st.executeUpdate("CREATE INDEX IF NOT EXISTS idx_uuid ON " + qi(getTableName()) + " (" + qi("uuid") + ");");
st.executeUpdate("CREATE INDEX IF NOT EXISTS idx_time ON " + qi(getTableName()) + " (" + qi("time") + ");");
} catch (SQLException e) {
debug(e);
}
}
private void addVoteIdColumnIfMissingOnce() {
final String key = getDbType() + ":" + getTableName() + ":voteid";
if (!MIGRATED_VOTEID.add(key)) {
return;
}
addVoteIdColumnIfMissing();
}
private void addVoteIdColumnIfMissing() {
// Avoid nested connections: do check + alter using the SAME connection.
final boolean pg = getDbType() == DbType.POSTGRESQL;
final String schemaFilter = pg ? "table_schema = current_schema()" : "TABLE_SCHEMA = DATABASE()";
// Case-insensitive match helps with MySQL lower_case_table_names / quoting
// differences.
final String checkSql = "SELECT 1 FROM information_schema.columns WHERE " + schemaFilter
+ " AND LOWER(table_name) = LOWER(?) AND LOWER(column_name) = LOWER(?) LIMIT 1;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(checkSql)) {
ps.setString(1, getTableName());
ps.setString(2, "voteid");
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return; // exists
}
}
// Execute ALTER using same connection to avoid deadlock when pool size is
// small.
final String alter = "ALTER TABLE " + qi(getTableName()) + " ADD COLUMN " + qi("voteid") + " VARCHAR(36);";
try (Statement st = conn.createStatement()) {
st.executeUpdate(alter);
}
} catch (SQLException e) {
// don't hard fail startup
logSevere("Failed to add voteid column to " + getTableName() + ": " + e.getMessage());
debug(e);
}
}
// ---- INSERT ----
/**
* Inserts a new vote into the cache.
* @param voteId the vote ID
* @param uuid the player UUID
* @param playerName the player name
* @param service the voting service
* @param time the vote time
* @param real whether this is a real vote
* @param text the vote text/payload
* @param server the server name
*/
public void insertVote(UUID voteId, String uuid, String playerName, String service, long time, boolean real,
String text, String server) {
String sql = "INSERT INTO " + qi(getTableName()) + " (" + qi("uuid") + ", " + qi("voteid") + ", "
+ qi("playerName") + ", " + qi("service") + ", " + qi("time") + ", " + qi("realVote") + ", "
+ qi("text") + ", " + qi("server") + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(1, UUID.fromString(uuid));
} else {
ps.setString(1, uuid);
}
ps.setString(2, voteId != null ? voteId.toString() : null);
ps.setString(3, playerName);
ps.setString(4, service);
ps.setLong(5, time);
if (getDbType() == DbType.POSTGRESQL) {
ps.setBoolean(6, real);
} else {
ps.setInt(6, real ? 1 : 0);
}
ps.setString(7, text);
ps.setString(8, server);
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
// ---- GET ----
/**
* Gets all votes from the table.
* @return list of all vote rows
*/
public List<VoteRow> getAllVotes() {
return selectVotes("SELECT * FROM " + qi(getTableName()) + ";", null);
}
/**
* Gets all votes for a specific server.
* @param server the server name
* @return list of vote rows
*/
public List<VoteRow> getVotesForServer(String server) {
return selectVotes("SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("server") + " = ?;",
new Object[] { server });
}
/**
* Gets a vote by its ID.
* @param id the vote ID
* @return the vote row or null if not found
*/
public VoteRow getVoteById(int id) {
List<VoteRow> list = selectVotes("SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("id") + " = ?;",
new Object[] { id });
return list.isEmpty() ? null : list.get(0);
}
/**
* Gets all votes for a specific UUID.
* @param uuid the player UUID
* @return list of vote rows
*/
public List<VoteRow> getVotesByUUID(String uuid) {
String sql = "SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("uuid") + " = ?;";
Object[] params = (getDbType() == DbType.POSTGRESQL) ? new Object[] { UUID.fromString(uuid) }
: new Object[] { uuid };
return selectVotes(sql, params);
}
/**
* Gets all votes for a specific player name.
* @param playerName the player name
* @return list of vote rows
*/
public List<VoteRow> getVotesByPlayerName(String playerName) {
return selectVotes("SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("playerName") + " = ?;",
new Object[] { playerName });
}
/**
* Gets all distinct servers that have votes.
* @return set of server names
*/
public Set<String> getServers() {
Set<String> servers = new HashSet<>();
String sql = "SELECT DISTINCT " + qi("server") + " FROM " + qi(getTableName()) + " WHERE " + qi("server")
+ " IS NOT NULL;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String s = rs.getString(1);
if (s != null) {
servers.add(s);
}
}
} catch (SQLException e) {
debug(e);
}
return servers;
}
// ---- DELETE ----
/**
* Removes a vote by its ID.
* @param id the vote ID
*/
public void removeVoteById(int id) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("id") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
ps.executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Removes all votes for a specific server and UUID.
* @param server the server name
* @param uuid the player UUID
*/
public void removeVotesByServerAndUUID(String server, String uuid) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("server") + " = ? AND " + qi("uuid")
+ " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, server);
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(2, UUID.fromString(uuid));
} else {
ps.setString(2, uuid);
}
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
/**
* Removes a specific vote.
* @param vote the vote to remove
* @param server the server name
*/
public void removeVote(OfflineBungeeVote vote, String server) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("uuid") + " = ? AND " + qi("service")
+ " = ? AND " + qi("time") + " = ? AND " + qi("server") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (getDbType() == DbType.POSTGRESQL) {
ps.setObject(1, UUID.fromString(vote.getUuid()));
} else {
ps.setString(1, vote.getUuid());
}
ps.setString(2, vote.getService());
ps.setLong(3, vote.getTime());
ps.setString(4, server);
ps.executeUpdate();
} catch (SQLException | IllegalArgumentException e) {
debug(e);
}
}
/**
* Removes all votes for a specific server.
* @param server the server name
*/
public void removeVotesByServer(String server) {
String sql = "DELETE FROM " + qi(getTableName()) + " WHERE " + qi("server") + " = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, server);
ps.executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Clears all votes from the table.
*/
public void clearTable() {
try {
if (getDbType() == DbType.POSTGRESQL) {
new Query(mysql, "TRUNCATE TABLE " + qi(getTableName()) + " RESTART IDENTITY;").executeUpdate();
} else {
new Query(mysql, "TRUNCATE TABLE " + qi(getTableName()) + ";").executeUpdate();
}
} catch (SQLException e) {
debug(e);
}
}
// ---- Internals ----
private List<VoteRow> selectVotes(String sql, Object[] params) {
List<VoteRow> list = new ArrayList<>();
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
if (params != null) {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
}
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
VoteRow v = new VoteRow(rs.getInt("id"), rs.getString("voteid"), rs.getString("uuid"),
rs.getString("playerName"), rs.getString("service"), rs.getLong("time"),
(getDbType() == DbType.POSTGRESQL ? rs.getBoolean("realVote") : rs.getInt("realVote") == 1),
rs.getString("text"), rs.getString("server"));
list.add(v);
}
}
} catch (SQLException e) {
debug(e);
}
return list;
}
/**
* Represents a row in the vote cache table.
*/
public static class VoteRow {
private final int id;
private final String voteId;
private final String uuid;
private final String playerName;
private final String service;
private final long time;
private final boolean realVote;
private final String text;
private final String server;
/**
* Constructor for VoteRow.
* @param id the row ID
* @param voteId the vote ID
* @param uuid the player UUID
* @param playerName the player name
* @param service the voting service
* @param time the vote time
* @param realVote whether this is a real vote
* @param text the vote text
* @param server the server name
*/
public VoteRow(int id, String voteId, String uuid, String playerName, String service, long time,
boolean realVote, String text, String server) {
this.id = id;
this.voteId = voteId;
this.uuid = uuid;
this.playerName = playerName;
this.service = service;
this.time = time;
this.realVote = realVote;
this.text = text;
this.server = server;
}
/**
* Gets the row ID.
* @return the ID
*/
public int getId() {
return id;
}
/**
* Gets the vote ID.
* @return the vote ID
*/
public String getVoteId() {
return voteId;
}
/**
* Gets the player UUID.
* @return the UUID
*/
public String getUuid() {
return uuid;
}
/**
* Gets the player name.
* @return the player name
*/
public String getPlayerName() {
return playerName;
}
/**
* Gets the voting service.
* @return the service name
*/
public String getService() {
return service;
}
/**
* Gets the vote time.
* @return the time in milliseconds
*/
public long getTime() {
return time;
}
/**
* Checks if this is a real vote.
* @return true if real vote
*/
public boolean isRealVote() {
return realVote;
}
/**
* Gets the vote text.
* @return the text/payload
*/
public String getText() {
return text;
}
/**
* Gets the server name.
* @return the server name
*/
public String getServer() {
return server;
}
}
}
@@ -1,92 +0,0 @@
package com.bencodez.votingplugin.proxy.cache.nonvoted;
import java.util.function.Consumer;
/**
* Interface for storing non-voted players data.
*/
public interface INonVotedPlayersStorage {
/**
* Insert or update a player entry.
*
* @param uuid the player UUID
* @param playerName the player name
* @param lastTime the last recorded time
*/
void upsertPlayer(String uuid, String playerName, long lastTime);
/**
* Returns UUID for the player or empty string if not found.
*
* @param playerName the player name
* @return the UUID or empty string
*/
String getUuidByPlayerName(String playerName);
/**
* Remove a player from storage.
*
* @param playerName the player name
*/
void removeByPlayerName(String playerName);
/**
* Iterate snapshot of all entries.
*
* @param consumer the consumer to process each entry
*/
void forEach(Consumer<NonVotedPlayerEntry> consumer);
/** Close resources if needed (MySQL); no-op for JSON. */
void close();
/**
* Entry for a non-voted player.
*/
final class NonVotedPlayerEntry {
private final String playerName;
private final String uuid;
private final long lastTime;
/**
* Constructs a new non-voted player entry.
*
* @param playerName the player name
* @param uuid the UUID
* @param lastTime the last time
*/
public NonVotedPlayerEntry(String playerName, String uuid, long lastTime) {
this.playerName = playerName;
this.uuid = uuid;
this.lastTime = lastTime;
}
/**
* Gets the player name.
*
* @return the player name
*/
public String getPlayerName() {
return playerName;
}
/**
* Gets the UUID.
*
* @return the UUID
*/
public String getUuid() {
return uuid;
}
/**
* Gets the last time.
*
* @return the last time
*/
public long getLastTime() {
return lastTime;
}
}
}
@@ -1,70 +0,0 @@
package com.bencodez.votingplugin.proxy.cache.nonvoted;
import java.util.List;
import java.util.function.Consumer;
import com.bencodez.simpleapi.sql.mysql.MySQL;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
/**
* MySQL storage for non-voted players.
*/
public abstract class MySQLNonVotedPlayersStorage extends ProxyNonVotedPlayersTable
implements INonVotedPlayersStorage {
private final boolean ownsConnection;
/**
* Constructor with existing MySQL connection.
* @param existingMysql existing MySQL connection
* @param tablePrefix table prefix
* @param debug enable debug mode
*/
public MySQLNonVotedPlayersStorage(MySQL existingMysql, String tablePrefix, boolean debug) {
super(existingMysql, tablePrefix, debug);
this.ownsConnection = false;
}
/**
* Constructor with new MySQL configuration.
* @param config MySQL configuration
* @param debug enable debug mode
*/
public MySQLNonVotedPlayersStorage(MysqlConfig config, boolean debug) {
super(config, debug);
this.ownsConnection = true;
}
@Override
public void upsertPlayer(String uuid, String playerName, long lastTime) {
super.upsertPlayer(uuid, playerName, lastTime);
}
@Override
public String getUuidByPlayerName(String playerName) {
return super.getUuidByPlayerName(playerName);
}
@Override
public void removeByPlayerName(String playerName) {
super.removeByPlayerName(playerName);
}
@Override
public void forEach(Consumer<NonVotedPlayerEntry> consumer) {
List<NonVotedPlayerRow> rows = getAllRows();
for (NonVotedPlayerRow row : rows) {
consumer.accept(new NonVotedPlayerEntry(
row.getPlayerName(),
row.getUuid(),
row.getLastTime()));
}
}
@Override
public void close() {
if (ownsConnection) {
getMysql().disconnect();
}
}
}
@@ -1,176 +0,0 @@
package com.bencodez.votingplugin.proxy.cache.nonvoted;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.bencodez.simpleapi.sql.mysql.MySQL;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
/**
* Cache for players who haven't voted yet.
*/
public abstract class NonVotedPlayersCache {
private INonVotedPlayersStorage storage;
// 5 days in millis (same as your current logic)
private static final long MAX_AGE_MILLIS = TimeUnit.DAYS.toMillis(5);
/**
* Constructor for NonVotedPlayersCache.
* @param mysqlConfig MySQL configuration
* @param useMysql whether to use MySQL
* @param useExistingConnection whether to use existing connection
* @param mysql existing MySQL connection
* @param storage storage implementation
* @param debug enable debug mode
*/
public NonVotedPlayersCache(MysqlConfig mysqlConfig, boolean useMysql, boolean useExistingConnection, MySQL mysql,
INonVotedPlayersStorage storage, boolean debug) {
if (useMysql) {
if (useExistingConnection) {
this.storage = new MySQLNonVotedPlayersStorage(mysql, mysqlConfig.getTablePrefix(), debug) {
@Override
public void logSevere(String string) {
logSevere1(string);
}
@Override
public void logInfo(String string) {
logInfo1(string);
}
@Override
public void debug(Exception e) {
if (debug)
debug1(e);
}
};
} else {
this.storage = new MySQLNonVotedPlayersStorage(mysqlConfig, debug) {
@Override
public void logSevere(String string) {
logSevere1(string);
}
@Override
public void logInfo(String string) {
logInfo1(string);
}
@Override
public void debug(Exception e) {
if (debug)
debug1(e);
}
};
}
} else {
this.storage = storage;
}
}
/**
* Check if user exists.
* @param uuid the player UUID
* @return true if user exists
*/
public abstract boolean userExists(String uuid);
/**
* Add a player directly by uuid + name, only if they don't already have votes.
* @param uuid the player UUID
* @param playerName the player name
*/
public void addPlayer(String uuid, String playerName) {
if (uuid == null || uuid.isEmpty()) {
return;
}
if (!userExists(uuid)) {
debug1("Adding nonvotedplayer: " + playerName + " / " + uuid);
storage.upsertPlayer(uuid, playerName, System.currentTimeMillis());
}
}
/**
* Get all UUIDs in cache.
* @return set of UUIDs
*/
public abstract Set<String> getAllUUIDs();
/**
* Cleanup: - remove entries older than 5 days - remove entries whose UUID has
* now appeared in proxy MySQL - remove entries with empty/invalid UUID
*/
public void check() {
final long now = System.currentTimeMillis();
Set<String> uuids = getAllUUIDs();
storage.forEach(entry -> {
boolean remove = false;
long time = entry.getLastTime();
// too old?
if ((now - time) > MAX_AGE_MILLIS) {
remove = true;
} else {
String uuid = entry.getUuid();
if (uuid == null || uuid.isEmpty()) {
remove = true;
} else if (uuids.contains(uuid)) {
// player has now voted in the main proxy MySQL
remove = true;
}
}
if (remove) {
debug1("Removing nonvotedplayer: " + entry.getPlayerName());
storage.removeByPlayerName(entry.getPlayerName());
}
});
}
/**
* Returns the UUID for a cached non-voted player, or empty string if not
* present.
* @param playerName the player name
* @return the UUID or empty string
*/
public String getUUID(String playerName) {
return storage.getUuidByPlayerName(playerName);
}
/**
* Close storage connections.
*/
public void close() {
storage.close();
}
/**
* Log info message.
* @param msg the message
*/
public abstract void logInfo1(String msg);
/**
* Log severe message.
* @param msg the message
*/
public abstract void logSevere1(String msg);
/**
* Debug log exception.
* @param e the exception
*/
public abstract void debug1(Exception e);
/**
* Debug log message.
* @param msg the message
*/
public abstract void debug1(String msg);
}
@@ -1,262 +0,0 @@
package com.bencodez.votingplugin.proxy.cache.nonvoted;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.bencodez.simpleapi.sql.mysql.MySQL;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
import com.bencodez.simpleapi.sql.mysql.queries.Query;
import lombok.Getter;
/**
* Table for tracking non-voted players in the proxy cache.
*/
public abstract class ProxyNonVotedPlayersTable {
@Getter
private final MySQL mysql;
private final String tableName;
/**
* Logs a severe message.
* @param msg the message to log
*/
public abstract void logSevere(String msg);
/**
* Logs an info message.
* @param msg the message to log
*/
public abstract void logInfo(String msg);
/**
* Logs debug information for an exception.
* @param e the exception to debug
*/
public abstract void debug(Exception e);
/**
* Constructor using an existing MySQL connection.
* @param existingMysql the existing MySQL instance
* @param tablePrefix the table prefix
* @param debug whether debug mode is enabled
*/
public ProxyNonVotedPlayersTable(MySQL existingMysql, String tablePrefix, boolean debug) {
this.mysql = existingMysql;
this.tableName = (tablePrefix != null ? tablePrefix : "") + "votingplugin_nonvotedplayers";
createTableIfNeeded();
}
/**
* Constructor using a MySQL configuration.
* @param config the MySQL configuration
* @param debug whether debug mode is enabled
*/
public ProxyNonVotedPlayersTable(MysqlConfig config, boolean debug) {
String prefix = config.getTablePrefix() != null ? config.getTablePrefix() : "";
this.tableName = prefix + "votingplugin_nonvotedplayers";
this.mysql = new MySQL(config.getMaxThreads()) {
@Override
public void debug(SQLException e) {
if (debug) {
ProxyNonVotedPlayersTable.this.debug(e);
}
}
@Override
public void severe(String msg) {
logSevere(msg);
}
@Override
public void debug(String msg) {
if (debug) {
logInfo("MYSQL DEBUG: " + msg);
}
}
};
if (!mysql.connect(config)) {
logSevere("Failed to connect to MySQL for non-voted players cache!");
}
try {
new Query(mysql, "USE `" + config.getDatabase() + "`;").executeUpdate();
} catch (SQLException e) {
logSevere("Failed to select database: " + config.getDatabase());
debug(e);
}
createTableIfNeeded();
}
private void createTableIfNeeded() {
String sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` (" + "id INT AUTO_INCREMENT PRIMARY KEY,"
+ "uuid VARCHAR(36) NOT NULL," + "playerName VARCHAR(100) NOT NULL," + "`lastTime` BIGINT NOT NULL,"
+ "UNIQUE KEY uniq_playerName (`playerName`)," + "KEY idx_uuid (`uuid`),"
+ "KEY idx_lastTime (`lastTime`)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
try {
new Query(mysql, sql).executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Gets the table name.
* @return the table name
*/
public String getTableName() {
return tableName;
}
/**
* Upsert player record.
* @param uuid the player UUID
* @param playerName the player name
* @param lastTime the last time
*/
public void upsertPlayer(String uuid, String playerName, long lastTime) {
String sql = "INSERT INTO `" + tableName + "` (uuid, playerName, lastTime) " + "VALUES (?, ?, ?) "
+ "ON DUPLICATE KEY UPDATE uuid = VALUES(uuid), lastTime = VALUES(lastTime);";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, uuid);
ps.setString(2, playerName);
ps.setLong(3, lastTime);
ps.executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Get UUID by player name.
* @param playerName the player name
* @return the UUID or empty string
*/
public String getUuidByPlayerName(String playerName) {
String sql = "SELECT uuid FROM `" + tableName + "` WHERE playerName = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, playerName);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
String uuid = rs.getString("uuid");
return uuid != null ? uuid : "";
}
}
} catch (SQLException e) {
debug(e);
}
return "";
}
/**
* Remove player by name.
* @param playerName the player name
*/
public void removeByPlayerName(String playerName) {
String sql = "DELETE FROM `" + tableName + "` WHERE playerName = ?;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, playerName);
ps.executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Gets all rows from the table.
* @return list of all non-voted player rows
*/
public List<NonVotedPlayerRow> getAllRows() {
List<NonVotedPlayerRow> list = new ArrayList<>();
String sql = "SELECT id, uuid, playerName, lastTime FROM `" + tableName + "`;";
try (Connection conn = mysql.getConnectionManager().getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(new NonVotedPlayerRow(rs.getInt("id"), rs.getString("uuid"), rs.getString("playerName"),
rs.getLong("lastTime")));
}
} catch (SQLException e) {
debug(e);
}
return list;
}
/**
* Clears all rows from the table.
*/
public void clearAll() {
try {
new Query(mysql, "TRUNCATE TABLE `" + tableName + "`;").executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
/**
* Represents a row in the non-voted players table.
*/
public static class NonVotedPlayerRow {
private final int id;
private final String uuid;
private final String playerName;
private final long lastTime;
/**
* Constructor for NonVotedPlayerRow.
* @param id the row ID
* @param uuid the player UUID
* @param playerName the player name
* @param lastTime the last time
*/
public NonVotedPlayerRow(int id, String uuid, String playerName, long lastTime) {
this.id = id;
this.uuid = uuid;
this.playerName = playerName;
this.lastTime = lastTime;
}
/**
* Gets the row ID.
* @return the ID
*/
public int getId() {
return id;
}
/**
* Gets the player UUID.
* @return the UUID
*/
public String getUuid() {
return uuid;
}
/**
* Gets the player name.
* @return the player name
*/
public String getPlayerName() {
return playerName;
}
/**
* Gets the last time.
* @return the last time
*/
public long getLastTime() {
return lastTime;
}
}
}
@@ -1,436 +0,0 @@
// File: com/bencodez/votingplugin/proxy/multiproxy/MultiProxyHandler.java
package com.bencodez.votingplugin.proxy.multiproxy;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.bencodez.simpleapi.encryption.EncryptionHandler;
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
import com.bencodez.simpleapi.servercomm.redis.RedisHandler;
import com.bencodez.simpleapi.servercomm.redis.RedisListener;
import com.bencodez.simpleapi.servercomm.sockets.ClientHandler;
import com.bencodez.simpleapi.servercomm.sockets.SocketHandler;
import com.bencodez.simpleapi.servercomm.sockets.SocketReceiver;
import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
import lombok.Getter;
/**
* Abstract handler for multi-proxy support.
*/
public abstract class MultiProxyHandler {
private HashMap<String, ClientHandler> multiproxyClientHandles;
private SocketHandler multiproxySocketHandler;
@Getter
private RedisHandler multiProxyRedis;
/**
* Constructs a new multi-proxy handler.
*/
public MultiProxyHandler() {
}
/**
* Adds a non-voted player to the cache.
*
* @param uuid the player UUID
* @param playerName the player name
*/
public abstract void addNonVotedPlayerCache(String uuid, String playerName);
/**
* Clears a vote for a player.
*
* @param uuid the player UUID
*/
public abstract void clearVote(String uuid);
/**
* Closes the multi-proxy handler.
*/
public void close() {
if (multiproxySocketHandler != null) {
multiproxySocketHandler.closeConnection();
multiproxySocketHandler = null;
}
if (multiProxyRedis != null && !getMultiProxyRedisUseExistingConnection()) {
multiProxyRedis.close();
}
}
/**
* Gets whether debug mode is enabled.
*
* @return true if debug mode is enabled
*/
public abstract boolean getDebug();
/**
* Gets the encryption handler.
*
* @return the encryption handler
*/
public abstract EncryptionHandler getEncryptionHandler();
/**
* Gets the multi-proxy method.
*
* @return the multi-proxy method
*/
public abstract MultiProxyMethod getMultiProxyMethod();
/**
* Gets the multi-proxy password.
*
* @return the password
*/
public abstract String getMultiProxyPassword();
/**
* Gets the multi-proxy Redis host.
*
* @return the Redis host
*/
public abstract String getMultiProxyRedisHost();
/**
* Gets the multi-proxy Redis port.
*
* @return the Redis port
*/
public abstract int getMultiProxyRedisPort();
/**
* Gets the multi-proxy Redis database index.
*
* @return the database index
*/
public abstract int getMultiProxyRedisDbIndex();
/**
* Gets whether to use an existing Redis connection.
*
* @return true if using existing connection
*/
public abstract boolean getMultiProxyRedisUseExistingConnection();
/**
* Gets the multi-proxy server name.
*
* @return the server name
*/
public abstract String getMultiProxyServerName();
/**
* Gets the multi-proxy servers.
*
* @return the servers
*/
public abstract Collection<String> getMultiProxyServers();
/**
* Gets the configuration for a multi-proxy server.
*
* @param s the server name
* @return the server configuration
*/
public abstract MultiProxyServerSocketConfiguration getMultiProxyServersConfiguration(String s);
/**
* Gets the multi-proxy socket host.
*
* @return the socket host
*/
public abstract String getMultiProxySocketHostHost();
/**
* Gets the multi-proxy socket host port.
*
* @return the socket host port
*/
public abstract int getMultiProxySocketHostPort();
/**
* Gets whether multi-proxy support is enabled.
*
* @return true if multi-proxy support is enabled
*/
public abstract boolean getMultiProxySupportEnabled();
/**
* Gets the multi-proxy username.
*
* @return the username
*/
public abstract String getMultiProxyUsername();
/**
* Gets the plugin data folder.
*
* @return the plugin data folder
*/
public abstract File getPluginDataFolder();
/**
* Gets whether this is the primary server.
*
* @return true if this is the primary server
*/
public abstract boolean getPrimaryServer();
/**
* Gets the proxy servers.
*
* @return the proxy servers
*/
public abstract List<String> getProxyServers();
/**
* Gets the Redis handler.
*
* @return the Redis handler
*/
public abstract RedisHandler getRedisHandler();
/**
* Gets the version.
*
* @return the version
*/
public abstract String getVersion();
/**
* Logs an info message.
*
* @param msg the message to log
*/
public abstract void logInfo(String msg);
/**
* Runs a task asynchronously.
*
* @param runnable the task to run
*/
public abstract void runAsnc(Runnable runnable);
/**
* Sets the encryption handler.
*
* @param encryptionHandler the encryption handler
*/
public abstract void setEncryptionHandler(EncryptionHandler encryptionHandler);
/**
* Triggers a vote.
*
* @param player the player name
* @param service the service name
* @param realVote whether this is a real vote
* @param timeQueue whether to queue by time
* @param queueTime the queue time
* @param text the vote totals snapshot
* @param uuid the player UUID
*/
public abstract void triggerVote(String player, String service, boolean realVote, boolean timeQueue, long queueTime,
VoteTotalsSnapshot text, String uuid);
/**
* Loads multi-proxy support.
*/
public void loadMultiProxySupport() {
if (!getMultiProxySupportEnabled()) {
return;
}
if (getMultiProxyMethod().equals(MultiProxyMethod.SOCKETS)) {
if (getEncryptionHandler() == null) {
setEncryptionHandler(
new EncryptionHandler("VotingPlugin", new File(getPluginDataFolder(), "secretkey.key")));
}
if (multiproxySocketHandler != null) {
multiproxySocketHandler.closeConnection();
multiproxySocketHandler = null;
}
multiproxySocketHandler = new SocketHandler(getVersion(), getMultiProxySocketHostHost(),
getMultiProxySocketHostPort(), getEncryptionHandler(), getDebug()) {
@Override
public void log(String str) {
logInfo(str);
}
};
multiproxySocketHandler.add(new SocketReceiver() {
@Override
public void onReceiveEnvelope(JsonEnvelope envelope) {
handleEnvelope(envelope);
}
});
multiproxyClientHandles = new HashMap<>();
for (String s : getMultiProxyServers()) {
MultiProxyServerSocketConfiguration d = getMultiProxyServersConfiguration(s);
multiproxyClientHandles.put(s,
new ClientHandler(d.getHost(), d.getPort(), getEncryptionHandler(), getDebug()));
}
} else {
if (getMultiProxyRedisUseExistingConnection() && getRedisHandler() != null) {
multiProxyRedis = getRedisHandler();
} else {
multiProxyRedis = new RedisHandler(getMultiProxyRedisHost(), getMultiProxyRedisPort(),
getMultiProxyUsername(), getMultiProxyPassword(), getMultiProxyRedisDbIndex()) {
@Override
public void debug(String message) {
if (getDebug()) {
logInfo("MultiProxyRedis: " + message);
}
}
};
}
runAsnc(() -> {
RedisListener listener = multiProxyRedis.createEnvelopeListener(
"VotingPluginProxy_" + getMultiProxyServerName(), (ch, env) -> handleEnvelope(env));
multiProxyRedis.loadListener(listener);
});
}
logInfo("Loaded multi-proxy support: " + getMultiProxyMethod().toString());
}
/**
* Handles player login.
*
* @param uuid the player UUID
* @param playerName the player name
*/
public void login(String uuid, String playerName) {
if (!getMultiProxySupportEnabled() || getPrimaryServer()) {
return;
}
sendMultiProxyEnvelope(VotingPluginWire.login(playerName, uuid, getMultiProxyServerName()));
}
/**
* Sends a clear vote message.
*
* @param uuid the player UUID
* @param playerName the player name
*/
public void sendClearVote(String uuid, String playerName) {
if (!getMultiProxySupportEnabled()) {
return;
}
sendMultiProxyEnvelope(VotingPluginWire.clearVote(uuid, playerName, getMultiProxyServerName()));
}
/**
* Sends a status message.
*/
public void sendStatus() {
if (!getMultiProxySupportEnabled()) {
return;
}
sendMultiProxyEnvelope(VotingPluginWire.status(getMultiProxyServerName()));
}
/**
* Sends a multi-proxy envelope.
*
* @param envelope the envelope to send
*/
public void sendMultiProxyEnvelope(JsonEnvelope envelope) {
if (envelope == null) {
return;
}
if (getMultiProxyMethod().equals(MultiProxyMethod.SOCKETS)) {
if (multiproxyClientHandles == null) {
return;
}
for (ClientHandler h : multiproxyClientHandles.values()) {
h.sendEnvelope(envelope);
}
} else if (getMultiProxyMethod().equals(MultiProxyMethod.REDIS)) {
if (multiProxyRedis == null) {
return;
}
for (String server : getProxyServers()) {
multiProxyRedis.publishEnvelope("VotingPluginProxy_" + server, envelope);
}
}
}
private void handleEnvelope(JsonEnvelope envelope) {
if (envelope == null) {
return;
}
final String sub = envelope.getSubChannel() == null ? "" : envelope.getSubChannel();
Map<String, String> f = envelope.getFields();
if (f == null) {
f = new HashMap<>();
}
if (sub.equalsIgnoreCase(VotingPluginWire.SUB_STATUS)) {
logInfo("Multi-proxy status message received");
return;
}
if (sub.equalsIgnoreCase(VotingPluginWire.SUB_CLEAR_VOTE)) {
final String uuid = f.getOrDefault(VotingPluginWire.K_UUID, "");
final String player = f.getOrDefault(VotingPluginWire.K_PLAYER, "");
final String server = f.getOrDefault(VotingPluginWire.K_SERVER, "");
if (!uuid.isEmpty()) {
clearVote(uuid);
if (getPrimaryServer()) {
sendMultiProxyEnvelope(VotingPluginWire.clearVotePrimary(uuid, player, server));
}
}
return;
}
if (sub.equalsIgnoreCase(VotingPluginWire.SUB_CLEAR_VOTE_PRIMARY)) {
final String uuid = f.getOrDefault(VotingPluginWire.K_UUID, "");
if (!uuid.isEmpty()) {
clearVote(uuid);
}
return;
}
if (sub.equalsIgnoreCase(VotingPluginWire.SUB_LOGIN)) {
final String player = f.getOrDefault(VotingPluginWire.K_PLAYER, "");
final String uuid = f.getOrDefault(VotingPluginWire.K_UUID, "");
if (!player.isEmpty() && !uuid.isEmpty()) {
addNonVotedPlayerCache(uuid, player);
}
return;
}
if (sub.equalsIgnoreCase(VotingPluginWire.SUB_VOTE) || sub.equalsIgnoreCase(VotingPluginWire.SUB_VOTE_ONLINE)) {
final String player = f.getOrDefault(VotingPluginWire.K_PLAYER, "");
final String uuid = f.getOrDefault(VotingPluginWire.K_UUID, "");
final String service = f.getOrDefault(VotingPluginWire.K_SERVICE, "");
final String totals = f.getOrDefault(VotingPluginWire.K_TOTALS, "");
final boolean realVote = Boolean.parseBoolean(f.getOrDefault(VotingPluginWire.K_REAL_VOTE, "false"));
if (!player.isEmpty() && !uuid.isEmpty() && !service.isEmpty()) {
triggerVote(player, service, realVote, true, 0L, VoteTotalsSnapshot.parseStorage(totals), uuid);
}
return;
}
if (getDebug()) {
logInfo("MultiProxy ignored subchannel: " + sub + " fields=" + f);
}
}
}
@@ -1,27 +0,0 @@
package com.bencodez.votingplugin.proxy.multiproxy;
/**
* Configuration for a multi-proxy server socket.
*/
public interface MultiProxyServerSocketConfiguration {
/**
* Gets the host.
*
* @return the host
*/
String getHost();
/**
* Gets the port.
*
* @return the port
*/
int getPort();
/**
* Gets the server name.
*
* @return the server name
*/
String getServerName();
}
@@ -1,68 +0,0 @@
package com.bencodez.votingplugin.proxy.velocity;
import java.io.File;
import java.util.function.Consumer;
import com.bencodez.simpleapi.file.velocity.VelocityJSONFile;
import com.bencodez.votingplugin.proxy.cache.nonvoted.INonVotedPlayersStorage;
/**
* JSON-based non-voted players cache for Velocity proxy.
*/
public class VelocityJsonNonVotedPlayersCache extends VelocityJSONFile implements INonVotedPlayersStorage {
/**
* Constructs a new Velocity JSON non-voted players cache.
* @param file the file to store cache data
*/
public VelocityJsonNonVotedPlayersCache(File file) {
super(file);
}
@Override
public void upsertPlayer(String uuid, String playerName, long lastTime) {
setPath(uuid, "NonVotedPlayers", playerName, "UUID");
setPath(lastTime, "NonVotedPlayers", playerName, "LastTime");
save();
}
@Override
public String getUuidByPlayerName(String playerName) {
if (!getNode("NonVotedPlayers", playerName).empty()) {
return getNode("NonVotedPlayers", playerName, "UUID").getString("");
}
return "";
}
@Override
public void removeByPlayerName(String playerName) {
remove("NonVotedPlayers", playerName);
save();
}
@Override
public void forEach(Consumer<NonVotedPlayerEntry> consumer) {
// If "NonVotedPlayers" doesn't exist, getNode(...) will just be empty and
// getKeys(...) will be an empty set.
for (String player : getKeys(getNode("NonVotedPlayers"))) {
long time = getNode("NonVotedPlayers", player, "LastTime").getLong(0);
String uuid = getNode("NonVotedPlayers", player, "UUID").getString("");
consumer.accept(new NonVotedPlayerEntry(player, uuid, time));
}
}
@Override
public void close() {
}
/**
* Sets a value at a specific path.
* @param value the value to set
* @param path the path elements
*/
private void setPath(Object value, Object... path) {
set(path, value);
}
}
@@ -1,238 +0,0 @@
package com.bencodez.votingplugin.proxy.velocity;
import java.io.File;
import java.util.Collection;
import com.bencodez.simpleapi.file.velocity.VelocityJSONFile;
import com.bencodez.votingplugin.proxy.OfflineBungeeVote;
import com.bencodez.votingplugin.proxy.cache.ConfigDataNode;
import com.bencodez.votingplugin.proxy.cache.DataNode;
import com.bencodez.votingplugin.proxy.cache.IVoteCache;
import com.bencodez.votingplugin.timequeue.VoteTimeQueue;
/**
* JSON-based vote cache implementation for Velocity proxy.
*/
public class VelocityJsonVoteCache extends VelocityJSONFile implements IVoteCache {
/**
* Constructs a new Velocity JSON vote cache.
* @param file the file to store cache data
*/
public VelocityJsonVoteCache(File file) {
super(file);
}
@Override
public void addTimedVote(int num, VoteTimeQueue voteTimedQueue) {
setPath(voteTimedQueue.getName(), "TimedVoteCache", String.valueOf(num), "Name");
setPath(voteTimedQueue.getService(), "TimedVoteCache", String.valueOf(num), "Service");
setPath(voteTimedQueue.getTime(), "TimedVoteCache", String.valueOf(num), "Time");
}
@Override
public void addVote(String server, int num, OfflineBungeeVote voteData) {
setPath(voteData.getPlayerName(), "VoteCache", server, String.valueOf(num), "Name");
setPath(voteData.getService(), "VoteCache", server, String.valueOf(num), "Service");
setPath(voteData.getUuid(), "VoteCache", server, String.valueOf(num), "UUID");
setPath(voteData.getTime(), "VoteCache", server, String.valueOf(num), "Time");
setPath(voteData.isRealVote(), "VoteCache", server, String.valueOf(num), "Real");
setPath(voteData.getText(), "VoteCache", server, String.valueOf(num), "Text");
setPath(voteData.getVoteId() != null ? voteData.getVoteId().toString() : null, "VoteCache", server, String.valueOf(num), "VoteID");
}
@Override
public void addVoteOnline(String player, int num, OfflineBungeeVote voteData) {
setPath(voteData.getPlayerName(), "OnlineCache", player, String.valueOf(num), "Name");
setPath(voteData.getService(), "OnlineCache", player, String.valueOf(num), "Service");
setPath(voteData.getUuid(), "OnlineCache", player, String.valueOf(num), "UUID");
setPath(voteData.getTime(), "OnlineCache", player, String.valueOf(num), "Time");
setPath(voteData.isRealVote(), "OnlineCache", player, String.valueOf(num), "Real");
setPath(voteData.getText(), "OnlineCache", player, String.valueOf(num), "Text");
setPath(voteData.getVoteId() != null ? voteData.getVoteId().toString() : null, "OnlineCache", player, String.valueOf(num), "VoteID");
}
@Override
public void clearData() {
remove("VoteCache");
remove("OnlineCache");
remove("TimedVoteCache");
save();
}
@Override
public Collection<String> getOnlineVotes(String name) {
return getKeys(getNode("OnlineCache", name));
}
@Override
public ConfigDataNode getOnlineVotes(String name, String num) {
return new ConfigDataNode(getNode("OnlineCache", name, num));
}
@Override
public Collection<String> getPlayers() {
return getKeys(getNode("OnlineCache"));
}
@Override
public Collection<String> getServers() {
return getKeys(getNode("VoteCache"));
}
@Override
public Collection<String> getServerVotes(String server) {
return getKeys(getNode("VoteCache", server));
}
@Override
public ConfigDataNode getServerVotes(String server, String num) {
return new ConfigDataNode(getNode("VoteCache", server, num));
}
@Override
public Collection<String> getTimedVoteCache() {
return getKeys(getNode("TimedVoteCache"));
}
@Override
public ConfigDataNode getTimedVoteCache(String key) {
return new ConfigDataNode(getNode("TimedVoteCache", key));
}
@Override
public int getVotePartyCache(String server) {
return getNode("VoteParty", "Cache", server).getInt(0);
}
@Override
public int getVotePartyCurrentVotes() {
return getInt(getNode("VoteParty", "CurrentVotes"), 0);
}
@Override
public int getVotePartyInreaseVotesRequired() {
return getInt(getNode("VoteParty", "IncreaseVotes"), 0);
}
@Override
public void setVotePartyCache(String server, int amount) {
setPath(amount, "VoteParty", "Cache", server);
}
@Override
public void setVotePartyCurrentVotes(int amount) {
setPath(amount, "VoteParty", "CurrentVotes");
}
@Override
public void setVotePartyInreaseVotesRequired(int amount) {
setPath(amount, "VoteParty", "IncreaseVotes");
}
@Override
public void removeOnlineVotes(String player) {
remove("OnlineCache", player);
}
@Override
public void removeServerVotes(String server) {
remove("VoteCache", server);
}
@Override
public void removeServerVote(String server, String uuid) {
Collection<String> votes = getServerVotes(server);
if (votes == null) {
return;
}
// search for vote with uuid and remove it
for (String num : votes) {
ConfigDataNode node = getServerVotes(server, num);
if (node == null) {
continue;
}
DataNode uuidNode = node.get("UUID");
if (uuidNode == null) {
continue;
}
String nodeUuid = uuidNode.asString();
if (nodeUuid != null && nodeUuid.equals(uuid)) {
remove("VoteCache", server, num);
}
}
}
@Override
public void removeVote(String server, OfflineBungeeVote vote) {
Collection<String> votes = getServerVotes(server);
if (votes == null) {
return;
}
for (String num : votes) {
ConfigDataNode node = getServerVotes(server, num);
if (node == null) {
continue;
}
DataNode uuidNode = node.get("UUID");
DataNode serviceNode = node.get("Service");
DataNode timeNode = node.get("Time");
if (uuidNode == null || serviceNode == null || timeNode == null) {
continue;
}
String nodeUuid = uuidNode.asString();
String nodeService = serviceNode.asString();
Long nodeTime = timeNode.asLong();
if (nodeUuid != null && nodeService != null && nodeTime != null && nodeUuid.equals(vote.getUuid())
&& nodeService.equals(vote.getService()) && nodeTime.longValue() == vote.getTime()) {
remove("VoteCache", server, num);
}
}
}
@Override
public void removeOnlineVote(OfflineBungeeVote vote) {
Collection<String> players = getPlayers();
if (players == null) {
return;
}
for (String player : players) {
Collection<String> onlineVotes = getOnlineVotes(player);
if (onlineVotes == null) {
continue;
}
for (String num : onlineVotes) {
ConfigDataNode node = getOnlineVotes(player, num);
if (node == null) {
continue;
}
DataNode uuidNode = node.get("UUID");
DataNode serviceNode = node.get("Service");
DataNode timeNode = node.get("Time");
if (uuidNode == null || serviceNode == null || timeNode == null) {
continue;
}
String nodeUuid = uuidNode.asString();
String nodeService = serviceNode.asString();
Long nodeTime = timeNode.asLong();
if (nodeUuid != null && nodeService != null && nodeTime != null && nodeUuid.equals(vote.getUuid())
&& nodeService.equals(vote.getService()) && nodeTime.longValue() == vote.getTime()) {
remove("OnlineCache", player, num);
}
}
}
}
/**
* Sets a value at a specific path.
* @param value the value to set
* @param path the path elements
*/
private void setPath(Object value, Object... path) {
set(path, value);
}
}
@@ -1,1000 +0,0 @@
package com.bencodez.votingplugin.proxy.velocity;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.CodeSource;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.bstats.charts.SimplePie;
import org.bstats.velocity.Metrics;
import org.slf4j.Logger;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import com.bencodez.simpleapi.file.velocity.VelocityYMLFile;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfig;
import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigVelocity;
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
import com.bencodez.votingplugin.proxy.VotingPluginProxyConfig;
import com.google.inject.Inject;
import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Dependency;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.scheduler.ScheduledTask;
import lombok.Getter;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
/**
* VotingPlugin proxy implementation for Velocity.
*
* Reload behavior:
* <ul>
* <li><b>/vpp reload</b> = small reload (config.reload +
* votingPluginProxy.reload)</li>
* <li><b>/vpp reloadall</b> = full reload (rebuild runtime, reload mysql,
* reload caches/tasks)</li>
* </ul>
*
*/
@Plugin(id = "votingplugin", name = "VotingPlugin", version = "1.0", url = "https://www.spigotmc.org/resources/votingplugin.15358/", description = "VotingPlugin Velocity Version", authors = {
"BenCodez" }, dependencies = { @Dependency(id = "nuvotifier", optional = true) })
public class VotingPluginVelocity {
/**
* Current plugin message channel.
*/
private volatile ChannelIdentifier channel;
@Getter
private VelocityConfig config;
private final Path dataDirectory;
@Getter
private final Logger logger;
private final Metrics.Factory metricsFactory;
private final ProxyServer server;
private String version = "";
private String buildNumber = "NOTSET";
private File versionFile;
@Getter
private ScheduledExecutorService timer;
@Getter
private volatile VotingPluginProxy votingPluginProxy;
private VelocityJsonVoteCache voteCacheFile;
private VelocityJsonNonVotedPlayersCache nonVotedPlayersCache;
private ScheduledTask voteCheckTask;
private ScheduledTask cacheSaveTask;
/**
* Register votifier listener only once.
*/
private VoteEventVelocity voteEventVelocity;
/**
* Reload lock.
*/
private final Object reloadLock = new Object();
/**
* True while reloadall is happening.
*/
private volatile boolean reloading = false;
/**
* Plugin messages received during reload are queued.
*/
private final Queue<byte[]> queuedPluginMessages = new ConcurrentLinkedQueue<byte[]>();
@Inject
public VotingPluginVelocity(ProxyServer server, Logger logger, Metrics.Factory metricsFactory,
@DataDirectory Path dataDirectory) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
this.metricsFactory = metricsFactory;
this.timer = Executors.newScheduledThreadPool(1);
}
/**
* Debug logger.
*
* @param msg message
*/
public void debug(String msg) {
if (config != null && config.getDebug()) {
logger.info("Debug: " + msg);
}
}
/**
* Alias used by older code.
*
* @param msg message
*/
public void debug2(String msg) {
debug(msg);
}
/**
* Get list of eligible servers.
*
* @return servers
*/
public Set<String> getAvailableAllServers() {
Set<String> servers = new HashSet<String>();
if (config.getWhiteListedServers().isEmpty()) {
for (RegisteredServer s : server.getAllServers()) {
String name = s.getServerInfo().getName();
if (!config.getBlockedServers().contains(name)) {
servers.add(name);
}
}
} else {
for (RegisteredServer s : server.getAllServers()) {
String name = s.getServerInfo().getName();
if (config.getWhiteListedServers().contains(name)) {
servers.add(name);
}
}
}
return servers;
}
/**
* Prefer online name if possible.
*
* @param uuid uuid string
* @param currentName current stored name
* @return name
*/
public String getProperPlayerName(String uuid, String currentName) {
try {
UUID id = UUID.fromString(uuid);
if (server.getPlayer(id).isPresent()) {
Player p = server.getPlayer(id).get();
if (p != null && p.isActive()) {
return p.getUsername();
}
}
} catch (Exception ignored) {
}
return currentName;
}
/**
* Extracts internal version file.
*/
private void getVersionFile() {
try {
CodeSource src = this.getClass().getProtectionDomain().getCodeSource();
if (src != null) {
URL jar = src.getLocation();
ZipInputStream zip = new ZipInputStream(jar.openStream());
while (true) {
ZipEntry e = zip.getNextEntry();
if (e == null) {
break;
}
if ("votingpluginversion.yml".equals(e.getName())) {
Reader defConfigStream = new InputStreamReader(zip, StandardCharsets.UTF_8);
versionFile = new File(dataDirectory.toFile(),
"tmp" + File.separator + "votingpluginversion.yml");
if (!versionFile.exists()) {
versionFile.getParentFile().mkdirs();
versionFile.createNewFile();
}
FileWriter fileWriter = new FileWriter(versionFile);
int charVal;
while ((charVal = defConfigStream.read()) != -1) {
fileWriter.append((char) charVal);
}
fileWriter.close();
defConfigStream.close();
YamlConfigurationLoader loader = YamlConfigurationLoader.builder().path(versionFile.toPath())
.build();
ConfigurationNode node = loader.load();
if (node != null) {
version = node.node("version").getString("");
buildNumber = node.node("buildnumber").getString("NOTSET");
}
return;
}
}
}
} catch (Exception e) {
if (config != null && config.getDebug()) {
e.printStackTrace();
}
}
}
@Subscribe
public void onPluginMessagingReceived(PluginMessageEvent event) {
ChannelIdentifier ch = channel;
if (ch == null) {
return;
}
if (!event.getIdentifier().equals(ch)) {
return;
}
event.setResult(PluginMessageEvent.ForwardResult.handled());
if (!(event.getSource() instanceof ServerConnection)) {
return;
}
if (reloading) {
byte[] copy = new byte[event.getData().length];
System.arraycopy(event.getData(), 0, copy, 0, event.getData().length);
queuedPluginMessages.add(copy);
return;
}
handlePluginMessageBytes(event.getData());
}
@Subscribe
public void onProxyDisable(ProxyShutdownEvent event) {
synchronized (reloadLock) {
reloading = true;
cancelTasks();
try {
if (voteCacheFile != null) {
voteCacheFile.save();
}
} catch (Exception ignored) {
}
try {
if (nonVotedPlayersCache != null) {
nonVotedPlayersCache.save();
}
} catch (Exception ignored) {
}
try {
if (votingPluginProxy != null) {
votingPluginProxy.onDisable();
}
} catch (Exception ignored) {
}
try {
if (timer != null) {
timer.shutdownNow();
}
} catch (Exception ignored) {
}
reloading = false;
}
logger.info("VotingPlugin disabled");
}
@Subscribe
public void onProxyInitialization(ProxyInitializeEvent event) {
File configFile = new File(dataDirectory.toFile(), "bungeeconfig.yml");
configFile.getParentFile().mkdirs();
if (!configFile.exists()) {
try {
configFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
InputStream toCopyStream = VotingPluginVelocity.class.getClassLoader()
.getResourceAsStream("bungeeconfig.yml");
if (toCopyStream != null) {
try (FileOutputStream fos = new FileOutputStream(configFile)) {
byte[] buf = new byte[2048];
int r;
while (-1 != (r = toCopyStream.read(buf))) {
fos.write(buf, 0, r);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
config = new VelocityConfig(configFile);
channel = buildChannelIdentifier(config.getPluginMessageChannel());
server.getChannelRegistrar().register(channel);
CommandMeta meta = server.getCommandManager().metaBuilder("votingpluginproxy").aliases("vpp").build();
server.getCommandManager().register(meta, new VotingPluginVelocityCommand(this));
// create runtime
votingPluginProxy = createProxyRuntime();
// full init
reloadAllInternal(true);
// version
try {
getVersionFile();
if (versionFile != null) {
versionFile.delete();
if (versionFile.getParentFile() != null) {
versionFile.getParentFile().delete();
}
}
} catch (Exception ignored) {
}
// metrics (same as your original, shortened)
Metrics metrics = metricsFactory.make(this, 11547);
metrics.addCustomChart(new SimplePie("bungee_method", () -> getConfig().getBungeeMethod().toString()));
metrics.addCustomChart(new SimplePie("config_onlinemode", () -> "" + getConfig().getOnlineMode()));
metrics.addCustomChart(new SimplePie("sendtoallservers", () -> "" + getConfig().getSendVotesToAllServers()));
metrics.addCustomChart(new SimplePie("allowunjoined", () -> "" + getConfig().getAllowUnJoined()));
metrics.addCustomChart(new SimplePie("pointsonvote", () -> "" + getConfig().getPointsOnVote()));
metrics.addCustomChart(new SimplePie("bungeemanagetotals", () -> "" + getConfig().getBungeeManageTotals()));
metrics.addCustomChart(new SimplePie("waitforuseronline", () -> "" + getConfig().getWaitForUserOnline()));
metrics.addCustomChart(new SimplePie("plugin_version", () -> "" + version));
logger.info("VotingPlugin velocity loaded, method: " + getVotingPluginProxy().getMethod().toString()
+ ", Internal Jar Version: " + version);
if (!"NOTSET".equals(buildNumber)) {
logger.info("Detected using dev build number: " + buildNumber);
}
}
/**
* Reloads VotingPluginProxy on Velocity.
*
* <p>
* Two modes:
* </p>
* <ul>
* <li><b>Full reload</b> ({@code loadMysql=true}): shuts down the old runtime,
* recreates the proxy, reconnects MySQL, then loads caches and handlers.</li>
* <li><b>Soft reload</b> ({@code loadMysql=false}): does NOT recreate the proxy
* instance (because it owns MySQL). Only reloads config and applies
* runtime-only settings via {@link VotingPluginProxy#reload()}.</li>
* </ul>
*
* @param loadMysql whether to rebuild MySQL and fully reinitialize the proxy
*/
public void reloadAllInternal(boolean loadMysql) {
synchronized (reloadLock) {
reloading = true;
// Always stop repeating tasks while we touch state
cancelTasks();
// Always reload config first
try {
config.reload(); // or config.load() - whichever your VelocityConfig provides
} catch (Exception e) {
logger.error("Failed to reload bungeeconfig.yml", e);
}
// Update plugin message channel (safe for both modes)
try {
ChannelIdentifier old = channel;
ChannelIdentifier next = buildChannelIdentifier(config.getPluginMessageChannel());
if (old != null && !old.equals(next)) {
try {
server.getChannelRegistrar().unregister(old);
} catch (Exception ignored) {
}
}
try {
server.getChannelRegistrar().register(next);
} catch (Exception ignored) {
}
channel = next;
} catch (Exception e) {
logger.error("Failed to update plugin message channel", e);
}
// =========================
// SOFT RELOAD (NO MYSQL)
// =========================
if (!loadMysql) {
// We intentionally do NOT call onDisable()/shutdown, because that would stop
// MySQL.
// We also do NOT recreate the proxy instance, because it owns the MySQL
// pool/table.
try {
if (votingPluginProxy != null) {
votingPluginProxy.reload();
}
} catch (Throwable t) {
logger.error("Error while applying soft reload", t);
}
// Restart tasks that don't depend on rebuilding the proxy
scheduleTasks();
reloading = false;
} else {
// =========================
// FULL RELOAD (WITH MYSQL)
// =========================
// Best-effort save caches before shutdown
try {
if (voteCacheFile != null) {
voteCacheFile.save();
}
} catch (Exception ignored) {
}
try {
if (nonVotedPlayersCache != null) {
nonVotedPlayersCache.save();
}
} catch (Exception ignored) {
}
// Shutdown old runtime completely (this stops old MySQL pool)
try {
if (votingPluginProxy != null) {
votingPluginProxy.onDisable();
}
} catch (Exception ignored) {
}
// Recreate runtime
votingPluginProxy = createProxyRuntime();
// Init MySQL BEFORE calling proxy.load(...)
try {
if (config.hasDatabaseConfigured()) {
votingPluginProxy.loadMysql(getMysqlConfig(), getGlobalDataMysqlConfig());
} else {
logger.error("MySQL settings not set in bungeeconfig.yml");
votingPluginProxy.setProxyMySQL(null);
}
} catch (Throwable t) {
logger.error("Failed to initialize MySQL during reload", t);
votingPluginProxy.setProxyMySQL(null);
}
// If MySQL is required (your proxy.load assumes it), abort cleanly
if (votingPluginProxy.getProxyMySQL() == null) {
logger.error("Reload aborted: Proxy MySQL is not initialized.");
reloading = false;
return;
}
// Recreate storages (or keep existing if you prefer)
voteCacheFile = new VelocityJsonVoteCache(new File(dataDirectory.toFile(), "votecache.json"));
nonVotedPlayersCache = new VelocityJsonNonVotedPlayersCache(
new File(dataDirectory.toFile(), "nonvotedplayerscache.json"));
convertYamlCachesIfPresent();
// Load proxy state (requires MySQL)
try {
votingPluginProxy.load(voteCacheFile, nonVotedPlayersCache);
} catch (Throwable t) {
logger.error("Reload aborted while loading proxy state", t);
reloading = false;
return;
}
// Restart tasks
scheduleTasks();
// Apply runtime-only config
try {
votingPluginProxy.reload();
} catch (Throwable t) {
logger.error("Error while reloading proxy internals", t);
}
reloading = false;
}
}
// Out of lock: flush queued messages (optional)
drainQueuedPluginMessages();
initVotifierListenerIfNeeded();
// Optional: re-announce server names
try {
if (votingPluginProxy != null) {
votingPluginProxy.sendServerNameMessage();
}
} catch (Exception ignored) {
}
}
/**
* Create proxy runtime.
*
* @return runtime
*/
private VotingPluginProxy createProxyRuntime() {
return new VotingPluginProxy() {
@Override
public void broadcast(String message) {
server.getAllPlayers().forEach(
player -> player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(message)));
}
@Override
public void debug(String str) {
debug2(str);
}
@Override
public Set<String> getAllAvailableServers() {
return getAvailableAllServers();
}
@Override
public VotingPluginProxyConfig getConfig() {
return config;
}
@Override
public String getCurrentPlayerServer(String player) {
if (server.getPlayer(player).isPresent()) {
Player p = server.getPlayer(player).get();
if (p.getCurrentServer().isPresent()) {
return p.getCurrentServer().get().getServer().getServerInfo().getName();
}
}
return "";
}
@Override
public File getDataFolderPlugin() {
return dataDirectory.toFile();
}
@Override
public String getProperName(String uuid, String playerName) {
return getProperPlayerName(uuid, playerName);
}
@Override
public String getUUID(String playerName) {
if (playerName == null || playerName.isEmpty() || "null".equalsIgnoreCase(playerName)) {
return "";
}
if (!config.getOnlineMode()) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.toLowerCase(Locale.ROOT).trim())
.getBytes(StandardCharsets.UTF_8)).toString();
}
if (server.getPlayer(playerName).isPresent()) {
Player p = server.getPlayer(playerName).get();
if (p != null && p.isActive()) {
playerName = p.getUsername();
}
}
for (Entry<UUID, String> entry : getVotingPluginProxy().getUuidPlayerNameCache().entrySet()) {
if (entry.getValue() != null && entry.getValue().equalsIgnoreCase(playerName)) {
playerName = entry.getValue();
break;
}
}
if (server.getPlayer(playerName).isPresent()) {
Player p = server.getPlayer(playerName).get();
if (p != null && p.isActive()) {
return p.getUniqueId().toString();
}
}
for (Entry<UUID, String> entry : getVotingPluginProxy().getUuidPlayerNameCache().entrySet()) {
if (entry.getValue() != null && entry.getValue().equalsIgnoreCase(playerName)) {
return entry.getKey().toString();
}
}
if (getVotingPluginProxy().getProxyMySQL() != null) {
String str = getVotingPluginProxy().getProxyMySQL().getUUID(playerName);
if (str != null) {
return str;
}
}
return getVotingPluginProxy().getNonVotedPlayersCache().getUUID(playerName);
}
@Override
public String getPluginVersion() {
return version;
}
@Override
public int getVoteCacheCurrentVotePartyVotes() {
return voteCacheFile.getVotePartyCurrentVotes();
}
@Override
public long getVoteCacheLastUpdated() {
return voteCacheFile.getNode("Time", "LastUpdated").getLong();
}
@Override
public int getVoteCachePrevDay() {
return voteCacheFile.getNode("Time", "Day").getInt();
}
@Override
public String getVoteCachePrevMonth() {
return voteCacheFile.getNode("Time", "Month").getString("");
}
@Override
public int getVoteCachePrevWeek() {
return voteCacheFile.getNode("Time", "Week").getInt();
}
@Override
public int getVoteCacheVotePartyIncreaseVotesRequired() {
return voteCacheFile.getVotePartyInreaseVotesRequired();
}
@Override
public boolean isVoteCacheIgnoreTime() {
return voteCacheFile.getNode("Time", "IgnoreTime").getBoolean();
}
@Override
public void setVoteCacheLastUpdated() {
voteCacheFile.set(new Object[] { "Time", "LastUpdated" }, System.currentTimeMillis());
voteCacheFile.save();
}
@Override
public void setVoteCachePrevDay(int day) {
voteCacheFile.set(new Object[] { "Time", "Day" }, day);
voteCacheFile.save();
}
@Override
public void setVoteCachePrevMonth(String text) {
voteCacheFile.set(new Object[] { "Time", "Month" }, text);
voteCacheFile.save();
}
@Override
public void setVoteCachePrevWeek(int week) {
voteCacheFile.set(new Object[] { "Time", "Week" }, week);
voteCacheFile.save();
}
@Override
public void setVoteCacheVoteCacheIgnoreTime(boolean ignore) {
voteCacheFile.set(new Object[] { "Time", "IgnoreTime" }, ignore);
voteCacheFile.save();
}
@Override
public void setVoteCacheVotePartyCurrentVotes(int votes) {
voteCacheFile.setVotePartyCurrentVotes(votes);
}
@Override
public void setVoteCacheVotePartyIncreaseVotesRequired(int votes) {
voteCacheFile.setVotePartyInreaseVotesRequired(votes);
}
@Override
public boolean isPlayerOnline(String playerName) {
if (playerName == null) {
return false;
}
return server.getPlayer(playerName).map(Player::isActive).orElse(false);
}
@Override
public boolean isServerValid(String serverName) {
return server.getServer(serverName).isPresent();
}
@Override
public boolean isSomeoneOnlineServer(String serverName) {
if (server.getServer(serverName).isPresent()) {
return !server.getServer(serverName).get().getPlayersConnected().isEmpty();
}
return false;
}
@Override
public void log(String message) {
logger.info(message);
}
@Override
public void logSevere(String message) {
logger.error(message);
}
@Override
public void runAsync(Runnable run) {
runAsyncNow(run);
}
@Override
public void runConsoleCommand(String command) {
server.getCommandManager().executeAsync(server.getConsoleCommandSource(), command);
}
@Override
public void saveVoteCacheFile() {
voteCacheFile.save();
}
@Override
public void sendPluginMessageData(String serverName, String channelName, byte[] data, boolean queue) {
if (!server.getServer(serverName).isPresent()) {
return;
}
RegisteredServer send = server.getServer(serverName).get();
send.sendPluginMessage(channel, data);
}
@Override
public void warn(String message) {
logger.warn(message);
}
@Override
public void reloadCore(boolean mysql) {
// mysql=true is reloadall behavior
reloadAllInternal(mysql);
}
@Override
public ScheduledExecutorService getScheduler() {
return timer;
}
@Override
public MysqlConfig getVoteCacheMySQLConfig() {
return new MysqlConfigVelocity("VoteCache", config);
}
@Override
public MysqlConfig getNonVotedCacheMySQLConfig() {
return new MysqlConfigVelocity("NonVotedCache", config);
}
@Override
public MysqlConfig getVoteLoggingMySQLConfig() {
return new MysqlConfigVelocity("VoteLogging", config);
}
@Override
public void loadTaskTimer(Runnable runnable, long delaySeconds, long repeatSeconds) {
timer.scheduleAtFixedRate(runnable, delaySeconds, repeatSeconds, TimeUnit.SECONDS);
}
};
}
/**
* Gets the MySQL configuration.
*
* <p>
* Prefers the "Database" section if it exists and is usable. If the "Database"
* section is missing, not a map, or has no keys, falls back to legacy/root
* configuration parsing.
* </p>
*
* @return mysql config (never null)
*/
public MysqlConfig getMysqlConfig() {
final ConfigurationNode root = config.getData();
final ConfigurationNode db = root.node("Database");
// If Database section doesn't exist (virtual), isn't a section/map, or is empty
// -> fallback
if (isMissingOrEmptySection(db)) {
return new MysqlConfigVelocity(config);
}
return new MysqlConfigVelocity("Database", config);
}
/**
* Checks whether a configuration node represents a missing or empty section.
*
* @param node configuration node
* @return true if the node is missing/virtual, not a map/section, or contains
* no keys
*/
private boolean isMissingOrEmptySection(ConfigurationNode node) {
if (node == null) {
return true;
}
// "virtual" typically means it doesn't exist in the file
try {
if (node.virtual()) {
return true;
}
} catch (Throwable ignored) {
// Some shaded/older configurate nodes may not expose virtual() consistently.
}
// If it's not a map/section, it's not a usable "Database:" block
if (!node.isMap()) {
return true;
}
// Treat an empty map (Database: with no keys) as missing -> fallback
return node.childrenMap() == null || node.childrenMap().isEmpty();
}
public MysqlConfig getGlobalDataMysqlConfig() {
return new MysqlConfigVelocity("GlobalData", config);
}
/**
* Register votifier listener once.
*/
private void initVotifierListenerIfNeeded() {
try {
Class.forName("com.vexsoftware.votifier.velocity.event.VotifierEvent");
} catch (ClassNotFoundException e) {
getVotingPluginProxy().setVotifierEnabled(false);
return;
}
if (getVotingPluginProxy().isVotifierEnabled()) {
if (voteEventVelocity == null) {
try {
voteEventVelocity = new VoteEventVelocity(this);
server.getEventManager().register(this, voteEventVelocity);
} catch (Exception e) {
getVotingPluginProxy().setVotifierEnabled(false);
}
}
}
}
private void cancelTasks() {
try {
if (voteCheckTask != null) {
voteCheckTask.cancel();
voteCheckTask = null;
}
} catch (Exception ignored) {
}
try {
if (cacheSaveTask != null) {
cacheSaveTask.cancel();
cacheSaveTask = null;
}
} catch (Exception ignored) {
}
}
private void scheduleTasks() {
voteCheckTask = server.getScheduler().buildTask(this, () -> {
if (getVotingPluginProxy().getGlobalDataHandler() == null
|| !getVotingPluginProxy().getGlobalDataHandler().isTimeChangedHappened()) {
for (String srv : getVotingPluginProxy().getVoteCacheHandler().getCachedVotesServers()) {
getVotingPluginProxy().checkCachedVotes(srv);
}
for (Player player : server.getAllPlayers()) {
getVotingPluginProxy().checkOnlineVotes(player.getUsername(), player.getUniqueId().toString(),
null);
}
}
}).delay(120, TimeUnit.SECONDS).repeat(60, TimeUnit.SECONDS).schedule();
cacheSaveTask = server.getScheduler().buildTask(this, () -> {
if (nonVotedPlayersCache != null) {
debug("Checking nonvotedplayerscache...");
getVotingPluginProxy().getNonVotedPlayersCache().check();
}
if (voteCacheFile != null) {
voteCacheFile.save();
}
}).delay(1L, TimeUnit.MINUTES).repeat(60L, TimeUnit.MINUTES).schedule();
}
private void runAsyncNow(Runnable runnable) {
server.getScheduler().buildTask(this, runnable).schedule();
}
private void handlePluginMessageBytes(byte[] data) {
ByteArrayInputStream instream = new ByteArrayInputStream(data);
DataInputStream in = new DataInputStream(instream);
try {
getVotingPluginProxy().onPluginMessageReceived(in);
} catch (Exception e) {
e.printStackTrace();
}
}
private void drainQueuedPluginMessages() {
byte[] msg;
while ((msg = queuedPluginMessages.poll()) != null) {
handlePluginMessageBytes(msg);
}
}
/**
* Convert old YAML cache files to JSON.
*/
private void convertYamlCachesIfPresent() {
// vote cache
File yamlVoteCacheFile = new File(dataDirectory.toFile(), "votecache.yml");
if (yamlVoteCacheFile.exists()) {
VelocityYMLFile yamlVoteCache = new VelocityYMLFile(yamlVoteCacheFile);
voteCacheFile.setConf(yamlVoteCache.getData());
yamlVoteCacheFile.renameTo(new File(dataDirectory.toFile(), "oldvotecache.yml"));
voteCacheFile.save();
}
// non-voted cache (FIX: write to nonVotedPlayersCache, not voteCacheFile)
File yamlNonVotedFile = new File(dataDirectory.toFile(), "nonvotedplayerscache.yml");
if (yamlNonVotedFile.exists()) {
VelocityYMLFile yamlNonVoted = new VelocityYMLFile(yamlNonVotedFile);
nonVotedPlayersCache.setConf(yamlNonVoted.getData());
yamlNonVotedFile.renameTo(new File(dataDirectory.toFile(), "oldnonvotedplayerscache.yml"));
nonVotedPlayersCache.save();
}
}
private MinecraftChannelIdentifier buildChannelIdentifier(String raw) {
String[] parts = raw.split(":");
return MinecraftChannelIdentifier.create(parts[0].toLowerCase(Locale.ROOT), parts[1].toLowerCase(Locale.ROOT));
}
}
@@ -1,296 +0,0 @@
package com.bencodez.votingplugin.servicesites;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.bencodez.votingplugin.VotingPluginMain;
import lombok.Getter;
/**
* Handles service site name/value mappings.
*
* Primary runtime storage is serverdata.yml as a list of strings:
*
* ServiceSites:
* - "PlanetMinecraft - planetminecraft.com"
* - "TopG - topg.org"
*
* On startup this class:
* 1. Loads the list from serverdata.yml into memory.
* 2. Checks the web sources once for updates and overwrites
* serverdata.yml if changed.
*
* Lookups are case-insensitive while preserving original casing.
*/
public class ServiceSiteHandler {
private static final String PRIMARY_URL = "https://raw.githubusercontent.com/wiki/BenCodez/VotingPlugin/Minecraft-Server-Lists.md";
private static final String SECONDARY_URL = "https://wiki.bencodez.com/en/VotingPlugin/Minecraft-Server-Lists";
private static final String TERTIARY_URL = "https://wiki-backup.bencodez.com/VotingPlugin/Minecraft-Server-Lists/";
private static final String SERVERDATA_PATH = "ServiceSites";
private static final Pattern LINE_PATTERN = Pattern.compile("^\\s*(.+?)\\s+-\\s+(.+?)\\s*$");
private static final Pattern LI_PATTERN = Pattern.compile("<li>\\s*(.*?)\\s*</li>", Pattern.CASE_INSENSITIVE);
private final VotingPluginMain plugin;
/**
* Shared HTTP client.
*/
private final HttpClient httpClient = HttpClient.newBuilder()
.followRedirects(Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(5))
.build();
private final AtomicReference<Map<String, String>> sitesRef = new AtomicReference<>(
Collections.unmodifiableMap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
@Getter
private Map<String, String> serviceSites = sitesRef.get();
public ServiceSiteHandler(VotingPluginMain plugin) {
this.plugin = plugin;
loadFromServerData();
refreshFromWebOnce(true);
for (Map.Entry<String, String> entry : sitesRef.get().entrySet()) {
plugin.extraDebug(entry.getKey() + " - " + entry.getValue());
}
}
public boolean contains(String service) {
return service != null && sitesRef.get().containsKey(service);
}
public String match(String service) {
if (service == null) {
return null;
}
return sitesRef.get().getOrDefault(service, service);
}
public String matchReverse(String value) {
if (value == null) {
return null;
}
for (Map.Entry<String, String> e : sitesRef.get().entrySet()) {
if (e.getValue().equalsIgnoreCase(value)) {
return e.getKey();
}
}
return value;
}
public void loadFromServerData() {
List<String> lines = plugin.getServerData().getData().getStringList(SERVERDATA_PATH);
Map<String, String> parsed = parseLines(lines, true);
if (!parsed.isEmpty()) {
setSites(parsed);
plugin.extraDebug("Loaded " + parsed.size() + " service sites from serverdata.yml");
}
}
public void saveToServerData() {
List<String> out = toListFormat(sitesRef.get());
plugin.getServerData().getData().set(SERVERDATA_PATH, out);
plugin.getServerData().saveData();
}
public void refreshFromWebOnce(boolean logMore) {
try {
FetchResult res = fetchWithFallback(PRIMARY_URL, SECONDARY_URL, TERTIARY_URL);
Map<String, String> parsed = parseFromWebBody(res.body, res.contentType);
if (parsed.isEmpty()) {
if (logMore) {
plugin.extraDebug("ServiceSite refresh returned 0 entries from " + res.sourceUrl);
}
return;
}
if (mapsEqual(sitesRef.get(), parsed)) {
if (logMore) {
plugin.extraDebug("ServiceSite refresh: no changes");
}
return;
}
setSites(parsed);
saveToServerData();
plugin.extraDebug("ServiceSite refresh updated from " + res.sourceUrl + " (" + parsed.size() + " entries)");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
plugin.debug(e);
} catch (Exception e) {
plugin.debug(e);
}
}
public List<String> toListFormat(Map<String, String> map) {
List<String> out = new ArrayList<>(map.size());
for (Map.Entry<String, String> e : map.entrySet()) {
out.add(e.getKey() + " - " + e.getValue());
}
return out;
}
private void setSites(Map<String, String> newSites) {
Map<String, String> unmodifiable = Collections.unmodifiableMap(
new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER) {
{
putAll(newSites);
}
});
sitesRef.set(unmodifiable);
serviceSites = unmodifiable;
}
private FetchResult fetchWithFallback(String... urls) throws IOException, InterruptedException {
IOException last = null;
for (String url : urls) {
try {
return fetch(url);
} catch (IOException e) {
last = e;
plugin.extraDebug("ServiceSite fetch failed: " + url + " (" + e.getMessage() + ")");
}
}
throw last != null ? last : new IOException("No URLs available");
}
private FetchResult fetch(String urlStr) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(urlStr))
.GET()
.timeout(Duration.ofSeconds(7))
.header("User-Agent", "VotingPlugin/ServiceSiteHandler")
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
int code = response.statusCode();
if (code < 200 || code >= 300) {
throw new IOException("HTTP " + code);
}
String contentType = response.headers().firstValue("Content-Type").orElse(null);
return new FetchResult(urlStr, response.body(), contentType);
}
private Map<String, String> parseFromWebBody(String body, String contentType) {
if (body == null || body.isEmpty()) {
return new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
}
boolean isHtml = (contentType != null && contentType.toLowerCase(Locale.ROOT).contains("text/html"))
|| body.contains("<html") || body.contains("<li");
if (isHtml) {
List<String> lines = new ArrayList<>();
Matcher m = LI_PATTERN.matcher(body);
while (m.find()) {
lines.add(htmlStrip(m.group(1)));
}
return parseLines(lines, false);
}
String[] split = body.split("\n");
List<String> lines = new ArrayList<>(split.length);
for (String s : split) {
lines.add(stripMarkup(s));
}
return parseLines(lines, false);
}
private Map<String, String> parseLines(List<String> lines, boolean debugInvalid) {
Map<String, String> parsed = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (String line : lines) {
if (line == null) {
continue;
}
String trimmed = line.trim();
if (trimmed.isEmpty()) {
continue;
}
Matcher m = LINE_PATTERN.matcher(trimmed);
if (!m.matches()) {
if (debugInvalid) {
plugin.extraDebug("Unknown ServiceSites entry: " + trimmed + ", may not be added yet");
}
continue;
}
String key = m.group(1).trim();
String val = m.group(2).trim();
if (!key.isEmpty() && !val.isEmpty()) {
parsed.put(key, val);
}
}
return parsed;
}
private static boolean mapsEqual(Map<String, String> a, Map<String, String> b) {
if (a.size() != b.size()) {
return false;
}
for (Map.Entry<String, String> e : a.entrySet()) {
if (!e.getValue().equals(b.get(e.getKey()))) {
return false;
}
}
return true;
}
private static String htmlStrip(String s) {
String out = s.replaceAll("(?i)<br\\s*/?>", "\n");
out = out.replaceAll("<[^>]+>", "");
out = out.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">");
return out.trim();
}
private static String stripMarkup(String s) {
String out = s.replaceAll("\\[(.*?)]\\((.*?)\\)", "$1");
out = out.replace("`", "");
out = out.replaceAll("^\\s*[-*]\\s+", "");
return out.trim();
}
private static final class FetchResult {
private final String sourceUrl;
private final String body;
private final String contentType;
private FetchResult(String sourceUrl, String body, String contentType) {
this.sourceUrl = sourceUrl;
this.body = body;
this.contentType = contentType;
}
}
}
@@ -1,134 +0,0 @@
package com.bencodez.votingplugin.specialrewards;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitRunnable;
import com.bencodez.advancedcore.api.rewards.RewardBuilder;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import lombok.Getter;
import lombok.Setter;
/**
* Periodically checks NameMC likes for the configured server and gives a
* one-time reward to players who have not claimed it yet.
*/
@Getter
@Setter
public class NameMCLikeCheckerTask extends BukkitRunnable {
private VotingPluginMain plugin;
/**
* Creates a new NameMC like checker task.
*
* @param plugin the plugin
*/
public NameMCLikeCheckerTask(VotingPluginMain plugin) {
this.plugin = plugin;
}
@Override
public void run() {
if (!plugin.getSpecialRewardsConfig().isNameMCLikeRewardEnabled()) {
return;
}
String urlValue = plugin.getSpecialRewardsConfig().getNameMCLikeRewardUrl();
if (urlValue == null || urlValue.trim().isEmpty()) {
return;
}
Set<UUID> likedUuids = fetchLikedUuids(urlValue);
if (likedUuids.isEmpty()) {
return;
}
Bukkit.getScheduler().runTask(plugin, () -> {
for (UUID uuid : likedUuids) {
processUuid(uuid);
}
});
}
/**
* Processes a UUID returned by NameMC.
*
* @param uuid the uuid
*/
private void processUuid(UUID uuid) {
if (uuid == null) {
return;
}
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
if (user == null) {
return;
}
if (user.hasClaimedNameMCLikeReward()) {
return;
}
new RewardBuilder(plugin.getSpecialRewardsConfig().getData(),
plugin.getSpecialRewardsConfig().getNameMCLikeRewardPath()).setOnline(user.isOnline())
.withPlaceHolder("NameMCServer", plugin.getSpecialRewardsConfig().getNameMCLikeRewardUrl()).send(user);
user.setClaimedNameMCLikeReward(true);
plugin.debug("Gave NameMC like reward to " + user.getPlayerName() + " (" + uuid + ")");
}
/**
* Fetches all liked UUIDs from NameMC for the configured server.
*
* @param serverUrl the server URL or IP
* @return set of liked UUIDs
*/
private Set<UUID> fetchLikedUuids(String serverUrl) {
Set<UUID> uuids = new HashSet<>();
try {
String url = "https://api.namemc.com/server/" + serverUrl + "/likes";
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").GET().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
JsonArray array = JsonParser.parseString(response.body()).getAsJsonArray();
for (JsonElement element : array) {
try {
UUID uuid = UUID.fromString(element.getAsString());
uuids.add(uuid);
} catch (IllegalArgumentException ignored) {
}
}
} else {
plugin.debug("NameMC API returned status: " + response.statusCode());
}
} catch (Exception e) {
plugin.debug("Failed to fetch NameMC likes: " + e.getMessage());
e.printStackTrace();
}
return uuids;
}
}
@@ -1,121 +0,0 @@
package com.bencodez.votingplugin.specialrewards;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Bukkit;
import com.bencodez.advancedcore.api.rewards.RewardBuilder;
import com.bencodez.simpleapi.messages.MessageAPI;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.events.PlayerSpecialRewardEvent;
import com.bencodez.votingplugin.events.SpecialRewardType;
import com.bencodez.votingplugin.user.VotingPluginUser;
// TODO: Auto-generated Javadoc
/**
* The Class OtherVoteReward.
*/
public class SpecialRewards {
private VotingPluginMain plugin;
/**
* Constructs a new SpecialRewards handler.
*
* @param plugin the main plugin instance
*/
public SpecialRewards(VotingPluginMain plugin) {
this.plugin = plugin;
}
/**
* Checks and processes vote streak rewards for a user.
*
* @param voteUUID the vote UUID
* @param user the voting user
* @param type the streak type (day/week/month)
* @param forceBungee whether to force bungee mode
* @return true if a reward was given
*/
public boolean checkVoteStreak(UUID voteUUID, VotingPluginUser user, String type, boolean forceBungee) {
boolean gotReward = false;
Set<String> streaks = plugin.getSpecialRewardsConfig().getVoteStreakVotes(type);
for (String streak : streaks) {
boolean multiple = false;
if (streak.contains("-")) {
// multiple
multiple = true;
}
String s = streak.replaceAll("-", "");
plugin.debug("Streak: " + streak + " multiple: " + multiple);
if (MessageAPI.isInt(s)) {
int streakRequired = Integer.parseInt(s);
if (streakRequired != 0) {
if (plugin.getSpecialRewardsConfig().getVoteStreakRewardEnabled(type, streak)
&& plugin.getRewardHandler().hasRewards(plugin.getSpecialRewardsConfig().getData(),
plugin.getSpecialRewardsConfig().getVoteStreakRewardsPath(type, "" + streak))) {
int curStreak = 0;
if (type.equalsIgnoreCase("day")) {
curStreak = user.getDayVoteStreak();
} else if (type.equalsIgnoreCase("week")) {
curStreak = user.getWeekVoteStreak();
} else if (type.equalsIgnoreCase("month")) {
curStreak = user.getMonthVoteStreak();
}
if (!multiple) {
if (curStreak == streakRequired) {
giveVoteStreakReward(voteUUID, user, user.isOnline(), type, "" + streakRequired,
curStreak, forceBungee);
gotReward = true;
plugin.debug(
user.getPlayerName() + " got VoteStreak " + streakRequired + " for " + type);
}
} else {
if (curStreak != 0 && curStreak % streakRequired == 0) {
giveVoteStreakReward(voteUUID, user, user.isOnline(), type, streak, curStreak,
forceBungee);
gotReward = true;
plugin.debug(
user.getPlayerName() + " got VoteStreak " + streakRequired + "* for " + type);
}
}
}
}
}
}
return gotReward;
}
/**
* Gives a vote streak reward to a user.
*
* @param voteUUID the vote UUID
* @param user the voting user
* @param online whether the user is online
* @param type the streak type
* @param string the streak value
* @param votes the current streak count
* @param forceBungee whether to force bungee mode
*/
public void giveVoteStreakReward(UUID voteUUID, VotingPluginUser user, boolean online, String type, String string,
int votes, boolean forceBungee) {
PlayerSpecialRewardEvent event = new PlayerSpecialRewardEvent(user,
SpecialRewardType.VOTESTREAK.setType(type).setAmount(votes), voteUUID);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
return;
}
new RewardBuilder(plugin.getSpecialRewardsConfig().getData(),
plugin.getSpecialRewardsConfig().getVoteStreakRewardsPath(type, string)).setOnline(online)
.withPlaceHolder("Type", type).setServer(forceBungee).withPlaceHolder("Streak", "" + votes).send(user);
}
}
@@ -1,282 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.bukkit.configuration.ConfigurationSection;
/**
* Supports: - At: 0 - At: 50 - At: [20, 50, 100] - At: - 10..200 step 10 - 250
*
* Stateless evaluation: matches based solely on newTotal.
*/
public class AtMatcher {
private final boolean hasZero;
private final Set<Long> exactTotals;
private AtMatcher(boolean hasZero, Set<Long> exactTotals) {
this.hasZero = hasZero;
this.exactTotals = exactTotals;
}
/**
* Checks if a new total matches the criteria.
*
* @param newTotal the new total to check
* @return true if the total matches
*/
public boolean matches(long newTotal) {
// At: 0 => first vote in that period => newTotal == 1
if (hasZero && newTotal == 1) {
return true;
}
return exactTotals.contains(newTotal);
}
/**
* Human-friendly debug string.
*
* Examples: - "0" - "20,50,100" - "0 + 10 values: [10,20,30,...,980,990,1000]"
*
* @return the debug string representation
*/
public String toDebugString() {
return toDebugString(8, 3);
}
/**
* Check if the matcher has zero.
*
* @return true if zero is included
*/
public boolean hasZero() {
return hasZero;
}
/**
* Get the exact totals.
*
* @return an unmodifiable set of exact totals
*/
public Set<Long> getExactTotals() {
return java.util.Collections.unmodifiableSet(exactTotals);
}
/**
* Returns a compact string for logs.
*
* @param maxValues the maximum number of values to display
* @return the debug string representation
*/
public String toDebugString(int maxValues) {
StringBuilder sb = new StringBuilder();
boolean first = true;
if (hasZero) {
sb.append("0(first)");
first = false;
}
java.util.List<Long> vals = new java.util.ArrayList<>(exactTotals);
java.util.Collections.sort(vals);
int shown = 0;
for (Long v : vals) {
if (shown >= maxValues) {
sb.append(first ? "" : "|").append("...+").append(vals.size() - shown);
break;
}
sb.append(first ? "" : "|").append(v);
first = false;
shown++;
}
if (sb.length() == 0) {
return "none";
}
return sb.toString();
}
/**
* Generate debug string with head and tail values.
*
* @param head how many smallest values to show
* @param tail how many largest values to show when truncating
* @return the debug string representation
*/
public String toDebugString(int head, int tail) {
head = Math.max(0, head);
tail = Math.max(0, tail);
int size = exactTotals != null ? exactTotals.size() : 0;
boolean zero = hasZero;
if (!zero && size == 0) {
return "";
}
// If it's small, print everything
int inlineLimit = head + tail + 1;
TreeSet<Long> sorted = new TreeSet<>();
if (exactTotals != null) {
sorted.addAll(exactTotals);
}
List<String> parts = new ArrayList<>();
if (zero) {
parts.add("0");
}
if (size == 0) {
return String.join(",", parts);
}
if (size <= inlineLimit) {
for (Long v : sorted) {
parts.add(String.valueOf(v));
}
return String.join(",", parts);
}
// Truncated format: include count + head...tail
List<Long> headVals = new ArrayList<>(head);
List<Long> tailVals = new ArrayList<>(tail);
int i = 0;
for (Long v : sorted) {
if (i < head) {
headVals.add(v);
}
i++;
}
if (tail > 0) {
List<Long> rev = new ArrayList<>(sorted);
Collections.reverse(rev);
for (int j = 0; j < tail && j < rev.size(); j++) {
tailVals.add(rev.get(j));
}
Collections.reverse(tailVals);
}
StringBuilder sb = new StringBuilder();
if (zero) {
sb.append("0 + ");
}
sb.append(size).append(" values: [");
boolean first = true;
for (Long v : headVals) {
if (!first)
sb.append(",");
first = false;
sb.append(v);
}
sb.append(",...");
for (Long v : tailVals) {
sb.append(",").append(v);
}
sb.append("]");
return sb.toString();
}
@Override
public String toString() {
// make default toString() useful in logs
return toDebugString();
}
/**
* Create an AtMatcher from a configuration section.
*
* @param sec the configuration section
* @param path the configuration path
* @return the AtMatcher instance or null
*/
public static AtMatcher fromConfig(ConfigurationSection sec, String path) {
if (sec == null || !sec.contains(path)) {
return null;
}
boolean hasZero = false;
Set<Long> set = new HashSet<>();
Object raw = sec.get(path);
List<Object> entries = new ArrayList<>();
if (raw instanceof List) {
entries.addAll(sec.getList(path));
} else {
entries.add(raw);
}
for (Object o : entries) {
if (o == null) {
continue;
}
if (o instanceof Number) {
long v = ((Number) o).longValue();
if (v == 0) {
hasZero = true;
} else if (v > 0) {
set.add(v);
}
continue;
}
String s = String.valueOf(o).trim();
if (s.isEmpty()) {
continue;
}
// Allow "0" as string
if (s.equals("0")) {
hasZero = true;
continue;
}
// Range: "A..B step S" or "A..B"
RangeSpec r = RangeSpec.tryParse(s);
if (r != null) {
if (r.start == 0) {
// if someone writes 0..something, treat 0 specially (first vote)
hasZero = true;
}
for (long v : r.expand()) {
if (v == 0) {
hasZero = true;
} else if (v > 0) {
set.add(v);
}
}
continue;
}
// Fallback: parse as long
try {
long v = Long.parseLong(s);
if (v == 0) {
hasZero = true;
} else if (v > 0) {
set.add(v);
}
} catch (NumberFormatException ignored) {
// ignore invalid entry
}
}
if (!hasZero && set.isEmpty()) {
return null;
}
return new AtMatcher(hasZero, set);
}
}
@@ -1,114 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import java.util.ArrayList;
import java.util.List;
/**
* Parses: - "10..200 step 10" - "10..200"
*
* Expands to discrete values (simple + predictable for server owners).
*/
public final class RangeSpec {
/** The start value of the range. */
public final long start;
/** The end value of the range. */
public final long end;
/** The step increment for the range. */
public final long step;
/**
* Constructs a new RangeSpec.
*
* @param start the start value
* @param end the end value
* @param step the step increment
*/
public RangeSpec(long start, long end, long step) {
this.start = start;
this.end = end;
this.step = step;
}
/**
* Attempts to parse a range specification string.
*
* @param s the string to parse
* @return the parsed RangeSpec or null if parsing fails
*/
public static RangeSpec tryParse(String s) {
// Normalize whitespace
String str = s.trim().replaceAll("\\s+", " ");
if (!str.contains("..")) {
return null;
}
String[] parts = str.split("\\.\\.");
if (parts.length != 2) {
return null;
}
String left = parts[0].trim();
String right = parts[1].trim();
long step = 1;
// Right side might contain "step"
String[] rightParts = right.split(" step ");
String endStr = rightParts[0].trim();
if (rightParts.length == 2) {
try {
step = Long.parseLong(rightParts[1].trim());
} catch (NumberFormatException ignored) {
return null;
}
} else if (rightParts.length > 2) {
return null;
}
long start;
long end;
try {
start = Long.parseLong(left);
end = Long.parseLong(endStr);
} catch (NumberFormatException ignored) {
return null;
}
if (step <= 0) {
return null;
}
// allow reversed ranges by swapping
if (end < start) {
long tmp = start;
start = end;
end = tmp;
}
return new RangeSpec(start, end, step);
}
/**
* Expands the range into a list of discrete values.
*
* @return the list of values in the range
*/
public List<Long> expand() {
List<Long> out = new ArrayList<>();
// Safety: avoid accidental huge expansions
// If you want, expose this as a config value. For now keep sane.
final int HARD_MAX = 250_000;
long count = 0;
for (long v = start; v <= end; v += step) {
out.add(v);
count++;
if (count >= HARD_MAX) {
break;
}
}
return out;
}
}
@@ -1,100 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import lombok.Getter;
/**
* Represents a single VoteMilestone entry.
*
* rewardPath:
* - Absolute path from SpecialRewards.yml root (e.g.
* "VoteMilestones.MyMilestone.Rewards", "Cumulative.20.Rewards", "FirstVote")
*/
public class VoteMilestone {
@Getter
private final String id;
@Getter
private final boolean enabled;
@Getter
private final VoteMilestoneTotal total;
@Getter
private final AtMatcher atMatcher; // may be null
@Getter
private final Integer every; // may be null
@Getter
private final String rewardPath;
@Getter
private final VoteMilestoneGroupSelect groupSelect;
@Getter
private final String groupKey;
@Getter
private final VoteMilestoneLimit limit;
/**
* Constructs a new VoteMilestone.
*
* @param id milestone id
* @param enabled whether milestone is enabled
* @param total total type to check
* @param atMatcher matcher for specific values (may be null)
* @param every interval for repeating milestone (may be null)
* @param rewardPath path to rewards configuration
* @param groupSelect group selection mode
* @param groupKey group key
* @param limit retrigger limit
*/
public VoteMilestone(String id, boolean enabled, VoteMilestoneTotal total, AtMatcher atMatcher, Integer every,
String rewardPath, VoteMilestoneGroupSelect groupSelect, String groupKey, VoteMilestoneLimit limit) {
this.id = id;
this.enabled = enabled;
this.total = total;
this.atMatcher = atMatcher;
this.every = every;
this.rewardPath = rewardPath;
this.groupSelect = groupSelect;
this.groupKey = groupKey;
this.limit = (limit == null ? VoteMilestoneLimit.none() : limit);
}
/**
* Checks if this milestone has an at matcher for debugging.
*
* @return true if at matcher exists
*/
public boolean hasAtMatcherDebug() {
return atMatcher != null;
}
/**
* Gets debug string representation of the at matcher.
*
* @param maxValues maximum number of values to include
* @return debug string or "none"
*/
public String getAtMatcherDebugString(int maxValues) {
return atMatcher == null ? "none" : atMatcher.toDebugString(maxValues);
}
/**
* Checks if this milestone matches the given total.
*
* @param newTotal total to check against
* @return true if milestone matches
*/
public boolean matches(long newTotal) {
if (atMatcher != null && atMatcher.matches(newTotal)) {
return true;
}
if (every != null && every > 0) {
return newTotal > 0 && (newTotal % every == 0);
}
return false;
}
}
@@ -1,32 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import org.bukkit.configuration.MemoryConfiguration;
final class VoteMilestoneFactory {
private VoteMilestoneFactory() {
}
static VoteMilestone atZero(String id, boolean enabled, VoteMilestoneTotal total, String rewardPath,
VoteMilestoneGroupSelect groupSelect, String voteMilestoneGroupKey, VoteMilestoneLimit limit) {
MemoryConfiguration tmp = new MemoryConfiguration();
tmp.set("At", 1);
AtMatcher at = AtMatcher.fromConfig(tmp, "At");
return new VoteMilestone(id, enabled, total, at, null, rewardPath, groupSelect, voteMilestoneGroupKey, limit);
}
static VoteMilestone atExact(String id, boolean enabled, VoteMilestoneTotal total, long atValue, String rewardPath,
VoteMilestoneGroupSelect groupSelect, String voteMilestoneGroupKey, VoteMilestoneLimit limit) {
MemoryConfiguration tmp = new MemoryConfiguration();
tmp.set("At", atValue);
AtMatcher at = AtMatcher.fromConfig(tmp, "At");
return new VoteMilestone(id, enabled, total, at, null, rewardPath, groupSelect, voteMilestoneGroupKey, limit);
}
static VoteMilestone every(String id, boolean enabled, VoteMilestoneTotal total, int every, String rewardPath,
VoteMilestoneGroupSelect groupSelect, String voteMilestoneGroupKey, VoteMilestoneLimit limit) {
return new VoteMilestone(id, enabled, total, null, every, rewardPath, groupSelect, voteMilestoneGroupKey, limit);
}
}
@@ -1,115 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import java.util.HashMap;
import java.util.Map;
import org.bukkit.configuration.ConfigurationSection;
/**
* Utility class for parsing vote milestone group configuration.
*/
public final class VoteMilestoneGroupParser {
private VoteMilestoneGroupParser() {
}
/**
* Parses group selection modes from configuration.
*
* @param root root configuration section
* @return map of group id to selection mode
*/
public static Map<String, VoteMilestoneGroupSelect> parseGroups(ConfigurationSection root) {
HashMap<String, VoteMilestoneGroupSelect> out = new HashMap<>();
if (root == null) {
out.put("default", VoteMilestoneGroupSelect.ALL);
return out;
}
ConfigurationSection opts = root.getConfigurationSection("VoteMilestonesOptions");
if (opts == null) {
out.put("default", VoteMilestoneGroupSelect.ALL);
return out;
}
ConfigurationSection groups = opts.getConfigurationSection("Groups");
if (groups == null) {
out.put("default", VoteMilestoneGroupSelect.ALL);
return out;
}
// Default mode for groups not explicitly listed
VoteMilestoneGroupSelect defaultMode =
parseSelect(groups.getString("Default", "ALL"), VoteMilestoneGroupSelect.ALL);
out.put("default", defaultMode);
// Parse other group mappings
for (String key : groups.getKeys(false)) {
if (key == null) {
continue;
}
String groupId = key.trim();
if (groupId.isEmpty()) {
continue;
}
if (groupId.equalsIgnoreCase("Default")) {
continue;
}
String raw = groups.getString(key, null);
VoteMilestoneGroupSelect mode = parseSelect(raw, defaultMode);
// If you want case-insensitive ids everywhere, normalize here:
// groupId = groupId.toLowerCase();
out.put(groupId, mode);
}
return out;
}
/**
* Parse selection mode with optional aliases. Falls back to fallback.
*/
private static VoteMilestoneGroupSelect parseSelect(String raw, VoteMilestoneGroupSelect fallback) {
if (raw == null) {
return fallback;
}
String v = raw.trim().toUpperCase();
if (v.isEmpty()) {
return fallback;
}
// Optional aliases
switch (v) {
case "ALL":
case "ANY":
case "EVERY":
case "0":
return VoteMilestoneGroupSelect.ALL;
case "HIGHEST":
case "MAX":
case "TOP":
case "2":
return VoteMilestoneGroupSelect.HIGHEST;
case "FIRST":
case "ONE":
case "EARLIEST":
case "1":
return VoteMilestoneGroupSelect.FIRST;
default:
break;
}
try {
return VoteMilestoneGroupSelect.valueOf(v);
} catch (Exception e) {
return fallback;
}
}
}
@@ -1,22 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
/**
* Enum for selecting which milestones to trigger when multiple match in a group.
*/
public enum VoteMilestoneGroupSelect {
/**
* Trigger every matching milestone in the group.
*/
ALL,
/**
* If multiple milestones in the group match, only trigger the highest
* "priority" (largest AT value, else largest EVERY).
*/
HIGHEST,
/**
* If multiple match, only trigger the first one encountered (stable ordering).
*/
FIRST;
}
@@ -1,243 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.concurrent.TimeUnit;
import org.bukkit.configuration.ConfigurationSection;
import com.bencodez.simpleapi.time.ParsedDuration;
/**
* Per-milestone retrigger limiter.
*
* Stored data is lastTriggeredMs (epoch millis).
*/
public final class VoteMilestoneLimit {
/**
* Limit types for milestone retriggering.
*/
public enum Type {
/**
* No limit.
*/
NONE,
/**
* Once per calendar day window.
*/
WINDOW_DAY,
/**
* Once per week window.
*/
WINDOW_WEEK,
/**
* Once per month window.
*/
WINDOW_MONTH,
/**
* Fixed cooldown duration.
*/
COOLDOWN
}
private final Type type;
private final long cooldownMillis; // fixed millis cooldown
/**
* Constructs a new VoteMilestoneLimit.
*
* @param type limit type
* @param cooldownMillis cooldown duration in milliseconds
*/
public VoteMilestoneLimit(Type type, long cooldownMillis) {
this.type = type == null ? Type.NONE : type;
this.cooldownMillis = Math.max(0L, cooldownMillis);
}
/**
* Creates a VoteMilestoneLimit with no restrictions.
*
* @return limit with NONE type
*/
public static VoteMilestoneLimit none() {
return new VoteMilestoneLimit(Type.NONE, 0L);
}
/**
* Gets the limit type.
*
* @return limit type
*/
public Type getType() {
return type;
}
/**
* Gets the cooldown duration in milliseconds.
*
* @return cooldown milliseconds
*/
public long getCooldownMillis() {
return cooldownMillis;
}
/**
* Checks if the limit is enabled.
*
* @return true if limit is enabled
*/
public boolean isEnabled() {
return type != Type.NONE;
}
/**
* Parses a VoteMilestoneLimit from configuration.
*
* @param milestoneSection milestone configuration section
* @return parsed limit or none()
*/
public static VoteMilestoneLimit fromConfig(ConfigurationSection milestoneSection) {
if (milestoneSection == null) {
return none();
}
ConfigurationSection sec = milestoneSection.getConfigurationSection("Limit");
if (sec == null) {
return none();
}
String typeStr = sec.getString("Type", "NONE");
Type type = parseType(typeStr);
if (type == Type.COOLDOWN) {
String durRaw = sec.getString("Duration", "");
// Old behavior: number-only means MINUTES
ParsedDuration pd = ParsedDuration.parse(durRaw, TimeUnit.MINUTES);
// Optional legacy support:
// if no duration provided but Minutes exists
if (pd.isEmpty()) {
long minutes = sec.getLong("Minutes", 0L);
if (minutes > 0) {
return new VoteMilestoneLimit(Type.COOLDOWN, minutes * 60_000L);
}
return none(); // cooldown with no duration => treat as NONE
}
return new VoteMilestoneLimit(Type.COOLDOWN, pd.getMillis());
}
// window types or none
return new VoteMilestoneLimit(type, 0L);
}
private static Type parseType(String raw) {
if (raw == null) {
return Type.NONE;
}
String v = raw.trim().toUpperCase();
if (v.isEmpty()) {
return Type.NONE;
}
// Allow friendly aliases
switch (v) {
case "DAY":
case "DAILY":
case "WINDOW_DAILY":
return Type.WINDOW_DAY;
case "WEEK":
case "WEEKLY":
return Type.WINDOW_WEEK;
case "MONTH":
case "MONTHLY":
return Type.WINDOW_MONTH;
default:
break;
}
try {
return Type.valueOf(v);
} catch (Exception ignored) {
return Type.NONE;
}
}
/* ---------------- enforcement ---------------- */
/**
* Checks if the limit allows retriggering at the given time.
*
* @param lastTriggeredMs last triggered time in milliseconds
* @param nowMs current time in milliseconds
* @param zone time zone for window calculations
* @return true if retriggering is allowed
*/
public boolean allows(long lastTriggeredMs, long nowMs, ZoneId zone) {
if (type == Type.NONE) {
return true;
}
if (lastTriggeredMs <= 0) {
return true;
}
if (zone == null) {
zone = ZoneId.systemDefault();
}
switch (type) {
case WINDOW_DAY:
return !sameDay(lastTriggeredMs, nowMs, zone);
case WINDOW_WEEK:
return !sameWeek(lastTriggeredMs, nowMs, zone);
case WINDOW_MONTH:
return !sameMonth(lastTriggeredMs, nowMs, zone);
case COOLDOWN:
return nowMs >= nextAllowedMs(lastTriggeredMs);
default:
return true;
}
}
/**
* Calculates the next allowed trigger time in milliseconds.
*
* @param lastTriggeredMs last triggered time in milliseconds
* @return next allowed time or 0 if not applicable
*/
public long nextAllowedMs(long lastTriggeredMs) {
if (type != Type.COOLDOWN || lastTriggeredMs <= 0) {
return 0L;
}
return lastTriggeredMs + cooldownMillis;
}
private static boolean sameDay(long aMs, long bMs, ZoneId zone) {
ZonedDateTime a = Instant.ofEpochMilli(aMs).atZone(zone);
ZonedDateTime b = Instant.ofEpochMilli(bMs).atZone(zone);
return a.toLocalDate().equals(b.toLocalDate());
}
private static boolean sameMonth(long aMs, long bMs, ZoneId zone) {
ZonedDateTime a = Instant.ofEpochMilli(aMs).atZone(zone);
ZonedDateTime b = Instant.ofEpochMilli(bMs).atZone(zone);
return a.getYear() == b.getYear() && a.getMonthValue() == b.getMonthValue();
}
private static boolean sameWeek(long aMs, long bMs, ZoneId zone) {
ZonedDateTime a = Instant.ofEpochMilli(aMs).atZone(zone);
ZonedDateTime b = Instant.ofEpochMilli(bMs).atZone(zone);
// ISO week fields is a sane default
java.time.temporal.WeekFields wf = java.time.temporal.WeekFields.ISO;
int aw = a.get(wf.weekOfWeekBasedYear());
int ay = a.get(wf.weekBasedYear());
int bw = b.get(wf.weekOfWeekBasedYear());
int by = b.get(wf.weekBasedYear());
return aw == bw && ay == by;
}
}
@@ -1,156 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import java.util.Locale;
import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot;
import com.bencodez.votingplugin.topvoter.TopVoter;
import com.bencodez.votingplugin.user.VotingPluginUser;
/**
* Defines what numeric value a VoteMilestone evaluates against.
*/
public enum VoteMilestoneTotal {
// ---- Vote totals (TopVoter-based) ----
/** All-time votes total. */
ALLTIME_VOTES {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getAllTimeTotal();
}
return user.getTotal(TopVoter.AllTime);
}
},
/** Daily votes total. */
DAILY_VOTES {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getDailyTotal();
}
return user.getTotal(TopVoter.Daily);
}
},
/** Weekly votes total. */
WEEKLY_VOTES {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getWeeklyTotal();
}
return user.getTotal(TopVoter.Weekly);
}
},
/** Monthly votes total. */
MONTHLY_VOTES {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getMonthTotal();
}
return user.getTotal(TopVoter.Monthly);
}
},
// ---- Global points (single value only) ----
/** Player points total. */
POINTS {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getPoints();
}
return user.getPoints();
}
},
/** All unique sites voted today. */
ALLSITES_TODAY {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
return user.getUniqueVoteSitesToday();
}
};
/**
* Gets the value for this total type.
*
* @param user the voting plugin user
* @param bungeeMessageData the bungee message data snapshot
* @return the total value
*/
public abstract long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData);
/**
* Parse config values flexibly.
*
* Supported examples: - ALLTIME_VOTES - AllTime - ALLTIME - Daily / Weekly /
* Monthly - Points / VotePoints - Legacy_Milestone_Count
*
* @param input the input string to parse
* @return the parsed VoteMilestoneTotal, or ALLTIME_VOTES as default
*/
public static VoteMilestoneTotal parse(String input) {
if (input == null || input.trim().isEmpty()) {
return ALLTIME_VOTES;
}
String raw = input.trim();
String key = raw.toUpperCase(Locale.ROOT);
// 1) Exact enum match (fast path)
try {
return VoteMilestoneTotal.valueOf(key);
} catch (IllegalArgumentException ignored) {
}
// 2) Friendly / legacy aliases
switch (key) {
// Votes (AllTime)
case "ALLTIME":
case "ALL_TIME":
case "ALLTIMEVOTES":
case "ALLTIMEVOTE":
case "VOTES":
return ALLTIME_VOTES;
// Votes (Daily)
case "DAILY":
case "DAY":
case "DAILYVOTES":
case "DAILYVOTE":
return DAILY_VOTES;
// Votes (Weekly)
case "WEEKLY":
case "WEEK":
case "WEEKLYVOTES":
case "WEEKLYVOTE":
return WEEKLY_VOTES;
// Votes (Monthly)
case "MONTHLY":
case "MONTH":
case "MONTHLYVOTES":
case "MONTHLYVOTE":
return MONTHLY_VOTES;
// Points
case "POINT":
case "POINTS":
case "VOTEPOINT":
case "VOTEPOINTS":
return POINTS;
case "SITES_TODAY":
case "ALLSITES_TODAY":
return ALLSITES_TODAY;
default:
return ALLTIME_VOTES;
}
}
}
@@ -1,90 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import java.util.LinkedHashMap;
import java.util.Map;
import org.bukkit.configuration.ConfigurationSection;
import com.bencodez.votingplugin.VotingPluginMain;
/**
* Configuration for vote milestones.
*/
public class VoteMilestonesConfig {
private final Map<String, VoteMilestone> milestones;
private VoteMilestonesConfig(Map<String, VoteMilestone> milestones) {
this.milestones = milestones;
}
/**
* Gets the map of milestones.
*
* @return the milestones map
*/
public Map<String, VoteMilestone> getMilestones() {
return milestones;
}
/**
* Loads vote milestones configuration from the configuration section.
*
* @param plugin the main plugin instance
* @param manager the vote milestones manager
* @param root the root configuration section
* @return the loaded vote milestones configuration
*/
public static VoteMilestonesConfig load(VotingPluginMain plugin, VoteMilestonesManager manager,
ConfigurationSection root) {
Map<String, VoteMilestone> out = new LinkedHashMap<>();
// Load new format entries (VoteMilestones section)
ConfigurationSection voteMilestonesRoot = root.getConfigurationSection("VoteMilestones");
if (voteMilestonesRoot != null) {
for (String id : voteMilestonesRoot.getKeys(false)) {
ConfigurationSection sec = voteMilestonesRoot.getConfigurationSection(id);
if (sec == null) {
continue;
}
boolean enabled = sec.getBoolean("Enabled", true);
String totalStr = sec.getString("Total", "ALLTIME_VOTES");
VoteMilestoneTotal total = VoteMilestoneTotal.parse(totalStr);
String groupSelectStr = sec.getString("Group", "Default");
AtMatcher at = AtMatcher.fromConfig(sec, "At");
Integer every = sec.contains("Every") ? sec.getInt("Every") : null;
// NEW: per-milestone limit settings (default NONE)
VoteMilestoneLimit limit = VoteMilestoneLimit.fromConfig(sec);
// New config: reward container is the milestone section itself
VoteMilestone m = new VoteMilestone(id, enabled, total, at, every, "VoteMilestones." + id + ".Rewards",
manager.getGroupModes().getOrDefault(groupSelectStr, VoteMilestoneGroupSelect.ALL),
groupSelectStr, limit);
out.put(id, m);
}
}
// Merge legacy compiled milestones (new format wins on ID collision)
ConfigurationSection legacyRoot = plugin.getSpecialRewardsConfig().getData();
int totalSites = plugin.getVoteSiteManager().getVoteSitesEnabled() != null
? plugin.getVoteSiteManager().getVoteSitesEnabled().size()
: 0;
Map<String, VoteMilestone> legacy = VoteMilestonesMigrator.compileLegacy(legacyRoot, totalSites,
plugin.getSpecialRewardsConfig().getData().getBoolean("OnlyOneCumulative", false),
plugin.getSpecialRewardsConfig().getData().getBoolean("ResetMilestonesMonthly", false));
plugin.debug(plugin.getSpecialRewardsConfig().getData().getBoolean("OnlyOneCumulative", false)
+ " Only one cumulative");
for (Map.Entry<String, VoteMilestone> e : legacy.entrySet()) {
out.putIfAbsent(e.getKey(), e.getValue());
}
return new VoteMilestonesConfig(out);
}
}
@@ -1,205 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import java.util.LinkedHashMap;
import java.util.Map;
import org.bukkit.configuration.ConfigurationSection;
/**
* Legacy migration: - Uses SpecialRewards.yml root as the reward container so
* reward paths can be absolute. - FirstVote / FirstVoteToday: rewards are on
* the section itself (no "Rewards" child) - Cumulative / MileStones: rewards
* are under "...Rewards" - Totals are mapped into VoteMilestoneTotal (votes +
* points + legacy counter)
*
* Adds per-milestone Limit support (default NONE).
*/
public final class VoteMilestonesMigrator {
private VoteMilestonesMigrator() {
}
/**
* Compiles legacy milestone configuration into the new format.
*
* @param root the configuration section containing legacy milestones
* @param totalSites the total number of vote sites
* @param onlyOneCumulative whether only one cumulative milestone should trigger
* @param resetMilestonesMonthly whether milestones reset monthly
* @return map of milestone names to VoteMilestone objects
*/
public static Map<String, VoteMilestone> compileLegacy(ConfigurationSection root, int totalSites,
boolean onlyOneCumulative, boolean resetMilestonesMonthly) {
Map<String, VoteMilestone> out = new LinkedHashMap<>();
if (root == null) {
return out;
}
compileFirstVote(root, out);
compileFirstVoteToday(root, out);
compileCumulative(root, out, onlyOneCumulative);
compileMilestones(root, out, resetMilestonesMonthly);
compileAllSites(root, out, totalSites);
compileAlmostAllSites(root, out, totalSites);
return out;
}
private static void compileFirstVote(ConfigurationSection root, Map<String, VoteMilestone> out) {
ConfigurationSection sec = root.getConfigurationSection("FirstVote");
if (sec == null || isDisabledObject(sec)) {
return;
}
// rewards are on FirstVote section itself (legacy)
VoteMilestone vm = VoteMilestoneFactory.atZero("Legacy_FirstVote", true, VoteMilestoneTotal.ALLTIME_VOTES,
"FirstVote", VoteMilestoneGroupSelect.ALL, "Default", VoteMilestoneLimit.none());
out.put(vm.getId(), vm);
}
private static void compileFirstVoteToday(ConfigurationSection root, Map<String, VoteMilestone> out) {
ConfigurationSection sec = root.getConfigurationSection("FirstVoteToday");
if (sec == null || isDisabledObject(sec)) {
return;
}
// rewards are on FirstVoteToday section itself (legacy)
VoteMilestone vm = VoteMilestoneFactory.atZero("Legacy_FirstVoteToday", true, VoteMilestoneTotal.DAILY_VOTES,
"FirstVoteToday", VoteMilestoneGroupSelect.ALL, "Default", VoteMilestoneLimit.none());
out.put(vm.getId(), vm);
}
private static void compileCumulative(ConfigurationSection root, Map<String, VoteMilestone> out,
boolean onlyOneCumulative) {
VoteMilestoneGroupSelect groupSelect = onlyOneCumulative ? VoteMilestoneGroupSelect.HIGHEST
: VoteMilestoneGroupSelect.ALL;
ConfigurationSection sec = root.getConfigurationSection("Cumulative");
if (sec == null || isDisabledObject(sec)) {
return;
}
for (String key : sec.getKeys(false)) {
ConfigurationSection c = sec.getConfigurationSection(key);
if (c == null) {
continue;
}
if (!c.getBoolean("Enabled", true)) {
continue;
}
int every;
try {
every = Integer.parseInt(key.replace("'", "").trim());
} catch (Exception e) {
continue;
}
if (every <= 0) {
continue;
}
VoteMilestoneTotal total = mapTotalToUse(c.getString("TotalToUse", "AllTime"));
// rewards are under Cumulative.<key>.Rewards in legacy config
VoteMilestone vm = VoteMilestoneFactory.every("Legacy_Cumulative_" + every + "_" + total.name(), true,
total, every, "Cumulative." + key + ".Rewards", groupSelect, "Legacy_Cumulative",
VoteMilestoneLimit.none());
out.put(vm.getId(), vm);
}
}
private static void compileMilestones(ConfigurationSection root, Map<String, VoteMilestone> out,
boolean resetMonthly) {
ConfigurationSection sec = root.getConfigurationSection("MileStones");
if (sec == null || isDisabledObject(sec)) {
return;
}
for (String key : sec.getKeys(false)) {
ConfigurationSection m = sec.getConfigurationSection(key);
if (m == null) {
continue;
}
if (!m.getBoolean("Enabled", true)) {
continue;
}
long at;
try {
at = Long.parseLong(key.replace("'", "").trim());
} catch (Exception e) {
continue;
}
if (at <= 0) {
continue;
}
// if resetMonthy is true use month total
VoteMilestoneTotal totalToUse = resetMonthly ? VoteMilestoneTotal.MONTHLY_VOTES
: VoteMilestoneTotal.ALLTIME_VOTES;
// rewards are under MileStones.<key>.Rewards in legacy config
VoteMilestone vm = VoteMilestoneFactory.atExact("Legacy_Milestone_" + at + "_ALLTIME", true, totalToUse, at,
"MileStones." + key + ".Rewards", VoteMilestoneGroupSelect.ALL, "Default",
VoteMilestoneLimit.none());
out.put(vm.getId(), vm);
}
}
private static void compileAllSites(ConfigurationSection root, Map<String, VoteMilestone> out, int totalSites) {
String name = "AllSites";
// Trigger when "unique sites voted today" reaches required number of sites
long at = Math.max(1, totalSites);
// IMPORTANT: ALLSITES_TODAY stays true after you hit it, so legacy AllSites
// must be once-per-day
VoteMilestoneLimit limit = new VoteMilestoneLimit(VoteMilestoneLimit.Type.WINDOW_DAY, 0L);
VoteMilestone vm = VoteMilestoneFactory.atExact("Legacy_" + name, true, VoteMilestoneTotal.ALLSITES_TODAY, at,
name, VoteMilestoneGroupSelect.ALL, "Default", limit);
out.put(vm.getId(), vm);
}
private static void compileAlmostAllSites(ConfigurationSection root, Map<String, VoteMilestone> out,
int totalSites) {
String name = "AlmostAllSites";
// "almost all" = totalSites - 1 (minimum 1)
long at = Math.max(1, totalSites - 1);
// Same issue as AllSites: once reached for the day, it would keep matching
// without a limiter
VoteMilestoneLimit limit = new VoteMilestoneLimit(VoteMilestoneLimit.Type.WINDOW_DAY, 0L);
VoteMilestone vm = VoteMilestoneFactory.atExact("Legacy_" + name, true, VoteMilestoneTotal.ALLSITES_TODAY, at,
name, VoteMilestoneGroupSelect.ALL, "Default", limit);
out.put(vm.getId(), vm);
}
private static VoteMilestoneTotal mapTotalToUse(String s) {
String v = (s == null ? "ALLTIME" : s).trim().toUpperCase();
switch (v) {
case "ALLTIME":
case "ALL_TIME":
return VoteMilestoneTotal.ALLTIME_VOTES;
case "DAILY":
case "DAY":
return VoteMilestoneTotal.DAILY_VOTES;
case "WEEKLY":
case "WEEK":
return VoteMilestoneTotal.WEEKLY_VOTES;
case "MONTHLY":
case "MONTH":
return VoteMilestoneTotal.MONTHLY_VOTES;
default:
return VoteMilestoneTotal.ALLTIME_VOTES;
}
}
private static boolean isDisabledObject(ConfigurationSection sec) {
return sec.getKeys(false).isEmpty();
}
}
@@ -1,58 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votestreak;
import lombok.Getter;
public final class VoteStreakDefinition {
@Getter
private final String id;
@Getter
private final VoteStreakType type;
@Getter
private final boolean enabled;
@Getter
private final int requiredAmount;
@Getter
private final int allowMissedAmount;
@Getter
private final int allowMissedPeriod;
@Getter
private final int votesRequired;
@Getter
private final boolean recurring;
@Getter
private final String progressGroup;
@Getter
private final String rewardPath;
public VoteStreakDefinition(String id, VoteStreakType type, boolean enabled, int requiredAmount, int votesRequired,
int allowMissedAmount, int allowMissedPeriod, boolean recurring) {
this(id, type, enabled, requiredAmount, votesRequired, allowMissedAmount, allowMissedPeriod, recurring, "",
"VoteStreaks." + id + ".Rewards");
}
public VoteStreakDefinition(String id, VoteStreakType type, boolean enabled, int requiredAmount, int votesRequired,
int allowMissedAmount, int allowMissedPeriod, boolean recurring, String progressGroup) {
this(id, type, enabled, requiredAmount, votesRequired, allowMissedAmount, allowMissedPeriod, recurring,
progressGroup, "VoteStreaks." + id + ".Rewards");
}
public VoteStreakDefinition(String id, VoteStreakType type, boolean enabled, int requiredAmount, int votesRequired,
int allowMissedAmount, int allowMissedPeriod, boolean recurring, String progressGroup, String rewardPath) {
this.id = id;
this.type = type;
this.enabled = enabled;
this.requiredAmount = requiredAmount;
this.allowMissedAmount = Math.max(0, allowMissedAmount);
this.allowMissedPeriod = Math.max(0, allowMissedPeriod);
this.votesRequired = Math.max(1, votesRequired);
this.recurring = recurring;
this.progressGroup = progressGroup == null ? "" : progressGroup.trim();
this.rewardPath = rewardPath == null || rewardPath.trim().isEmpty() ? "VoteStreaks." + id + ".Rewards"
: rewardPath.trim();
}
}
@@ -1,13 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votestreak;
public enum VoteStreakType {
DAILY, WEEKLY, MONTHLY;
public static VoteStreakType from(String s) {
try {
return VoteStreakType.valueOf(s.trim().toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException("Invalid VoteStreak Type: " + s + " (expected DAILY/WEEKLY/MONTHLY)");
}
}
}
@@ -1,47 +0,0 @@
package com.bencodez.votingplugin.votelog.listeners;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.events.PlayerPostVoteEvent;
import com.bencodez.votingplugin.votelog.VoteLogMysqlTable.VoteLogStatus;
/**
* Listener for logging player post-vote events.
*/
public class PlayerPostVoteLoggerListener implements Listener {
private VotingPluginMain plugin;
/**
* Instantiates a new votifer event.
*
* @param plugin the plugin
*/
public PlayerPostVoteLoggerListener(VotingPluginMain plugin) {
this.plugin = plugin;
}
/**
* On votifer event.
*
* @param event the event
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onplayerVote(PlayerPostVoteEvent event) {
if (!plugin.isEnabled()) {
return;
}
VoteLogStatus status = VoteLogStatus.IMMEDIATE;
if (event.isCached()) {
status = VoteLogStatus.CACHED;
}
if (plugin.getConfigFile().isVoteLoggingEnabled() && !event.isBungee()) {
plugin.getVoteLogMysqlTable().logVote(event.getVoteUUID(), status, event.getService(),
event.getUuid().toString(), event.getPlayerName(), event.getVoteTime(),
event.getUser().getOfflineVotes().size());
}
}
}
@@ -1,61 +0,0 @@
package com.bencodez.votingplugin.votelog.listeners;
import java.time.LocalDateTime;
import java.time.ZoneId;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.events.PlayerSpecialRewardEvent;
/**
* Listener for logging player special reward events.
*/
public class PlayerSpecialRewardLoggerListener implements Listener {
private VotingPluginMain plugin;
/**
* Instantiates a new votifer event.
*
* @param plugin the plugin
*/
public PlayerSpecialRewardLoggerListener(VotingPluginMain plugin) {
this.plugin = plugin;
}
/**
* On votifer event.
*
* @param event the event
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onplayerVote(PlayerSpecialRewardEvent event) {
if (!plugin.isEnabled()) {
return;
}
if (plugin.getConfigFile().isVoteLoggingEnabled()) {
switch (event.getType()) {
case VOTESTREAK:
plugin.getVoteLogMysqlTable().logVoteStreakReward(event.getVoteUUID(), event.getUser().getUUID(),
event.getUser().getPlayerName(),
LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
event.getType().toString());
break;
case TOPVOTER:
plugin.getVoteLogMysqlTable().logTopVoterReward(event.getVoteUUID(), event.getUser().getUUID(),
event.getUser().getPlayerName(),
LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
event.getType().toString());
break;
default:
break;
}
}
}
}
@@ -1,65 +0,0 @@
package com.bencodez.votingplugin.votelog.listeners;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.events.VoteMilestoneRewardEvent;
import com.bencodez.votingplugin.specialrewards.votemilestones.VoteMilestone;
/**
* Logs VoteMilestone reward executions to the VoteLog table.
*/
public class VoteMilestoneVoteLogListener implements Listener {
private final VotingPluginMain plugin;
/**
* Constructs a new VoteMilestoneVoteLogListener.
*
* @param plugin the main plugin instance
*/
public VoteMilestoneVoteLogListener(VotingPluginMain plugin) {
this.plugin = plugin;
}
/**
* Handles vote milestone reward events and logs them.
*
* @param e the vote milestone reward event
*/
@EventHandler
public void onVoteMilestoneReward(VoteMilestoneRewardEvent e) {
if (e == null || e.getUser() == null || e.getMilestone() == null) {
return;
}
if (plugin == null || !plugin.isEnabled()) {
return;
}
if (plugin.getVoteLogMysqlTable() == null) {
return;
}
VoteMilestone m = e.getMilestone();
String milestoneId = safe(e.getMilestoneId());
String groupId = safe(e.getGroupId());
String totalType = m.getTotal() == null ? "null" : m.getTotal().name();
long value = e.getValue();
long timeMillis = System.currentTimeMillis();
try {
plugin.getVoteLogMysqlTable().logVoteMilestoneReward(e.getVoteUUID(),
e.getUuid() == null ? null : e.getUuid().toString(), e.getPlayerName(), timeMillis, milestoneId,
groupId, totalType, value);
} catch (Exception ex) {
plugin.debug("[VoteLog] Failed to log VoteMilestone reward: " + ex.getClass().getSimpleName() + ":"
+ (ex.getMessage() == null ? "" : ex.getMessage()));
}
}
private static String safe(String s) {
return s == null ? "" : s.replace('\n', ' ').replace('\r', ' ').trim();
}
}
@@ -1,35 +0,0 @@
package com.bencodez.votingplugin.votelog.listeners;
import java.time.LocalDateTime;
import java.time.ZoneId;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.events.VoteShopPurchaseEvent;
public class VoteShopPurchaseLoggerListener implements Listener {
private final VotingPluginMain plugin;
public VoteShopPurchaseLoggerListener(VotingPluginMain plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onVoteShopPurchase(VoteShopPurchaseEvent event) {
if (!plugin.isEnabled()) {
return;
}
if (!plugin.getConfigFile().isVoteLoggingEnabled()) {
return;
}
plugin.getVoteLogMysqlTable().logVoteShopPurchase(event.getPlayerUuid().toString(), event.getPlayerName(),
LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), event.getIdentifier(),
event.getCost());
}
}
Loaded 100 of 310 files, more files were not shown because too many files have changed in this diff. Show more