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
278 changed files with 12018 additions and 28955 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 17
uses: actions/setup-java@v4
with:
java-version: '17'
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: 17
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/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>
-681
View File
@@ -1,681 +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.0</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>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.compiler.release>8</maven.compiler.release>
</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.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>1.18.20.0</version>
<executions>
<execution>
<id>delombok</id>
<phase>deploy</phase>
<goals>
<goal>delombok</goal>
</goals>
<configuration>
<addOutputDirectory>false</addOutputDirectory>
<outputDirectory>${project.basedir}/target/delombok</outputDirectory>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.1</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>1.21.11-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>3.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-velocity</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.bencodez</groupId>
<artifactId>advancedcore</artifactId>
<version>3.8</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.15.2</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.1.2</version>
</dependency>
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>configurate-yaml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>configurate-gson</artifactId>
<version>4.1.2</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}</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>install</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<sourceFile>
${project.basedir}/target/${project.name}.jar</sourceFile>
<destinationFile>
${project.basedir}/target/${project.name}-${build.number}.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>
<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,700 +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;
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;
public BungeeHandler(VotingPluginMain plugin) {
this.plugin = plugin;
}
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);
}
}
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;
}
public boolean checkGlobalDataTimeValue(DataValue data) {
if (data.isBoolean()) {
return data.getBoolean();
}
return Boolean.valueOf(data.getString());
}
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();
}
}
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);
VotingPluginUser user = plugin.getVotingPluginUserManager()
.getVotingPluginUser(UUID.fromString(playerUuid));
user.cache();
user.offVote();
// 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);
// 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.getPlaceholders().onBungeeVotePartyUpdate();
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;
}
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,102 +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;
public BroadcastFormat(String broadcastMsg, String header, String listLine) {
this.broadcastMsg = broadcastMsg == null ? "" : broadcastMsg;
this.header = header == null ? "" : header;
this.listLine = listLine == null ? "" : listLine;
}
public List<String> render(String playerName, List<String> items, String reason) {
return render(playerName, items, reason, Collections.<String, String>emptyMap());
}
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,430 +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;
public BroadcastHandler(VotingPluginMain plugin, BroadcastSettings settings, ZoneId zoneId) {
this.plugin = plugin;
this.settings = settings;
this.zoneId = zoneId == null ? ZoneId.systemDefault() : zoneId;
rescheduleIntervalIfNeeded();
}
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)
*/
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,76 +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;
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;
}
public VoteBroadcastType getType() {
return type;
}
public ParsedDuration getDuration() {
return duration;
}
public int getMaxSitesListed() {
return maxSitesListed;
}
public BroadcastFormat getFormat() {
return format;
}
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: '...'
*/
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,60 +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 {
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;
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,145 +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;
public AdminVoteLogEventSelect(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days) {
this(plugin, sender, table, user, days, 0);
}
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,85 +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;
public class AdminVoteLogHelpers {
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;
}
}
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";
}
}
public static String safe(String s) {
return s == null ? "" : s;
}
public static boolean notEmpty(String s) {
return s != null && !s.trim().isEmpty();
}
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);
}
}
public static String shortUuid(String uuid) {
if (uuid == null || uuid.isEmpty()) {
return "unknown";
}
return uuid.length() > 8 ? uuid.substring(0, 8) : uuid;
}
}
@@ -1,188 +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;
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;
public AdminVoteLogMenu(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, int days) {
this(plugin, sender, table, null, days);
}
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,170 +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;
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 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,140 +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;
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 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,136 +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;
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 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,222 +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;
public AdminVoteLogRecent(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, int page) {
this(plugin, sender, table, null, page, 0, null);
}
public AdminVoteLogRecent(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int page) {
this(plugin, sender, table, user, page, 0, null);
}
// 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 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,178 +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;
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);
}
}
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 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,137 +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;
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 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,178 +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;
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);
}
}
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 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,137 +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;
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 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,135 +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;
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 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,211 +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;
public AdminVoteLogStats(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table, int days) {
this(plugin, sender, table, null, days, 10, 10);
}
public AdminVoteLogStats(VotingPluginMain plugin, CommandSender sender, VoteLogMysqlTable table,
VotingPluginUser user, int days, int topServicesLimit) {
this(plugin, sender, table, user, days, topServicesLimit, 10);
}
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 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,231 +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;
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);
}
}
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 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,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,368 +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;
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;
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;
}
public void check(UUID uuid) {
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
if (!user.isOnline()) {
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
}
check(user);
}
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();
}
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).
*/
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 + ")"));
}
public void checkEnabled() {
cooldownCheckEnabled = !plugin.getConfigFile().isDisableCoolDownCheck()
&& plugin.getRewardHandler().hasRewards(plugin.getSpecialRewardsConfig().getData(),
"VoteCoolDownEndedReward");
}
public void checkPerSite(UUID uuid) {
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
if (!user.isOnline()) {
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
}
checkPerSite(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);
}
}
public void load() {
plugin.addUserStartup(new UserStartup() {
@Override
public void onFinish() {
checkAllVoteSites();
}
@Override
public void onStart() {
}
@Override
public void onStartUp(AdvancedCoreUser advancedcoreUser) {
}
});
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onCoolDownEnd(PlayerVoteCoolDownEndEvent event) {
plugin.getRewardHandler().giveReward(event.getPlayer(), plugin.getSpecialRewardsConfig().getData(),
"VoteCoolDownEndedReward", new RewardOptions());
}
@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)));
}
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);
});
}
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);
});
}
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,52 +0,0 @@
package com.bencodez.votingplugin.events;
import lombok.Getter;
public enum SpecialRewardType {
@Deprecated
ALLSITE, @Deprecated
CUMMULATIVE, @Deprecated
FIRSTVOTE, @Deprecated
FIRSTVOTETODAY, @Deprecated
MILESTONE, VOTESTREAK, @Deprecated
ALMOSTALLSITES, TOPVOTER, VOTESTREAKS;
@Getter
private int amount = -1;
@Getter
private String type = "";
public SpecialRewardType setAmount(int num) {
amount = num;
return this;
}
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,109 +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));
}
}
public VotingPluginUser getUser() {
return user;
}
public UUID getUuid() {
return uuid;
}
public String getPlayerName() {
return playerName;
}
public VoteMilestone getMilestone() {
return milestone;
}
public String getMilestoneId() {
return milestone == null ? null : milestone.getId();
}
public long getValue() {
return value;
}
public String getGroupId() {
return groupId;
}
public String getRewardPath() {
return rewardPath;
}
public Map<String, String> getPlaceholders() {
return placeholders;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
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,122 +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()));
}
}
@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,58 +0,0 @@
package com.bencodez.votingplugin.proxy;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
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;
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;
}
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,484 +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;
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
public abstract void debug(SQLException e);
// ---- Constructors ----
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).
*/
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 ----
public String getName() {
return name;
}
/**
* Keep old method name and behavior.
*/
public Set<String> getUuids() {
return getPrimaryKeys();
}
/**
* Keep old behavior: query uuids fresh.
*/
public ArrayList<String> getUuidsQuery() {
return new ArrayList<>(getPrimaryKeysQuery());
}
public void loadData() {
// old name -> new behavior
loadBasicCaches();
}
public void clearCache() {
clearCacheBasic();
}
public void clearCacheBasic() {
clearCaches();
}
public void shutdown() {
close();
}
// Keep old containsKeyQuery signature
public boolean containsKeyQuery(String index) {
return super.containsKeyQuery(index);
}
// ---- Required old utility methods (kept) ----
public ArrayList<String> getColumnsQueury() {
return new ArrayList<>(getColumnsQuery());
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
public boolean isIntColumn(String key) {
return intColumns.contains(key);
}
// ---- INSERT/UPDATE/WIPE (kept similar to your existing behavior) ----
public void insert(String index, String column, DataValue value) {
insertQuery(index, Arrays.asList(new Column(column, value)));
}
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);
}
}
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());
}
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);
}
}
}
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,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,20 +0,0 @@
package com.bencodez.votingplugin.proxy.broadcast;
import java.util.Locale;
public enum OfflineMode {
NONE, QUEUE, FORWARD;
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,150 +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;
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)
*/
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?"
*/
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,57 +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;
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;
}
}
public static ScopeMode parse(String value) {
return parse(value, ALL_SERVERS);
}
}
@@ -1,47 +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;
public class BungeeJsonNonVotedPlayersCache extends BungeeJsonFile
implements INonVotedPlayersStorage {
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,359 +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;
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;";
}
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);
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();
}
public ProxyOnlineVoteCacheTable(MysqlConfig config, boolean debug) {
super("votingplugin_onlinevotecache", config, debug);
alterColumnType("uuid", bestUuidType());
addVoteIdColumnIfMissingOnce();
ensureIndexesOnce();
}
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 ---
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 ---
public List<VoteRow> getAllVotes() {
return selectVotes("SELECT * FROM " + qi(getTableName()) + ";", null);
}
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 ---
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);
}
}
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);
}
}
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;
}
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;
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;
}
public int getId() {
return id;
}
public String getVoteId() {
return voteId;
}
public String getUuid() {
return uuid;
}
public String getPlayerName() {
return playerName;
}
public String getService() {
return service;
}
public long getTime() {
return time;
}
public boolean isRealVote() {
return realVote;
}
public String getText() {
return text;
}
}
}
@@ -1,191 +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;
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);
public ProxyTimedVoteCacheTable(MySQL existingMysql, String tablePrefix, boolean debug) {
super((tablePrefix != null ? tablePrefix : "") + "votingplugin_timedvotecache",
existingMysql,
debug);
ensureIndexes();
}
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 ---
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 ---
public List<TimedVoteRow> getAllVotes() {
return selectVotes("SELECT * FROM " + qi(getTableName()) + ";", null);
}
public List<TimedVoteRow> getExpiredVotes(long now) {
return selectVotes("SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("time") + " <= ?;",
new Object[] { now });
}
// --- DELETE ---
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);
}
}
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);
}
}
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;
}
public static class TimedVoteRow {
private final int id;
private final String playerName;
private final String service;
private final long time;
public TimedVoteRow(int id, String playerName, String service, long time) {
this.id = id;
this.playerName = playerName;
this.service = service;
this.time = time;
}
public int getId() {
return id;
}
public String getPlayerName() {
return playerName;
}
public String getService() {
return service;
}
public long getTime() {
return time;
}
}
}
@@ -1,453 +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();
}
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 ----
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();
}
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 ----
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 ----
public List<VoteRow> getAllVotes() {
return selectVotes("SELECT * FROM " + qi(getTableName()) + ";", null);
}
public List<VoteRow> getVotesForServer(String server) {
return selectVotes("SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("server") + " = ?;",
new Object[] { server });
}
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);
}
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);
}
public List<VoteRow> getVotesByPlayerName(String playerName) {
return selectVotes("SELECT * FROM " + qi(getTableName()) + " WHERE " + qi("playerName") + " = ?;",
new Object[] { playerName });
}
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 ----
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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;
}
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;
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;
}
public int getId() {
return id;
}
public String getVoteId() {
return voteId;
}
public String getUuid() {
return uuid;
}
public String getPlayerName() {
return playerName;
}
public String getService() {
return service;
}
public long getTime() {
return time;
}
public boolean isRealVote() {
return realVote;
}
public String getText() {
return text;
}
public String getServer() {
return server;
}
}
}
@@ -1,45 +0,0 @@
package com.bencodez.votingplugin.proxy.cache.nonvoted;
import java.util.function.Consumer;
public interface INonVotedPlayersStorage {
/** Insert or update a player entry. */
void upsertPlayer(String uuid, String playerName, long lastTime);
/** Returns UUID for the player or empty string if not found. */
String getUuidByPlayerName(String playerName);
/** Remove a player from storage. */
void removeByPlayerName(String playerName);
/** Iterate snapshot of all entries. */
void forEach(Consumer<NonVotedPlayerEntry> consumer);
/** Close resources if needed (MySQL); no-op for JSON. */
void close();
final class NonVotedPlayerEntry {
private final String playerName;
private final String uuid;
private final long lastTime;
public NonVotedPlayerEntry(String playerName, String uuid, long lastTime) {
this.playerName = playerName;
this.uuid = uuid;
this.lastTime = lastTime;
}
public String getPlayerName() {
return playerName;
}
public String getUuid() {
return uuid;
}
public long getLastTime() {
return lastTime;
}
}
}
@@ -1,56 +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;
public abstract class MySQLNonVotedPlayersStorage extends ProxyNonVotedPlayersTable
implements INonVotedPlayersStorage {
private final boolean ownsConnection;
public MySQLNonVotedPlayersStorage(MySQL existingMysql, String tablePrefix, boolean debug) {
super(existingMysql, tablePrefix, debug);
this.ownsConnection = false;
}
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,132 +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;
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);
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;
}
}
public abstract boolean userExists(String uuid);
/**
* Add a player directly by uuid + name, only if they don't already have votes.
*/
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());
}
}
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.
*/
public String getUUID(String playerName) {
return storage.getUuidByPlayerName(playerName);
}
public void close() {
storage.close();
}
public abstract void logInfo1(String msg);
public abstract void logSevere1(String msg);
public abstract void debug1(Exception e);
public abstract void debug1(String msg);
}
@@ -1,184 +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;
public abstract class ProxyNonVotedPlayersTable {
@Getter
private final MySQL mysql;
private final String tableName;
public abstract void logSevere(String msg);
public abstract void logInfo(String msg);
public abstract void debug(Exception e);
public ProxyNonVotedPlayersTable(MySQL existingMysql, String tablePrefix, boolean debug) {
this.mysql = existingMysql;
this.tableName = (tablePrefix != null ? tablePrefix : "") + "votingplugin_nonvotedplayers";
createTableIfNeeded();
}
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);
}
}
public String getTableName() {
return tableName;
}
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);
}
}
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 "";
}
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);
}
}
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;
}
public void clearAll() {
try {
new Query(mysql, "TRUNCATE TABLE `" + tableName + "`;").executeUpdate();
} catch (SQLException e) {
debug(e);
}
}
public static class NonVotedPlayerRow {
private final int id;
private final String uuid;
private final String playerName;
private final long lastTime;
public NonVotedPlayerRow(int id, String uuid, String playerName, long lastTime) {
this.id = id;
this.uuid = uuid;
this.playerName = playerName;
this.lastTime = lastTime;
}
public int getId() {
return id;
}
public String getUuid() {
return uuid;
}
public String getPlayerName() {
return playerName;
}
public long getLastTime() {
return lastTime;
}
}
}
@@ -1,266 +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;
public abstract class MultiProxyHandler {
private HashMap<String, ClientHandler> multiproxyClientHandles;
private SocketHandler multiproxySocketHandler;
@Getter
private RedisHandler multiProxyRedis;
public MultiProxyHandler() {
}
public abstract void addNonVotedPlayerCache(String uuid, String playerName);
public abstract void clearVote(String uuid);
public void close() {
if (multiproxySocketHandler != null) {
multiproxySocketHandler.closeConnection();
multiproxySocketHandler = null;
}
if (multiProxyRedis != null && !getMultiProxyRedisUseExistingConnection()) {
multiProxyRedis.close();
}
}
public abstract boolean getDebug();
public abstract EncryptionHandler getEncryptionHandler();
public abstract MultiProxyMethod getMultiProxyMethod();
public abstract String getMultiProxyPassword();
public abstract String getMultiProxyRedisHost();
public abstract int getMultiProxyRedisPort();
public abstract int getMultiProxyRedisDbIndex();
public abstract boolean getMultiProxyRedisUseExistingConnection();
public abstract String getMultiProxyServerName();
public abstract Collection<String> getMultiProxyServers();
public abstract MultiProxyServerSocketConfiguration getMultiProxyServersConfiguration(String s);
public abstract String getMultiProxySocketHostHost();
public abstract int getMultiProxySocketHostPort();
public abstract boolean getMultiProxySupportEnabled();
public abstract String getMultiProxyUsername();
public abstract File getPluginDataFolder();
public abstract boolean getPrimaryServer();
public abstract List<String> getProxyServers();
public abstract RedisHandler getRedisHandler();
public abstract String getVersion();
public abstract void logInfo(String msg);
public abstract void runAsnc(Runnable runnable);
public abstract void setEncryptionHandler(EncryptionHandler encryptionHandler);
public abstract void triggerVote(String player, String service, boolean realVote, boolean timeQueue, long queueTime,
VoteTotalsSnapshot text, String uuid);
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());
}
public void login(String uuid, String playerName) {
if (!getMultiProxySupportEnabled() || getPrimaryServer()) {
return;
}
sendMultiProxyEnvelope(VotingPluginWire.login(playerName, uuid, getMultiProxyServerName()));
}
public void sendClearVote(String uuid, String playerName) {
if (!getMultiProxySupportEnabled()) {
return;
}
sendMultiProxyEnvelope(VotingPluginWire.clearVote(uuid, playerName, getMultiProxyServerName()));
}
public void sendStatus() {
if (!getMultiProxySupportEnabled()) {
return;
}
sendMultiProxyEnvelope(VotingPluginWire.status(getMultiProxyServerName()));
}
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,56 +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;
public class VelocityJsonNonVotedPlayersCache extends VelocityJSONFile implements INonVotedPlayersStorage {
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() {
}
private void setPath(Object value, Object... path) {
set(path, value);
}
}
@@ -1,226 +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;
public class VelocityJsonVoteCache extends VelocityJSONFile implements IVoteCache {
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);
}
}
}
}
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,385 +0,0 @@
package com.bencodez.votingplugin.servicesites;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
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.
*
* <p>
* Primary runtime storage is {@code serverdata.yml} as a list of strings:
*
* <pre>
* ServiceSites:
* - "PlanetMinecraft - planetminecraft.com"
* - "TopG - topg.org"
* </pre>
*
* <p>
* On startup this class:
* <ol>
* <li>Loads the list from {@code serverdata.yml} into memory.</li>
* <li>Checks the web sources once for updates and overwrites
* {@code serverdata.yml} if changed.</li>
* </ol>
*
* <p>
* Lookups are case-insensitive while preserving original casing.
* </p>
*/
public class ServiceSiteHandler {
/**
* Primary source: GitHub wiki raw markdown (most stable).
*/
private static final String PRIMARY_URL = "https://raw.githubusercontent.com/wiki/BenCodez/VotingPlugin/Minecraft-Server-Lists.md";
/**
* Secondary source: main wiki (HTML).
*/
private static final String SECONDARY_URL = "https://wiki.bencodez.com/en/VotingPlugin/Minecraft-Server-Lists";
/**
* Tertiary source: backup wiki (HTML).
*/
private static final String TERTIARY_URL = "https://wiki-backup.bencodez.com/VotingPlugin/Minecraft-Server-Lists/";
/**
* YAML path used in {@code serverdata.yml}.
*/
private static final String SERVERDATA_PATH = "ServiceSites";
/**
* Matches entries in the format {@code Key - Value}.
*/
private static final Pattern LINE_PATTERN = Pattern.compile("^\\s*(.+?)\\s+-\\s+(.+?)\\s*$");
/**
* Extracts {@code
*
<li>...</li>} content from HTML.
*/
private static final Pattern LI_PATTERN = Pattern.compile("<li>\\s*(.*?)\\s*</li>", Pattern.CASE_INSENSITIVE);
private final VotingPluginMain plugin;
/**
* Case-insensitive lookup map (atomic swap on update).
*/
private final AtomicReference<Map<String, String>> sitesRef = new AtomicReference<>(
Collections.unmodifiableMap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
/**
* Exposed read-only view of current service sites.
*/
@Getter
private Map<String, String> serviceSites = sitesRef.get();
/**
* Creates the handler, loads from {@code serverdata.yml}, then checks the web
* sources once for updates.
*
* <p>
* Important: this performs a blocking web fetch on the current thread. If you
* need this to be async, call {@link #refreshFromWebOnce(boolean)} from your
* own async executor.
* </p>
*
* @param plugin plugin instance
*/
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());
}
}
/**
* Checks whether a service key exists (case-insensitive).
*
* @param service service name
* @return {@code true} if present
*/
public boolean contains(String service) {
return service != null && sitesRef.get().containsKey(service);
}
/**
* Maps a service name to its configured value.
*
* @param service service name
* @return mapped value, or {@code service} if not found
*/
public String match(String service) {
if (service == null) {
return null;
}
return sitesRef.get().getOrDefault(service, service);
}
/**
* Reverse lookup from value to key.
*
* @param value value to reverse lookup
* @return matching key, or {@code value} if not found
*/
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;
}
/**
* Loads service sites from {@code serverdata.yml}.
*/
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");
}
}
/**
* Saves current service sites to {@code serverdata.yml}.
*/
public void saveToServerData() {
List<String> out = toListFormat(sitesRef.get());
plugin.getServerData().getData().set(SERVERDATA_PATH, out);
plugin.getServerData().saveData();
}
/**
* Checks the web sources once and updates {@code serverdata.yml} if changes are
* detected.
*
* <p>
* Tries primary, secondary, then tertiary URL.
* </p>
*
* @param logMore whether to log debug details
*/
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 (Exception e) {
plugin.debug(e);
}
}
/**
* Converts a map into list format used by {@code serverdata.yml}.
*
* @param map map to convert
* @return list of {@code "Key - Value"} entries
*/
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 {
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 {
HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(7000);
conn.setInstanceFollowRedirects(true);
conn.setRequestProperty("User-Agent", "VotingPlugin/ServiceSiteHandler");
int code = conn.getResponseCode();
if (code < 200 || code >= 300) {
conn.disconnect();
throw new IOException("HTTP " + code);
}
String ct = conn.getContentType();
StringBuilder sb = new StringBuilder(16 * 1024);
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append('\n');
}
} finally {
conn.disconnect();
}
return new FetchResult(urlStr, sb.toString(), ct);
}
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();
}
/**
* Web fetch result container.
*/
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,96 +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;
public SpecialRewards(VotingPluginMain plugin) {
this.plugin = plugin;
}
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;
}
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,249 +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;
}
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]"
*/
public String toDebugString() {
return toDebugString(8, 3);
}
public boolean hasZero() {
return hasZero;
}
public Set<Long> getExactTotals() {
return java.util.Collections.unmodifiableSet(exactTotals);
}
/** Returns a compact string for logs. */
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();
}
/**
* @param head how many smallest values to show
* @param tail how many largest values to show when truncating
*/
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();
}
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,93 +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 {
public final long start;
public final long end;
public final long step;
public RangeSpec(long start, long end, long step) {
this.start = start;
this.end = end;
this.step = step;
}
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);
}
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,70 +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;
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);
}
public boolean hasAtMatcherDebug() {
return atMatcher != null;
}
public String getAtMatcherDebugString(int maxValues) {
return atMatcher == null ? "none" : atMatcher.toDebugString(maxValues);
}
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,106 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
import java.util.HashMap;
import java.util.Map;
import org.bukkit.configuration.ConfigurationSection;
public final class VoteMilestoneGroupParser {
private VoteMilestoneGroupParser() {
}
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,19 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votemilestones;
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,175 +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 {
public enum Type {
NONE, WINDOW_DAY, WINDOW_WEEK, WINDOW_MONTH, COOLDOWN
}
private final Type type;
private final long cooldownMillis; // fixed millis cooldown
public VoteMilestoneLimit(Type type, long cooldownMillis) {
this.type = type == null ? Type.NONE : type;
this.cooldownMillis = Math.max(0L, cooldownMillis);
}
public static VoteMilestoneLimit none() {
return new VoteMilestoneLimit(Type.NONE, 0L);
}
public Type getType() {
return type;
}
public long getCooldownMillis() {
return cooldownMillis;
}
public boolean isEnabled() {
return type != Type.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 ---------------- */
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;
}
}
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,140 +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) ----
ALLTIME_VOTES {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getAllTimeTotal();
}
return user.getTotal(TopVoter.AllTime);
}
},
DAILY_VOTES {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getDailyTotal();
}
return user.getTotal(TopVoter.Daily);
}
},
WEEKLY_VOTES {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getWeeklyTotal();
}
return user.getTotal(TopVoter.Weekly);
}
},
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) ----
POINTS {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
if (bungeeMessageData != null) {
return bungeeMessageData.getPoints();
}
return user.getPoints();
}
},
ALLSITES_TODAY {
@Override
public long getValue(VotingPluginUser user, VoteTotalsSnapshot bungeeMessageData) {
return user.getUniqueVoteSitesToday();
}
};
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
*/
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,74 +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;
public class VoteMilestonesConfig {
private final Map<String, VoteMilestone> milestones;
private VoteMilestonesConfig(Map<String, VoteMilestone> milestones) {
this.milestones = milestones;
}
public Map<String, VoteMilestone> getMilestones() {
return milestones;
}
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,196 +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() {
}
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,35 +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;
public VoteStreakDefinition(String id, VoteStreakType type, boolean enabled, int requiredAmount, int votesRequired,
int allowMissedAmount, int allowMissedPeriod) {
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);
}
}
@@ -1,592 +0,0 @@
package com.bencodez.votingplugin.specialrewards.votestreak;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import com.bencodez.advancedcore.api.rewards.RewardBuilder;
import com.bencodez.advancedcore.api.time.TimeChecker;
import com.bencodez.advancedcore.api.user.usercache.keys.UserDataKeyString;
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;
import lombok.Getter;
public class VoteStreakHandler {
private final VotingPluginMain plugin;
@Getter
private final Map<String, VoteStreakDefinition> byId = new LinkedHashMap<>();
private final List<VoteStreakDefinition> ordered = new ArrayList<>();
public VoteStreakHandler(VotingPluginMain plugin) {
this.plugin = plugin;
}
/**
* Reload streak definitions from config.
*/
public void reload() {
byId.clear();
ordered.clear();
// VoteStreak definitions live in specialrewards.yml now (Option B)
ConfigurationSection root = plugin.getSpecialRewardsConfig().getData();
new VoteStreakConfigLoader().load(root);
}
public List<VoteStreakDefinition> getDefinitions() {
return Collections.unmodifiableList(ordered);
}
public VoteStreakDefinition getDefinition(String id) {
if (id == null) {
return null;
}
return byId.get(id.toLowerCase(Locale.ROOT));
}
/**
* Call this whenever a vote happens
*
* @param user voting plugin user
* @param voteTimeMillis vote time
* @param voteUUID vote unique id
*/
public void processVote(VotingPluginUser user, long voteTimeMillis, UUID voteUUID) {
if (user == null) {
plugin.extraDebug("[VoteStreak] processVote: user is null, returning");
return;
}
if (ordered.isEmpty()) {
plugin.extraDebug("[VoteStreak] processVote: ordered is empty (no definitions loaded), returning");
return;
}
plugin.extraDebug("[VoteStreak] processVote: user=" + safeUser(user) + " timeMillis=" + voteTimeMillis
+ " defs=" + ordered.size());
for (VoteStreakDefinition def : ordered) {
if (!def.isEnabled()) {
plugin.extraDebug(
"[VoteStreak] processVote: skip disabled def id=" + def.getId() + " idKey=" + def.getId());
continue;
}
try {
processVoteForDefinition(user, def, voteTimeMillis, voteUUID);
} catch (Exception e) {
plugin.getLogger().warning("VoteStreak processing failed for '" + def.getId() + "': " + e.getMessage());
plugin.debug(e);
}
}
}
private void processVoteForDefinition(VotingPluginUser user, VoteStreakDefinition def, long voteTimeMillis,
UUID voteUUID) {
final String col = getColumnName(def);
final String rawBefore = readStateString(user, col);
StreakState state = StreakState.deserialize(rawBefore);
final String currentPeriodKey = periodKey(def.getType(), voteTimeMillis);
plugin.extraDebug("[VoteStreak] def=" + def.getId() + " idKey=" + def.getId() + " type=" + def.getType()
+ " col=" + col + " period=" + currentPeriodKey + " votesReq=" + def.getVotesRequired() + " interval="
+ def.getRequiredAmount() + " rawBefore='" + rawBefore + "' stateBefore={periodKey=" + state.periodKey
+ ",streakCount=" + state.streakCount + ",votesThisPeriod=" + state.votesThisPeriod
+ ",countedThisPeriod=" + state.countedThisPeriod + ",missesUsed=" + state.missesUsed
+ ",missWindowStartKey=" + state.missWindowStartKey + "}");
// First time init
if (state.periodKey == null || state.periodKey.isEmpty()) {
plugin.extraDebug("[VoteStreak] init state (no prior periodKey)");
state.periodKey = currentPeriodKey;
state.countedThisPeriod = false;
state.votesThisPeriod = 0;
state.missWindowStartKey = "";
state.missesUsed = 0;
} else if (!state.periodKey.equals(currentPeriodKey)) {
plugin.extraDebug("[VoteStreak] period advanced from " + state.periodKey + " -> " + currentPeriodKey);
advancePeriodsAndApplyMisses(def, state, voteTimeMillis, currentPeriodKey);
plugin.extraDebug("[VoteStreak] after advance state={periodKey=" + state.periodKey + ",streakCount="
+ state.streakCount + ",votesThisPeriod=" + state.votesThisPeriod + ",countedThisPeriod="
+ state.countedThisPeriod + ",missesUsed=" + state.missesUsed + ",missWindowStartKey="
+ state.missWindowStartKey + "}");
}
// Count votes within the period until threshold reached, then mark period
// satisfied once.
if (!state.countedThisPeriod) {
state.votesThisPeriod++;
int votesRequired = Math.max(1, def.getVotesRequired());
plugin.extraDebug("[VoteStreak] vote counted in period: votesThisPeriod=" + state.votesThisPeriod + "/"
+ votesRequired);
if (state.votesThisPeriod >= votesRequired) {
state.countedThisPeriod = true;
state.streakCount++;
int interval = Math.max(1, def.getRequiredAmount());
boolean shouldReward = state.streakCount > 0 && (state.streakCount % interval) == 0;
plugin.extraDebug("[VoteStreak] period satisfied: streakCount=" + state.streakCount + " interval="
+ interval + " shouldReward=" + shouldReward);
if (shouldReward) {
plugin.extraDebug("[VoteStreak] giving rewards for idKey=" + def.getId());
giveRewards(user, def, voteUUID);
}
}
} else {
plugin.extraDebug("[VoteStreak] already satisfied this period; ignoring vote. votesThisPeriod="
+ state.votesThisPeriod);
}
// Persist + verify
final String rawAfter = state.serialize();
plugin.extraDebug("[VoteStreak] writing col=" + col + " rawAfter='" + rawAfter + "'");
writeStateString(user, col, rawAfter);
final String readBack = readStateString(user, col);
plugin.extraDebug("[VoteStreak] read-back col=" + col + " raw='" + readBack + "' (writtenLen="
+ rawAfter.length() + " readLen=" + readBack.length() + ")");
}
/**
* When periods change, we determine if previous periods were misses.
*
* @param def vote streak definition
* @param state current streak state
* @param nowMillis current time millis
* @param currentPeriodKey current period key
*/
private void advancePeriodsAndApplyMisses(VoteStreakDefinition def, StreakState state, long nowMillis,
String currentPeriodKey) {
if (!state.countedThisPeriod) {
plugin.extraDebug("[VoteStreak] advance: previous period " + state.periodKey + " NOT satisfied -> miss");
recordMiss(def, state, nowMillis);
}
int skipped = periodsBetween(def.getType(), state.periodKey, currentPeriodKey);
plugin.extraDebug(
"[VoteStreak] advance: skipped=" + skipped + " between " + state.periodKey + " -> " + currentPeriodKey);
if (skipped > 1) {
for (int i = 0; i < skipped - 1; i++) {
plugin.extraDebug(
"[VoteStreak] advance: recording intermediate missed period " + (i + 1) + "/" + (skipped - 1));
recordMiss(def, state, nowMillis);
}
}
state.periodKey = currentPeriodKey;
state.countedThisPeriod = false;
state.votesThisPeriod = 0;
if (def.getAllowMissedAmount() <= 0) {
if (state.missesUsed > 0) {
plugin.extraDebug(
"[VoteStreak] advance: no misses allowed and missesUsed=" + state.missesUsed + " -> reset");
resetStreakAndMissTracking(state);
}
} else if (state.missesUsed > def.getAllowMissedAmount()) {
plugin.extraDebug("[VoteStreak] advance: missesUsed=" + state.missesUsed + " > allowMissedAmount="
+ def.getAllowMissedAmount() + " -> reset");
resetStreakAndMissTracking(state);
}
}
private void recordMiss(VoteStreakDefinition def, StreakState state, long nowMillis) {
if (def.getAllowMissedPeriod() <= 0) {
if (state.missWindowStartKey == null || state.missWindowStartKey.isEmpty()) {
state.missWindowStartKey = periodKey(def.getType(), nowMillis);
state.missesUsed = 1;
plugin.extraDebug("[VoteStreak] recordMiss: start window=" + state.missWindowStartKey
+ " missesUsed=1 (no window)");
} else {
state.missesUsed++;
plugin.extraDebug("[VoteStreak] recordMiss: missesUsed=" + state.missesUsed + " (no window)");
}
return;
}
String nowKey = periodKey(def.getType(), nowMillis);
if (state.missWindowStartKey == null || state.missWindowStartKey.isEmpty()) {
state.missWindowStartKey = nowKey;
state.missesUsed = 1;
plugin.extraDebug("[VoteStreak] recordMiss: start window=" + nowKey + " missesUsed=1");
return;
}
int windowSpan = periodsBetween(def.getType(), state.missWindowStartKey, nowKey);
if (windowSpan >= def.getAllowMissedPeriod()) {
plugin.extraDebug("[VoteStreak] recordMiss: window expired span=" + windowSpan + " >= allowMissedPeriod="
+ def.getAllowMissedPeriod() + " -> reset window to " + nowKey);
state.missWindowStartKey = nowKey;
state.missesUsed = 1;
} else {
state.missesUsed++;
plugin.extraDebug(
"[VoteStreak] recordMiss: within window span=" + windowSpan + " missesUsed=" + state.missesUsed);
}
}
private void resetStreakAndMissTracking(StreakState state) {
plugin.extraDebug("[VoteStreak] resetStreakAndMissTracking: reset streakCount/misses/votesThisPeriod");
state.streakCount = 0;
state.missesUsed = 0;
state.missWindowStartKey = "";
state.votesThisPeriod = 0;
}
private LocalDate getPluginDateFromMillis(long millis) {
TimeChecker tc = plugin.getTimeChecker();
LocalDateTime ldt = tryInvokeLocalDateTime(tc, "getTime", millis);
if (ldt != null) {
return ldt.toLocalDate();
}
ldt = tryInvokeLocalDateTime(tc, "getTimeFromMillis", millis);
if (ldt != null) {
return ldt.toLocalDate();
}
ldt = tryInvokeLocalDateTime(tc, "getTimeAt", millis);
if (ldt != null) {
return ldt.toLocalDate();
}
LocalDateTime pluginNow = tc.getTime();
long nowMillis = System.currentTimeMillis();
long deltaMillis = millis - nowMillis;
return pluginNow.plusNanos(deltaMillis * 1_000_000L).toLocalDate();
}
private LocalDateTime tryInvokeLocalDateTime(Object target, String method, long millis) {
try {
Method m = target.getClass().getMethod(method, long.class);
Object out = m.invoke(target, millis);
if (out instanceof LocalDateTime) {
return (LocalDateTime) out;
}
} catch (Exception ignored) {
}
return null;
}
private String periodKey(VoteStreakType type, long millis) {
switch (type) {
case DAILY: {
LocalDate d = getPluginDateFromMillis(millis);
return d.toString();
}
case WEEKLY: {
LocalDate d = getPluginDateFromMillis(millis);
WeekFields wf = WeekFields.ISO;
int week = d.get(wf.weekOfWeekBasedYear());
int year = d.get(wf.weekBasedYear());
return year + "-W" + String.format(Locale.ROOT, "%02d", week);
}
case MONTHLY: {
YearMonth ym = YearMonth.from(getPluginDateFromMillis(millis));
return ym.toString();
}
default:
return String.valueOf(millis);
}
}
private int periodsBetween(VoteStreakType type, String startKey, String endKey) {
if (startKey == null || endKey == null) {
return 0;
}
if (startKey.equals(endKey)) {
return 0;
}
try {
switch (type) {
case DAILY: {
LocalDate a = LocalDate.parse(startKey);
LocalDate b = LocalDate.parse(endKey);
long days = ChronoUnit.DAYS.between(a, b);
return (int) Math.max(0, days);
}
case MONTHLY: {
YearMonth a = YearMonth.parse(startKey);
YearMonth b = YearMonth.parse(endKey);
long months = ChronoUnit.MONTHS.between(a, b);
return (int) Math.max(0, months);
}
case WEEKLY: {
WeekKey a = WeekKey.parse(startKey);
WeekKey b = WeekKey.parse(endKey);
return Math.max(0, b.toIndex() - a.toIndex());
}
default:
return 0;
}
} catch (Exception e) {
return 0;
}
}
private static final class WeekKey {
final int year;
final int week;
private WeekKey(int year, int week) {
this.year = year;
this.week = week;
}
static WeekKey parse(String s) {
String[] parts = s.split("-W");
int y = Integer.parseInt(parts[0]);
int w = Integer.parseInt(parts[1]);
return new WeekKey(y, w);
}
int toIndex() {
return (year * 53) + week;
}
}
private void giveRewards(VotingPluginUser user, VoteStreakDefinition def, UUID voteUUID) {
PlayerSpecialRewardEvent event = new PlayerSpecialRewardEvent(user,
SpecialRewardType.VOTESTREAKS.setType(def.getType().toString()).setAmount(def.getVotesRequired()),
voteUUID);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
return;
}
new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), "VoteStreaks." + def.getId() + ".Rewards")
.withPlaceHolder("id", def.getId()).send(user);
}
public String getColumnName(VoteStreakDefinition def) {
return "VoteStreak_" + def.getId();
}
public StreakState readState(VotingPluginUser user, VoteStreakDefinition def) {
String raw = readStateString(user, getColumnName(def));
return StreakState.deserialize(raw);
}
public void writeState(VotingPluginUser user, VoteStreakDefinition def, StreakState state) {
writeStateString(user, getColumnName(def), state.serialize());
}
public String readStateString(VotingPluginUser user, String columnName) {
String val = user.getVoteStreakState(columnName);
return val == null ? "" : val;
}
private void writeStateString(VotingPluginUser user, String columnName, String value) {
user.setVoteStreakState(columnName, value);
}
private static String safeUser(VotingPluginUser user) {
try {
String n = user.getPlayerName();
if (n != null && !n.isEmpty()) {
return n;
}
} catch (Exception ignored) {
}
try {
return String.valueOf(user.getUUID());
} catch (Exception ignored) {
}
return "unknown";
}
private static final class StreakState {
String periodKey = "";
int streakCount = 0;
int votesThisPeriod = 0;
boolean countedThisPeriod = false;
String missWindowStartKey = "";
int missesUsed = 0;
String serialize() {
return safe(periodKey) + "|" + streakCount + "|" + votesThisPeriod + "|" + countedThisPeriod + "|"
+ safe(missWindowStartKey) + "|" + missesUsed;
}
static StreakState deserialize(String raw) {
StreakState s = new StreakState();
if (raw == null || raw.isEmpty()) {
return s;
}
String[] p = raw.split("\\|", -1);
if (p.length >= 6 && MessageAPI.isInt(p[2])) {
if (p.length > 0)
s.periodKey = p[0];
if (p.length > 1)
s.streakCount = parseInt(p[1], 0);
if (p.length > 2)
s.votesThisPeriod = parseInt(p[2], 0);
if (p.length > 3)
s.countedThisPeriod = parseBool(p[3], false);
if (p.length > 4)
s.missWindowStartKey = p[4];
if (p.length > 5)
s.missesUsed = parseInt(p[5], 0);
} else {
if (p.length > 0)
s.periodKey = p[0];
if (p.length > 1)
s.streakCount = parseInt(p[1], 0);
if (p.length > 2)
s.countedThisPeriod = parseBool(p[2], false);
if (p.length > 3)
s.missWindowStartKey = p[3];
if (p.length > 4)
s.missesUsed = parseInt(p[4], 0);
s.votesThisPeriod = s.countedThisPeriod ? 1 : 0;
}
if (s.periodKey == null)
s.periodKey = "";
if (s.missWindowStartKey == null)
s.missWindowStartKey = "";
if (s.streakCount < 0)
s.streakCount = 0;
if (s.votesThisPeriod < 0)
s.votesThisPeriod = 0;
if (s.missesUsed < 0)
s.missesUsed = 0;
return s;
}
private static String safe(String s) {
return s == null ? "" : s;
}
private static int parseInt(String s, int def) {
if (s == null)
return def;
if (MessageAPI.isInt(s))
return Integer.parseInt(s);
return def;
}
private static boolean parseBool(String s, boolean def) {
if (s == null)
return def;
return Boolean.parseBoolean(s);
}
}
public final class VoteStreakConfigLoader {
private final Pattern idPattern = Pattern.compile("^[A-Za-z0-9_\\-]+$"); // no spaces
public void load(ConfigurationSection root) {
if (root == null) {
plugin.getLogger().warning("VoteStreaks config root is null; no streaks loaded.");
return;
}
ConfigurationSection voteStreaks = root.getConfigurationSection("VoteStreaks");
if (voteStreaks == null) {
plugin.getLogger().warning("VoteStreaks is missing or not a section; no streaks loaded.");
return;
}
int count = 0;
for (String id : voteStreaks.getKeys(false)) {
count++;
if (id == null || id.trim().isEmpty()) {
plugin.getLogger().warning("VoteStreaks entry #" + count + " has empty key; skipping.");
continue;
}
id = id.trim();
if (!idPattern.matcher(id).matches()) {
plugin.getLogger()
.warning("VoteStreaks ID '" + id + "' is invalid (only A-Z, 0-9, _, -). Skipping.");
continue;
}
ConfigurationSection defSec = voteStreaks.getConfigurationSection(id);
if (defSec == null) {
plugin.getLogger().warning("VoteStreaks '" + id + "' is not a section; skipping.");
continue;
}
String typeStr = defSec.getString("Type");
VoteStreakType type = VoteStreakType.from(typeStr);
if (type == null) {
plugin.getLogger()
.warning("VoteStreaks '" + id + "' has invalid Type '" + typeStr + "'; skipping.");
continue;
}
boolean enabled = defSec.getBoolean("Enabled", true);
int amountInterval = 0;
int votesRequired = 1;
ConfigurationSection req = defSec.getConfigurationSection("Requirements");
if (req != null) {
amountInterval = req.getInt("Amount", 0);
votesRequired = req.getInt("VotesRequired", 1);
}
if (amountInterval <= 0) {
plugin.getLogger().warning("VoteStreaks '" + id + "' Requirements.Amount must be > 0; skipping.");
continue;
}
if (votesRequired <= 0) {
votesRequired = 1;
}
int allowMissedAmount = Math.max(0, defSec.getInt("AllowMissedAmount", 0));
int allowMissedPeriod = Math.max(0, defSec.getInt("AllowMissedPeriod", 0));
// ConfigurationSection editableTarget = getOrCreateVoteStreakSection(id);
VoteStreakDefinition def = new VoteStreakDefinition(id, type, enabled, amountInterval, votesRequired,
allowMissedAmount, allowMissedPeriod);
plugin.getUserManager().getDataManager()
.addKey(new UserDataKeyString(getColumnName(def)).setColumnType("MEDIUMTEXT"));
byId.put(id, def);
ordered.add(def);
}
plugin.getLogger().info("Loaded " + ordered.size() + " VoteStreak definitions.");
}
}
}
@@ -1,15 +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,44 +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;
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,58 +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;
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,55 +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;
public VoteMilestoneVoteLogListener(VotingPluginMain plugin) {
this.plugin = plugin;
}
@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());
}
}
@@ -1,118 +0,0 @@
package com.bencodez.votingplugin.votereminding;
import java.io.File;
import java.io.IOException;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import com.bencodez.votingplugin.VotingPluginMain;
public final class VoteRemindersLegacyMigrator {
private VoteRemindersLegacyMigrator() {
}
/**
* Migrates legacy VoteReminding -> VoteReminderOptions + VoteReminders in
* Config.yml.
*
* - NO BACKUPS (per request) - Runs once
* (VoteReminderOptions.MigratedFromLegacy) - Skips if new sections already
* exist
*/
public static boolean migrateIfNeeded(VotingPluginMain plugin, File configYmlFile, FileConfiguration cfg) {
// already migrated
if (cfg.getBoolean("VoteReminderOptions.MigratedFromLegacy", false)) {
return false;
}
// if new system exists, don't touch anything
if (cfg.isConfigurationSection("VoteReminderOptions") || cfg.isConfigurationSection("VoteReminders")) {
return false;
}
ConfigurationSection legacy = cfg.getConfigurationSection("VoteReminding");
if (legacy == null) {
return false;
}
plugin.getLogger().info("[VoteReminders] Migrating legacy VoteReminding from Config.yml...");
// Options
cfg.set("VoteReminderOptions.Enabled", legacy.getBoolean("Enabled", true));
cfg.set("VoteReminderOptions.StopAfterMatch", true);
cfg.set("VoteReminderOptions.GlobalCooldown", "");
cfg.set("VoteReminderOptions.DefaultPriority", 0);
cfg.set("VoteReminderOptions.Defaults.Cooldown", "");
cfg.set("VoteReminderOptions.Defaults.Delay", "");
// Default rewards copy
if (legacy.isConfigurationSection("Rewards")) {
copySection(legacy.getConfigurationSection("Rewards"),
ensureSection(cfg, "VoteReminderOptions.Defaults.Rewards"));
}
// RemindDelay -> Interval reminder
int remindDelayMinutes = legacy.getInt("RemindDelay", 30);
if (remindDelayMinutes > 0) {
String base = "VoteReminders.Basic";
cfg.set(base + ".Type", "INTERVAL");
cfg.set(base + ".Interval", remindDelayMinutes + "m");
cfg.set(base + ".Conditions.CanVoteAny", true);
}
// RemindOnLogin -> Login reminder
if (legacy.getBoolean("RemindOnLogin", true)) {
String base = "VoteReminders.OnLogin";
cfg.set(base + ".Type", "LOGIN");
cfg.set(base + ".Conditions.CanVoteAny", true);
}
// mark migrated
cfg.set("VoteReminderOptions.MigratedFromLegacy", true);
// save Config.yml
try {
cfg.save(configYmlFile);
} catch (IOException e) {
plugin.getLogger().severe("[VoteReminders] Failed saving Config.yml after migration: " + e.getMessage());
}
plugin.getLogger().info("[VoteReminders] Legacy VoteReminding migration complete.");
return true;
}
private static ConfigurationSection ensureSection(FileConfiguration cfg, String path) {
ConfigurationSection sec = cfg.getConfigurationSection(path);
if (sec != null) {
return sec;
}
String[] parts = path.split("\\.");
String cur = "";
ConfigurationSection out = null;
for (String p : parts) {
cur = cur.isEmpty() ? p : cur + "." + p;
ConfigurationSection s = cfg.getConfigurationSection(cur);
if (s == null) {
s = cfg.createSection(cur);
}
out = s;
}
return out;
}
private static void copySection(ConfigurationSection from, ConfigurationSection to) {
for (String key : from.getKeys(false)) {
Object val = from.get(key);
if (val instanceof ConfigurationSection) {
copySection((ConfigurationSection) val, to.createSection(key));
} else {
to.set(key, val);
}
}
}
}
@@ -1,58 +0,0 @@
package com.bencodez.votingplugin.votereminding;
import java.util.HashMap;
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.listeners.AdvancedCoreLoginEvent;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.events.PlayerPostVoteEvent;
import com.bencodez.votingplugin.events.PlayerVoteCoolDownEndEvent;
import com.bencodez.votingplugin.events.PlayerVoteSiteCoolDownEndEvent;
import com.bencodez.votingplugin.votereminding.VoteRemindersManager.VoteReminderType;
public class VoteRemindersListener implements Listener {
private VotingPluginMain plugin;
public VoteRemindersListener(VotingPluginMain plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPostVote(PlayerPostVoteEvent event) {
plugin.getVoteRemindersManager().onVoteCast(event.getUser(), event.getVoteSite().getDisplayName());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onLogin(AdvancedCoreLoginEvent event) {
plugin.getVoteRemindersManager().onJoin(event.getPlayer(),
plugin.getVotingPluginUserManager().getVotingPluginUser(event.getUser()));
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onQuit(PlayerQuitEvent event) {
plugin.getVoteRemindersManager().onQuit(event.getPlayer());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onCoolDownEnd(PlayerVoteCoolDownEndEvent event) {
HashMap<String, String> placeholders = new HashMap<>();
plugin.getVoteRemindersManager().onCooldownTrigger(event.getPlayer(), VoteReminderType.COOLDOWN_END_ALL_SITES,
placeholders);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onCoolDownEnd(PlayerVoteSiteCoolDownEndEvent event) {
HashMap<String, String> placeholders = new HashMap<>();
placeholders.put("votesite", event.getSite().getDisplayName());
placeholders.put("votesite_id", event.getSite().getKey());
plugin.getVoteRemindersManager().onCooldownTrigger(event.getPlayer(), VoteReminderType.COOLDOWN_END_ANY_SITE,
placeholders);
}
}
@@ -1,981 +0,0 @@
// File: com/bencodez/votingplugin/votereminding/VoteRemindersManager.java
package com.bencodez.votingplugin.votereminding;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.rewards.RewardBuilder;
import com.bencodez.simpleapi.time.ParsedDuration;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
import com.bencodez.votingplugin.votereminding.store.VoteReminderCooldownStore;
/**
* VoteReminders core manager (no Bukkit repeating tasks).
*
* Uses ScheduledExecutorService for: - interval tick (aligned to minute
* boundary) - flush coalescing - delayed evaluations
*
* Assumptions (per your note): - VotingPluginUser lookup + Reward giving are
* safe off-thread for your implementation.
*/
public final class VoteRemindersManager {
/*
* ========================= Enums / Models =========================
*/
public enum VoteReminderType {
// Player lifecycle
LOGIN, FIRST_JOIN,
// Voting lifecycle
VOTE_CAST,
// Cooldown-driven (from CoolDownCheck)
COOLDOWN_END_ANY_SITE, COOLDOWN_END_ALL_SITES,
// Time-driven
INTERVAL
}
public static final class VoteReminderConditions {
private Boolean canVoteAny;
private Boolean canVoteAll;
private ParsedDuration minOnlineTime;
private Boolean firstJoin;
public Boolean getCanVoteAny() {
return canVoteAny;
}
public void setCanVoteAny(Boolean canVoteAny) {
this.canVoteAny = canVoteAny;
}
public Boolean getCanVoteAll() {
return canVoteAll;
}
public void setCanVoteAll(Boolean canVoteAll) {
this.canVoteAll = canVoteAll;
}
public ParsedDuration getMinOnlineTime() {
return minOnlineTime;
}
public void setMinOnlineTime(ParsedDuration minOnlineTime) {
this.minOnlineTime = minOnlineTime;
}
public Boolean getFirstJoin() {
return firstJoin;
}
public void setFirstJoin(Boolean firstJoin) {
this.firstJoin = firstJoin;
}
}
public static final class VoteReminderOptions {
private final boolean enabled;
private final boolean stopAfterMatch;
private final ParsedDuration globalCooldown;
private final int defaultPriority;
private final ParsedDuration defaultCooldown;
private final ParsedDuration defaultDelay;
private final VoteReminderConditions defaultsConditions;
private final String defaultRewardsPath;
@SuppressWarnings("deprecation")
public VoteReminderOptions(boolean enabled, boolean stopAfterMatch, ParsedDuration globalCooldown,
int defaultPriority, ParsedDuration defaultCooldown, ParsedDuration defaultDelay,
VoteReminderConditions defaultsConditions, String defaultRewardsPath) {
this.enabled = enabled;
this.stopAfterMatch = stopAfterMatch;
this.globalCooldown = globalCooldown == null ? ParsedDuration.parse("") : globalCooldown;
this.defaultPriority = defaultPriority;
this.defaultCooldown = defaultCooldown == null ? ParsedDuration.parse("") : defaultCooldown;
this.defaultDelay = defaultDelay == null ? ParsedDuration.parse("") : defaultDelay;
this.defaultsConditions = defaultsConditions == null ? new VoteReminderConditions() : defaultsConditions;
this.defaultRewardsPath = defaultRewardsPath;
}
public boolean isEnabled() {
return enabled;
}
public boolean isStopAfterMatch() {
return stopAfterMatch;
}
public ParsedDuration getGlobalCooldown() {
return globalCooldown;
}
public int getDefaultPriority() {
return defaultPriority;
}
public ParsedDuration getDefaultCooldown() {
return defaultCooldown;
}
public ParsedDuration getDefaultDelay() {
return defaultDelay;
}
public VoteReminderConditions getDefaultsConditions() {
return defaultsConditions;
}
public String getDefaultRewardsPath() {
return defaultRewardsPath;
}
}
public static final class VoteReminderDefinition {
private final String name;
private final VoteReminderType type;
private final int priority;
private final ParsedDuration cooldown;
private final ParsedDuration delay;
private final String rewardsPath;
private final VoteReminderConditions conditions;
private final ParsedDuration interval;
@SuppressWarnings("deprecation")
public VoteReminderDefinition(String name, VoteReminderType type, int priority, ParsedDuration cooldown,
ParsedDuration delay, String rewardsPath, VoteReminderConditions conditions, ParsedDuration interval) {
this.name = name;
this.type = type;
this.priority = priority;
this.cooldown = cooldown == null ? ParsedDuration.parse("") : cooldown;
this.delay = delay == null ? ParsedDuration.parse("") : delay;
this.rewardsPath = rewardsPath;
this.conditions = conditions == null ? new VoteReminderConditions() : conditions;
this.interval = interval == null ? ParsedDuration.parse("") : interval;
}
public String getName() {
return name;
}
public VoteReminderType getType() {
return type;
}
public int getPriority() {
return priority;
}
public ParsedDuration getCooldown() {
return cooldown;
}
public ParsedDuration getDelay() {
return delay;
}
public String getRewardsPath() {
return rewardsPath;
}
public VoteReminderConditions getConditions() {
return conditions;
}
public ParsedDuration getInterval() {
return interval;
}
}
/*
* ========================= Cooldowns wrapper =========================
*/
public static final class VoteReminderCooldowns {
private final VoteReminderCooldownStore store;
private final ParsedDuration globalCooldown;
@SuppressWarnings("deprecation")
public VoteReminderCooldowns(VoteReminderCooldownStore store, ParsedDuration globalCooldown) {
this.store = store;
this.globalCooldown = globalCooldown == null ? ParsedDuration.parse("") : globalCooldown;
}
public boolean tryAcquireGlobal(UUID uuid, long nowMs) {
long cd = ms(globalCooldown);
if (cd <= 0) {
return true;
}
return store.tryClaimGlobal(uuid, nowMs, cd);
}
/**
* Gate per-reminder using max(cooldown, interval) in millis.
*/
public boolean canFireReminder(UUID uuid, String reminderName, long nowMs, ParsedDuration cooldown,
ParsedDuration interval) {
long req = Math.max(ms(cooldown), ms(interval));
if (req <= 0) {
return true;
}
Map<String, Long> map = store.getPerReminderMap(uuid);
long last = 0L;
Long v = map.get(reminderName);
if (v != null) {
last = v.longValue();
}
return last <= 0 || (nowMs - last) >= req;
}
public void markFired(UUID uuid, String reminderName, long nowMs) {
store.setPerReminderLast(uuid, reminderName, nowMs);
}
private long ms(ParsedDuration d) {
if (d == null || d.isEmpty()) {
return 0L;
}
return Math.max(0L, d.getMillis());
}
}
/*
* ========================= Fields =========================
*/
private final VotingPluginMain plugin;
private final VoteReminderCooldownStore store;
private VoteReminderOptions options;
private VoteReminderCooldowns cooldowns;
private List<VoteReminderDefinition> reminders = Collections.emptyList();
private Map<String, VoteReminderDefinition> byName = Collections.emptyMap();
private final ConcurrentHashMap<UUID, Long> joinTimes = new ConcurrentHashMap<>();
// Trigger queue (smart flush so StopAfterMatch works across multiple triggers)
private final ConcurrentHashMap<UUID, PendingTriggers> pending = new ConcurrentHashMap<>();
// Scheduler (timing + coalescing + delayed eval)
private final ScheduledExecutorService scheduler;
private volatile ScheduledFuture<?> minuteFuture;
private final ConcurrentHashMap<UUID, ScheduledFuture<?>> flushFutures = new ConcurrentHashMap<>();
private static final class PendingTriggers {
volatile boolean login;
volatile boolean firstJoin;
volatile boolean interval;
volatile boolean voteCast;
volatile boolean cooldownAny;
volatile boolean cooldownAll;
final Map<String, String> placeholders = new HashMap<>();
void mergePlaceholders(Map<String, String> ph) {
if (ph == null || ph.isEmpty()) {
return;
}
placeholders.putAll(ph);
}
List<VoteReminderType> snapshotTypes() {
List<VoteReminderType> out = new ArrayList<>();
if (login)
out.add(VoteReminderType.LOGIN);
if (firstJoin)
out.add(VoteReminderType.FIRST_JOIN);
if (voteCast)
out.add(VoteReminderType.VOTE_CAST);
if (cooldownAll)
out.add(VoteReminderType.COOLDOWN_END_ALL_SITES);
if (cooldownAny)
out.add(VoteReminderType.COOLDOWN_END_ANY_SITE);
if (interval)
out.add(VoteReminderType.INTERVAL);
return out;
}
}
/*
* ========================= Constructor =========================
*/
public VoteRemindersManager(VotingPluginMain plugin, VoteReminderCooldownStore store) {
this.plugin = plugin;
this.store = store;
this.scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
private final AtomicInteger n = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "VotingPlugin-VoteReminders-" + n.incrementAndGet());
t.setDaemon(true);
return t;
}
});
}
/*
* ========================= Lifecycle =========================
*/
public void reload() {
stopTasks();
loadConfig();
this.cooldowns = new VoteReminderCooldowns(store, options.getGlobalCooldown());
startTasks();
}
public void shutdown() {
stopTasks();
joinTimes.clear();
pending.clear();
flushFutures.clear();
scheduler.shutdownNow();
}
private boolean isEnabled() {
return options != null && options.isEnabled();
}
/*
* ========================= Entry points (called by ONE listener)
* =========================
*/
public void onJoin(Player player, VotingPluginUser user) {
if (!isEnabled()) {
return;
}
if (player == null || user == null) {
return;
}
if (plugin.getOptions().isTreatVanishAsOffline() && user.isVanished()) {
return;
}
UUID uuid = player.getUniqueId();
joinTimes.put(uuid, System.currentTimeMillis());
queueTrigger(user, VoteReminderType.LOGIN, null);
if (!player.hasPlayedBefore()) {
queueTrigger(user, VoteReminderType.FIRST_JOIN, null);
}
flushSoon(uuid, user.getPlayerName());
}
public void onQuit(Player player) {
if (player == null) {
return;
}
UUID uuid = player.getUniqueId();
joinTimes.remove(uuid);
pending.remove(uuid);
ScheduledFuture<?> f = flushFutures.remove(uuid);
if (f != null) {
f.cancel(false);
}
}
public void onVoteCast(VotingPluginUser user, String siteKey) {
if (!isEnabled() || user == null) {
return;
}
Player player = Bukkit.getPlayer(user.getJavaUUID());
if (player == null || !player.isOnline()) {
return;
}
if (plugin.getOptions().isTreatVanishAsOffline() && user.isVanished()) {
return;
}
Map<String, String> ph = new HashMap<>();
if (siteKey != null && !siteKey.isEmpty()) {
ph.put("site", siteKey);
}
queueTrigger(user, VoteReminderType.VOTE_CAST, ph);
flushSoon(user.getJavaUUID(), user.getPlayerName());
}
public void onCooldownTrigger(VotingPluginUser user, VoteReminderType type, Map<String, String> placeholders) {
if (!isEnabled() || user == null) {
return;
}
if (type != VoteReminderType.COOLDOWN_END_ANY_SITE && type != VoteReminderType.COOLDOWN_END_ALL_SITES) {
return;
}
Player player = Bukkit.getPlayer(user.getJavaUUID());
if (player == null || !player.isOnline()) {
return;
}
if (plugin.getOptions().isTreatVanishAsOffline() && user.isVanished()) {
return;
}
queueTrigger(user, type, placeholders);
flushSoon(user.getJavaUUID(), user.getPlayerName());
}
/*
* ========================= Tasks (ScheduledExecutorService)
* =========================
*/
private void startTasks() {
if (!isEnabled()) {
return;
}
// If no INTERVAL reminders exist, don't schedule anything.
boolean hasInterval = false;
for (VoteReminderDefinition def : reminders) {
if (def.getType() == VoteReminderType.INTERVAL) {
hasInterval = true;
break;
}
}
if (!hasInterval) {
return;
}
// Align to minute boundary to keep behavior stable.
long now = System.currentTimeMillis();
long initialDelayMs = 60_000L - (now % 60_000L);
if (initialDelayMs <= 0L || initialDelayMs > 60_000L) {
initialDelayMs = 60_000L;
}
minuteFuture = scheduler.scheduleAtFixedRate(() -> {
if (!isEnabled() || plugin == null || !plugin.isEnabled()) {
return;
}
fireIntervalTick();
}, initialDelayMs, 60_000L, TimeUnit.MILLISECONDS);
}
private void stopTasks() {
ScheduledFuture<?> mf = minuteFuture;
if (mf != null) {
mf.cancel(false);
minuteFuture = null;
}
for (ScheduledFuture<?> f : flushFutures.values()) {
if (f != null) {
f.cancel(false);
}
}
flushFutures.clear();
}
private void fireIntervalTick() {
long nowMs = System.currentTimeMillis();
long nowMinute = nowMs / 60000L;
for (VoteReminderDefinition def : reminders) {
if (def.getType() != VoteReminderType.INTERVAL) {
continue;
}
long intervalMs = safeMs(def.getInterval());
if (intervalMs <= 0) {
continue;
}
long intervalMinutes = Math.max(1L, intervalMs / 60000L);
if ((nowMinute % intervalMinutes) != 0) {
continue;
}
for (Player p : Bukkit.getOnlinePlayers()) {
VotingPluginUser u = plugin.getVotingPluginUserManager().getVotingPluginUser(p);
if (u == null) {
continue;
}
if (plugin.getOptions().isTreatVanishAsOffline() && u.isVanished()) {
continue;
}
queueTrigger(u, VoteReminderType.INTERVAL, null);
flushSoon(u.getJavaUUID(), u.getPlayerName());
}
}
}
/*
* ========================= Queue + flush (smart multi-trigger handling)
* =========================
*/
private void queueTrigger(VotingPluginUser user, VoteReminderType type, Map<String, String> placeholders) {
UUID uuid = user.getJavaUUID();
PendingTriggers pt = pending.computeIfAbsent(uuid, k -> new PendingTriggers());
switch (type) {
case LOGIN:
pt.login = true;
break;
case FIRST_JOIN:
pt.firstJoin = true;
break;
case VOTE_CAST:
pt.voteCast = true;
break;
case COOLDOWN_END_ANY_SITE:
pt.cooldownAny = true;
break;
case COOLDOWN_END_ALL_SITES:
pt.cooldownAll = true;
break;
case INTERVAL:
pt.interval = true;
break;
default:
break;
}
pt.mergePlaceholders(placeholders);
}
private void flushSoon(UUID uuid, String playerName) {
// ~1 tick coalesce without Bukkit scheduler
flushFutures.compute(uuid, (k, existing) -> {
if (existing != null && !existing.isDone() && !existing.isCancelled()) {
return existing;
}
return scheduler.schedule(() -> {
try {
flush(uuid, playerName);
} finally {
flushFutures.remove(uuid);
}
}, 50L, TimeUnit.MILLISECONDS);
});
}
private void flush(UUID uuid, String playerName) {
if (!isEnabled()) {
return;
}
Player player = Bukkit.getPlayer(uuid);
if (player == null || !player.isOnline()) {
pending.remove(uuid);
return;
}
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(player);
if (user == null) {
pending.remove(uuid);
return;
}
if (plugin.getOptions().isTreatVanishAsOffline() && user.isVanished()) {
pending.remove(uuid);
return;
}
PendingTriggers pt = pending.remove(uuid);
if (pt == null) {
return;
}
List<VoteReminderType> types = pt.snapshotTypes();
plugin.extraDebug("[VoteReminders] flush " + user.getPlayerName() + " triggers=" + types);
for (VoteReminderDefinition def : reminders) {
if (!types.contains(def.getType())) {
continue;
}
boolean matched = attemptOrSchedule(user, player, def, pt.placeholders);
if (matched) {
plugin.extraDebug("[VoteReminders] matched " + def.getName() + " for " + user.getPlayerName() + " type="
+ def.getType() + " stopAfterMatch=" + options.isStopAfterMatch());
if (options.isStopAfterMatch()) {
return;
}
}
}
}
/*
* ========================= Config parsing =========================
*/
private void loadConfig() {
boolean enabled = plugin.getConfig().getBoolean("VoteReminderOptions.Enabled", true);
boolean stopAfterMatch = plugin.getConfig().getBoolean("VoteReminderOptions.StopAfterMatch", true);
ParsedDuration globalCooldown = ParsedDuration
.parse(plugin.getConfig().getString("VoteReminderOptions.GlobalCooldown", ""), TimeUnit.MINUTES);
int defaultPriority = plugin.getConfig().getInt("VoteReminderOptions.DefaultPriority", 0);
ParsedDuration defaultCooldown = ParsedDuration
.parse(plugin.getConfig().getString("VoteReminderOptions.Defaults.Cooldown", ""), TimeUnit.MINUTES);
ParsedDuration defaultDelay = ParsedDuration
.parse(plugin.getConfig().getString("VoteReminderOptions.Defaults.Delay", ""), TimeUnit.SECONDS);
VoteReminderConditions defaultsConditions = new VoteReminderConditions();
if (plugin.getConfig().contains("VoteReminderOptions.Defaults.Conditions.CanVoteAny")) {
defaultsConditions
.setCanVoteAny(plugin.getConfig().getBoolean("VoteReminderOptions.Defaults.Conditions.CanVoteAny"));
}
if (plugin.getConfig().contains("VoteReminderOptions.Defaults.Conditions.CanVoteAll")) {
defaultsConditions
.setCanVoteAll(plugin.getConfig().getBoolean("VoteReminderOptions.Defaults.Conditions.CanVoteAll"));
}
if (plugin.getConfig().contains("VoteReminderOptions.Defaults.Conditions.MinOnlineTime")) {
defaultsConditions.setMinOnlineTime(ParsedDuration.parse(
plugin.getConfig().getString("VoteReminderOptions.Defaults.Conditions.MinOnlineTime", ""),
TimeUnit.MINUTES));
}
if (plugin.getConfig().contains("VoteReminderOptions.Defaults.Conditions.FirstJoin")) {
defaultsConditions
.setFirstJoin(plugin.getConfig().getBoolean("VoteReminderOptions.Defaults.Conditions.FirstJoin"));
}
String defaultRewardsPath = "VoteReminderOptions.Defaults.Rewards";
this.options = new VoteReminderOptions(enabled, stopAfterMatch, globalCooldown, defaultPriority,
defaultCooldown, defaultDelay, defaultsConditions, defaultRewardsPath);
List<VoteReminderDefinition> defs = new ArrayList<>();
Map<String, VoteReminderDefinition> by = new HashMap<>();
if (plugin.getConfig().isConfigurationSection("VoteReminders")) {
for (String key : plugin.getConfig().getConfigurationSection("VoteReminders").getKeys(false)) {
VoteReminderDefinition def = loadOneReminder(key);
if (def != null) {
defs.add(def);
}
}
}
defs.sort(Comparator.comparingInt(VoteReminderDefinition::getPriority).reversed()
.thenComparing(VoteReminderDefinition::getName, String.CASE_INSENSITIVE_ORDER));
for (VoteReminderDefinition d : defs) {
by.put(d.getName(), d);
}
this.reminders = Collections.unmodifiableList(defs);
this.byName = Collections.unmodifiableMap(by);
plugin.extraDebug("[VoteReminders] loaded reminders=" + reminders.size());
}
private VoteReminderDefinition loadOneReminder(String name) {
String base = "VoteReminders." + name + ".";
String typeRaw = plugin.getConfig().getString(base + "Type", "INTERVAL");
VoteReminderType type;
try {
type = VoteReminderType.valueOf(typeRaw.toUpperCase(Locale.ROOT));
} catch (Exception e) {
plugin.getLogger().warning("[VoteReminders] Invalid Type for " + name + ": " + typeRaw);
return null;
}
int priority = plugin.getConfig().contains(base + "Priority") ? plugin.getConfig().getInt(base + "Priority")
: options.getDefaultPriority();
ParsedDuration cooldown = ParsedDuration.parse(plugin.getConfig().getString(base + "Cooldown", ""),
TimeUnit.MINUTES);
if (cooldown.isEmpty()) {
cooldown = options.getDefaultCooldown();
}
ParsedDuration delay = ParsedDuration.parse(plugin.getConfig().getString(base + "Delay", ""), TimeUnit.SECONDS);
if (delay.isEmpty()) {
delay = options.getDefaultDelay();
}
ParsedDuration interval = ParsedDuration.parse(plugin.getConfig().getString(base + "Interval", ""),
TimeUnit.MINUTES);
if (type == VoteReminderType.INTERVAL && (interval == null || interval.isEmpty())) {
plugin.getLogger().warning("[VoteReminders] INTERVAL reminder missing Interval: " + name);
return null;
}
VoteReminderConditions cond = mergeConditions(options.getDefaultsConditions(),
loadConditions(base + "Conditions."));
String rewardsPath = options.getDefaultRewardsPath();
if (plugin.getConfig().isConfigurationSection(base + "Rewards")
|| plugin.getConfig().isList(base + "Rewards")) {
rewardsPath = base + "Rewards";
}
plugin.extraDebug("[VoteReminders] loaded " + name + " type=" + type + " priority=" + priority + " rewardsPath="
+ rewardsPath);
return new VoteReminderDefinition(name, type, priority, cooldown, delay, rewardsPath, cond, interval);
}
private VoteReminderConditions loadConditions(String base) {
VoteReminderConditions c = new VoteReminderConditions();
if (plugin.getConfig().contains(base + "CanVoteAny")) {
c.setCanVoteAny(plugin.getConfig().getBoolean(base + "CanVoteAny"));
}
if (plugin.getConfig().contains(base + "CanVoteAll")) {
c.setCanVoteAll(plugin.getConfig().getBoolean(base + "CanVoteAll"));
}
if (plugin.getConfig().contains(base + "MinOnlineTime")) {
c.setMinOnlineTime(
ParsedDuration.parse(plugin.getConfig().getString(base + "MinOnlineTime", ""), TimeUnit.MINUTES));
}
if (plugin.getConfig().contains(base + "FirstJoin")) {
c.setFirstJoin(plugin.getConfig().getBoolean(base + "FirstJoin"));
}
return c;
}
private VoteReminderConditions mergeConditions(VoteReminderConditions base, VoteReminderConditions override) {
VoteReminderConditions out = new VoteReminderConditions();
out.setCanVoteAny(override.getCanVoteAny() != null ? override.getCanVoteAny() : base.getCanVoteAny());
out.setCanVoteAll(override.getCanVoteAll() != null ? override.getCanVoteAll() : base.getCanVoteAll());
out.setFirstJoin(override.getFirstJoin() != null ? override.getFirstJoin() : base.getFirstJoin());
if (override.getMinOnlineTime() != null && !override.getMinOnlineTime().isEmpty()) {
out.setMinOnlineTime(override.getMinOnlineTime());
} else {
out.setMinOnlineTime(base.getMinOnlineTime());
}
return out;
}
/*
* ========================= Trigger processing =========================
*/
private boolean attemptOrSchedule(VotingPluginUser user, Player player, VoteReminderDefinition def,
Map<String, String> placeholders) {
if (!isEnabled()) {
return false;
}
plugin.extraDebug("[VoteReminders] attempt " + def.getName() + " player=" + user.getPlayerName() + " type="
+ def.getType());
if (!isUserReminderEnabled(user)) {
plugin.extraDebug("[VoteReminders] gate disabled-reminders-map for " + user.getPlayerName());
return false;
}
if (!hasBaseReminderPermission(user)) {
plugin.extraDebug("[VoteReminders] gate base-permission for " + user.getPlayerName());
return false;
}
long delayMs = safeMs(def.getDelay());
if (delayMs > 0) {
plugin.extraDebug("[VoteReminders] schedule delay " + def.getName() + " for " + user.getPlayerName()
+ " delayMs=" + delayMs);
scheduleDelayedEvaluation(user.getJavaUUID(), def.getName(), placeholders, delayMs);
return true;
}
return attemptFireNow(user, player, def, placeholders);
}
private void scheduleDelayedEvaluation(UUID uuid, String reminderName, Map<String, String> placeholders,
long delayMs) {
Map<String, String> ph = placeholders == null ? null : new HashMap<>(placeholders);
scheduler.schedule(() -> {
Player p = Bukkit.getPlayer(uuid);
if (p == null || !p.isOnline()) {
return;
}
VotingPluginUser u = plugin.getVotingPluginUserManager().getVotingPluginUser(p);
if (u == null) {
return;
}
if (plugin.getOptions().isTreatVanishAsOffline() && u.isVanished()) {
return;
}
VoteReminderDefinition def = byName.get(reminderName);
if (def == null) {
return;
}
plugin.extraDebug("[VoteReminders] delayed eval " + def.getName() + " for " + u.getPlayerName());
attemptFireNow(u, p, def, ph);
}, Math.max(1L, delayMs), TimeUnit.MILLISECONDS);
}
private boolean attemptFireNow(VotingPluginUser user, Player player, VoteReminderDefinition def,
Map<String, String> placeholders) {
if (!user.shouldBeReminded()) {
plugin.extraDebug(
"[VoteReminders] gate shouldBeReminded=false " + user.getPlayerName() + " def=" + def.getName());
return false;
}
if (!passesConditions(user, player, def.getConditions())) {
plugin.extraDebug(
"[VoteReminders] gate conditions=false " + user.getPlayerName() + " def=" + def.getName());
return false;
}
long now = System.currentTimeMillis();
if (!cooldowns.tryAcquireGlobal(user.getJavaUUID(), now)) {
plugin.extraDebug("[VoteReminders] gate globalCooldown " + user.getPlayerName() + " def=" + def.getName());
return false;
}
if (!cooldowns.canFireReminder(user.getJavaUUID(), def.getName(), now, def.getCooldown(), def.getInterval())) {
plugin.extraDebug(
"[VoteReminders] gate perReminderCooldown " + user.getPlayerName() + " def=" + def.getName());
return false;
}
plugin.extraDebug("[VoteReminders] FIRE " + def.getName() + " -> rewardsPath=" + def.getRewardsPath() + " user="
+ user.getPlayerName());
giveRewardFromPath(user, def.getRewardsPath(), placeholders);
cooldowns.markFired(user.getJavaUUID(), def.getName(), now);
plugin.extraDebug("[VoteReminders] fired " + user.getPlayerName() + " via " + def.getName());
return true;
}
/*
* ========================= Rewards =========================
*/
private void giveRewardFromPath(VotingPluginUser user, String rewardsPath, Map<String, String> placeholders) {
RewardBuilder rb = new RewardBuilder(plugin.getConfig(), rewardsPath).setGiveOffline(false)
.disableDefaultWorlds();
rb.withPlaceHolder("sitesavailable", "" + user.getSitesNotVotedOn());
if (placeholders != null) {
for (Map.Entry<String, String> e : placeholders.entrySet()) {
rb.withPlaceHolder(e.getKey(), e.getValue());
}
}
rb.send(user);
}
/*
* ========================= Conditions =========================
*/
private boolean passesConditions(VotingPluginUser user, Player player, VoteReminderConditions c) {
if (c.getCanVoteAny() != null) {
boolean canAny = user.canVoteAny();
if (c.getCanVoteAny().booleanValue() != canAny) {
plugin.extraDebug("[VoteReminders] gate CanVoteAny " + user.getPlayerName() + " required="
+ c.getCanVoteAny() + " actual=" + canAny);
return false;
}
}
if (c.getCanVoteAll() != null) {
boolean canAll = user.canVoteAll();
if (c.getCanVoteAll().booleanValue() != canAll) {
plugin.extraDebug("[VoteReminders] gate CanVoteAll " + user.getPlayerName() + " required="
+ c.getCanVoteAll() + " actual=" + canAll);
return false;
}
}
ParsedDuration mot = c.getMinOnlineTime();
if (mot != null && !mot.isEmpty()) {
Long join = joinTimes.get(user.getJavaUUID());
if (join != null) {
long onlineMs = System.currentTimeMillis() - join.longValue();
long need = safeMs(mot);
if (onlineMs < need) {
plugin.extraDebug("[VoteReminders] gate MinOnlineTime " + user.getPlayerName() + " onlineMs="
+ onlineMs + " needMs=" + need);
return false;
}
}
}
if (c.getFirstJoin() != null) {
boolean first = !player.hasPlayedBefore();
if (c.getFirstJoin().booleanValue() != first) {
plugin.extraDebug("[VoteReminders] gate FirstJoin " + user.getPlayerName() + " required="
+ c.getFirstJoin() + " actual=" + first);
return false;
}
}
return true;
}
/*
* ========================= Permission logic =========================
*/
private boolean hasBaseReminderPermission(VotingPluginUser user) {
return user.hasPermission("VotingPlugin.Login.RemindVotes") || user.hasPermission("VotingPlugin.Player");
}
private boolean isUserReminderEnabled(VotingPluginUser user) {
return true;
}
/*
* ========================= Helpers =========================
*/
private long safeMs(ParsedDuration d) {
if (d == null || d.isEmpty()) {
return 0L;
}
return Math.max(0L, d.getMillis());
}
}
@@ -1,193 +0,0 @@
package com.bencodez.votingplugin.votereminding.store;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.user.VotingPluginUser;
/**
* Uses VotingPluginUser.getUserData() cached columns:
* - VoteRemindersLast (BIGINT)
* - VoteRemindersMap (LONGTEXT) format: name=ms;name=ms
*
* NOTE: tryClaimGlobal is best-effort and not proxy-atomic unless your storage layer
* supports conditional updates.
*/
public final class UserDataVoteReminderCooldownStore implements VoteReminderCooldownStore {
public static final String KEY_GLOBAL_LAST = "VoteRemindersLast";
public static final String KEY_MAP = "VoteRemindersMap";
private final VotingPluginMain plugin;
private final ConcurrentHashMap<UUID, Map<String, Long>> mapCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<UUID, Object> locks = new ConcurrentHashMap<>();
public UserDataVoteReminderCooldownStore(VotingPluginMain plugin) {
this.plugin = plugin;
}
@Override
public boolean tryClaimGlobal(UUID uuid, long nowMs, long globalCooldownMs) {
if (globalCooldownMs <= 0) {
return true;
}
Object lock = locks.computeIfAbsent(uuid, k -> new Object());
synchronized (lock) {
VotingPluginUser user = getUser(uuid);
if (user == null) {
return false;
}
long last = readLong(user, KEY_GLOBAL_LAST);
if (last > 0 && (nowMs - last) < globalCooldownMs) {
return false;
}
writeLong(user, KEY_GLOBAL_LAST, nowMs);
return true;
}
}
@Override
public Map<String, Long> getPerReminderMap(UUID uuid) {
Map<String, Long> cached = mapCache.get(uuid);
if (cached != null) {
return cached;
}
VotingPluginUser user = getUser(uuid);
if (user == null) {
return Collections.emptyMap();
}
String raw = safe(user.getUserData().getString(KEY_MAP));
Map<String, Long> parsed = parseMap(raw);
mapCache.put(uuid, parsed);
return parsed;
}
@Override
public void setPerReminderLast(UUID uuid, String reminderName, long nowMs) {
if (reminderName == null || reminderName.isEmpty()) {
return;
}
Object lock = locks.computeIfAbsent(uuid, k -> new Object());
synchronized (lock) {
VotingPluginUser user = getUser(uuid);
if (user == null) {
return;
}
Map<String, Long> map = getPerReminderMap(uuid);
if (!(map instanceof HashMap)) {
map = new HashMap<>(map);
mapCache.put(uuid, map);
}
map.put(reminderName, nowMs);
String encoded = encodeMap(map);
user.getUserData().setString(KEY_MAP, encoded);
}
}
@Override
public boolean isPersistent(UUID uuid) {
VotingPluginUser user = getUser(uuid);
if (user == null) {
return false;
}
return user.getData().hasData();
}
public void clearCache(UUID uuid) {
mapCache.remove(uuid);
locks.remove(uuid);
}
private VotingPluginUser getUser(UUID uuid) {
return plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, false);
}
private long readLong(VotingPluginUser user, String key) {
String s = safe(user.getUserData().getString(key));
if (s.isEmpty() || s.equalsIgnoreCase("null")) {
return 0L;
}
try {
return Long.parseLong(s);
} catch (NumberFormatException e) {
return 0L;
}
}
private void writeLong(VotingPluginUser user, String key, long value) {
user.getUserData().setString(key, Long.toString(Math.max(0L, value)));
}
private String safe(String s) {
return s == null ? "" : s.trim();
}
private Map<String, Long> parseMap(String raw) {
HashMap<String, Long> out = new HashMap<>();
if (raw == null || raw.isEmpty()) {
return out;
}
String[] parts = raw.split(";");
for (String part : parts) {
if (part == null || part.isEmpty())
continue;
int idx = part.indexOf('=');
if (idx <= 0 || idx >= part.length() - 1)
continue;
String name = part.substring(0, idx).trim();
String msStr = part.substring(idx + 1).trim();
if (name.isEmpty() || msStr.isEmpty())
continue;
try {
long ms = Long.parseLong(msStr);
if (ms > 0) {
out.put(name, ms);
}
} catch (NumberFormatException ignore) {
}
}
return out;
}
private String encodeMap(Map<String, Long> map) {
if (map == null || map.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
map.entrySet().stream().sorted((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.getKey(), b.getKey()))
.forEach(e -> {
if (e.getKey() == null)
return;
long v = e.getValue() == null ? 0L : e.getValue().longValue();
if (v <= 0L)
return;
if (sb.length() > 0)
sb.append(';');
sb.append(e.getKey()).append('=').append(v);
});
return sb.toString();
}
}
@@ -1,14 +0,0 @@
package com.bencodez.votingplugin.votereminding.store;
import java.util.Map;
import java.util.UUID;
public interface VoteReminderCooldownStore {
boolean tryClaimGlobal(UUID uuid, long nowMs, long globalCooldownMs);
Map<String, Long> getPerReminderMap(UUID uuid);
void setPerReminderLast(UUID uuid, String reminderName, long nowMs);
boolean isPersistent(UUID uuid);
}
@@ -1,19 +0,0 @@
package com.bencodez.votingplugin.votesites;
public final class NextSite {
private final VoteSite site;
private final long secondsUntilAvailable;
public NextSite(VoteSite site, long secondsUntilAvailable) {
this.site = site;
this.secondsUntilAvailable = secondsUntilAvailable;
}
public VoteSite getSite() {
return site;
}
public long getSecondsUntilAvailable() {
return secondsUntilAvailable;
}
}
@@ -1,153 +0,0 @@
package com.bencodez.votingplugin.votesites;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.bencodez.votingplugin.VotingPluginMain;
import lombok.Getter;
import lombok.Setter;
public class VoteSiteManager {
private final VotingPluginMain plugin;
@Getter
@Setter
private List<VoteSite> voteSites = Collections.synchronizedList(new ArrayList<>());
public VoteSiteManager(VotingPluginMain plugin) {
this.plugin = plugin;
}
/**
* Reloads votesites from config and returns the backing list.
*/
public List<VoteSite> loadVoteSites() {
plugin.getConfigVoteSites().setup();
List<VoteSite> newSites = Collections.synchronizedList(new ArrayList<>());
newSites.addAll(plugin.getConfigVoteSites().getVoteSitesLoad());
voteSites = newSites;
if (voteSites.isEmpty()) {
plugin.getLogger().warning("Detected no voting sites, this may mean something isn't properly setup");
}
plugin.debug("Loaded VoteSites");
return voteSites;
}
/**
* Attempts to map a URL / display name / key to the configured "site name".
*
* @param checkEnabled Whether to only consider enabled vote sites.
* @param urls One or more URLs / names to resolve.
* @return The resolved vote site name, or the first provided value if no match
*/
public String getVoteSiteName(boolean checkEnabled, String... urls) {
ArrayList<String> sites = plugin.getConfigVoteSites().getVoteSitesNames(checkEnabled);
for (String url : urls) {
if (url == null) {
return null;
}
if (!url.isEmpty() && sites != null) {
for (String siteName : sites) {
String configuredUrl = plugin.getConfigVoteSites().getServiceSite(siteName);
if (configuredUrl != null && configuredUrl.equalsIgnoreCase(url)) {
return siteName;
}
if (siteName.equalsIgnoreCase(url)) {
return siteName;
}
}
}
}
// If no match, return the first provided value (original behavior)
for (String url : urls) {
return url;
}
return "";
}
/**
* Resolves a VoteSite from an identifier.
* - Matches on key or display name
* - Can optionally auto-create vote sites (same behavior as VotingPluginMain)
*/
public VoteSite getVoteSite(String site, boolean checkEnabled) {
String siteName = getVoteSiteName(checkEnabled, site);
for (VoteSite voteSite : getVoteSites()) {
if (voteSite.getKey().equalsIgnoreCase(siteName) || voteSite.getDisplayName().equals(siteName)) {
return voteSite;
}
}
if (plugin.getConfigFile().isAutoCreateVoteSites()
&& !plugin.getConfigVoteSites().getVoteSitesNames(false).contains(siteName)) {
plugin.getConfigVoteSites().generateVoteSite(siteName);
// Original code used: new VoteSite(plugin, siteName.replace(".", "_"));
// Keep identical behavior.
return new VoteSite(plugin, siteName.replace(".", "_"));
}
return null;
}
public ArrayList<VoteSite> getVoteSitesEnabled() {
ArrayList<VoteSite> sites = new ArrayList<>();
for (VoteSite site : getVoteSites()) {
if (site.isEnabled()) {
sites.add(site);
}
}
return sites;
}
/**
* Converts an input name/url into the configured service site URL if possible.
* If no mapping exists, returns the input (original behavior).
*/
public String getVoteSiteServiceSite(String name) {
ArrayList<String> sites = plugin.getConfigVoteSites().getVoteSitesNames(true);
if (name == null) {
return null;
}
if (sites != null) {
for (String siteName : sites) {
String url = plugin.getConfigVoteSites().getServiceSite(siteName);
if (url != null) {
if (url.equalsIgnoreCase(name) || name.equalsIgnoreCase(siteName)) {
return url;
}
}
}
}
return name;
}
public boolean hasVoteSite(String site) {
String siteName = getVoteSiteName(false, site);
for (VoteSite voteSite : getVoteSites()) {
if (voteSite.getKey().equalsIgnoreCase(siteName) || voteSite.getDisplayName().equals(siteName)) {
return true;
}
}
return false;
}
public boolean isVoteSite(String voteSite) {
for (VoteSite site : getVoteSites()) {
if (site.getKey().equalsIgnoreCase(voteSite)) {
return true;
}
}
return false;
}
}
@@ -1,72 +0,0 @@
package com.bencodez.votingplugin.webhook;
/**
* Utility methods for handling Discord webhook rate-limit responses.
*/
public final class DiscordRateLimitUtil {
private DiscordRateLimitUtil() {
// utility
}
/**
* Extracts Discord's "retry_after" value from a JSON string.
*
* <p>Discord rate-limit bodies typically look like:
* {"message":"You are being rate limited.","retry_after":1.234,"global":false}</p>
*
* <p>This method avoids requiring a JSON library by doing a small string parse.
* If parsing fails, returns 0.</p>
*
* @param body response body
* @return retry_after in milliseconds, or 0 if not found/parseable
*/
public static long extractRetryAfterMs(String body) {
if (body == null || body.isEmpty()) {
return 0;
}
int idx = body.indexOf("\"retry_after\"");
if (idx == -1) {
idx = body.indexOf("retry_after");
if (idx == -1) {
return 0;
}
}
int colon = body.indexOf(':', idx);
if (colon == -1) {
return 0;
}
int start = colon + 1;
while (start < body.length() && Character.isWhitespace(body.charAt(start))) {
start++;
}
int end = start;
while (end < body.length()) {
char c = body.charAt(end);
if ((c >= '0' && c <= '9') || c == '.' || c == '-') {
end++;
continue;
}
break;
}
if (end <= start) {
return 0;
}
try {
double seconds = Double.parseDouble(body.substring(start, end));
if (seconds <= 0) {
return 0;
}
// cushion so we don't immediately collide again
return (long) Math.ceil(seconds * 1000.0) + 250L;
} catch (NumberFormatException ignored) {
return 0;
}
}
}
@@ -1,191 +0,0 @@
package com.bencodez.votingplugin.webhook;
/**
* Builds Discord-compatible webhook JSON payloads.
*
* <p>This builder only outputs fields that are set/enabled.</p>
*/
public final class DiscordWebhookPayloadBuilder {
private String username;
private String avatarUrl;
private String content;
private boolean embedEnabled;
private String embedTitle;
private String embedDescription;
private String embedColorHex;
private String embedThumbnailUrl;
private boolean footerEnabled;
private String footerText;
/** @return new builder */
public static DiscordWebhookPayloadBuilder create() {
return new DiscordWebhookPayloadBuilder();
}
private DiscordWebhookPayloadBuilder() {
// builder
}
/** @param username username override */
public DiscordWebhookPayloadBuilder username(String username) {
this.username = username;
return this;
}
/** @param avatarUrl avatar URL override */
public DiscordWebhookPayloadBuilder avatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
return this;
}
/** @param content message content (plain text/markdown) */
public DiscordWebhookPayloadBuilder content(String content) {
this.content = content;
return this;
}
/**
* Enables an embed.
*
* @param title embed title
* @param description embed description
* @param colorHex color in #RRGGBB (optional)
* @param thumbnailUrl thumbnail URL (optional)
* @param footerEnabled whether footer is enabled
* @param footerText footer text (optional)
* @return builder
*/
public DiscordWebhookPayloadBuilder embed(String title, String description, String colorHex, String thumbnailUrl,
boolean footerEnabled, String footerText) {
this.embedEnabled = true;
this.embedTitle = title;
this.embedDescription = description;
this.embedColorHex = colorHex;
this.embedThumbnailUrl = thumbnailUrl;
this.footerEnabled = footerEnabled;
this.footerText = footerText;
return this;
}
/**
* Builds a JSON payload string suitable for Discord webhooks.
*
* @return JSON string
*/
public String buildJson() {
StringBuilder sb = new StringBuilder(256);
sb.append('{');
boolean first = true;
first = appendString(sb, "username", username, first);
first = appendString(sb, "avatar_url", avatarUrl, first);
first = appendString(sb, "content", content, first);
if (embedEnabled) {
if (!first) {
sb.append(',');
}
sb.append("\"embeds\":[{");
boolean eFirst = true;
eFirst = appendString(sb, "title", embedTitle, eFirst);
eFirst = appendString(sb, "description", embedDescription, eFirst);
Integer color = parseHexColor(embedColorHex);
if (color != null) {
if (!eFirst) {
sb.append(',');
}
sb.append("\"color\":").append(color.intValue());
eFirst = false;
}
if (embedThumbnailUrl != null && !embedThumbnailUrl.isEmpty()) {
if (!eFirst) {
sb.append(',');
}
sb.append("\"thumbnail\":{");
sb.append("\"url\":\"").append(escapeJson(embedThumbnailUrl)).append("\"");
sb.append('}');
eFirst = false;
}
if (footerEnabled && footerText != null && !footerText.isEmpty()) {
if (!eFirst) {
sb.append(',');
}
sb.append("\"footer\":{");
sb.append("\"text\":\"").append(escapeJson(footerText)).append("\"");
sb.append('}');
}
sb.append("}]");
first = false;
}
sb.append('}');
return sb.toString();
}
private static boolean appendString(StringBuilder sb, String key, String value, boolean first) {
if (value == null || value.isEmpty()) {
return first;
}
if (!first) {
sb.append(',');
}
sb.append('"').append(key).append("\":\"").append(escapeJson(value)).append('"');
return false;
}
private static Integer parseHexColor(String hex) {
if (hex == null || hex.isEmpty()) {
return null;
}
String h = hex.trim();
if (h.startsWith("#")) {
h = h.substring(1);
}
if (h.length() != 6) {
return null;
}
try {
return Integer.valueOf(Integer.parseInt(h, 16));
} catch (NumberFormatException ignored) {
return null;
}
}
private static String escapeJson(String s) {
StringBuilder out = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '\\':
out.append("\\\\");
break;
case '"':
out.append("\\\"");
break;
case '\n':
out.append("\\n");
break;
case '\r':
out.append("\\r");
break;
case '\t':
out.append("\\t");
break;
default:
out.append(c);
break;
}
}
return out.toString();
}
}
@@ -1,19 +0,0 @@
package com.bencodez.votingplugin.webhook;
/**
* Placeholder expansion abstraction.
*
* <p>VotingPlugin should supply an implementation that expands placeholders
* such as {PLAYER}, {SERVICE_SITE}, etc.</p>
*/
@FunctionalInterface
public interface PlaceholderExpander {
/**
* Expands placeholders inside a string.
*
* @param input input string (may be null)
* @return expanded string (never null)
*/
String expand(String input);
}
@@ -1,55 +0,0 @@
package com.bencodez.votingplugin.webhook;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Convenience wrapper to manage webhook lifecycle within VotingPlugin.
*/
public final class VotingPluginWebhooks {
private final WebhookService service;
private volatile Map<String, WebhookDefinition> definitions = Collections.emptyMap();
/**
* Creates a webhook manager.
*
* @param plugin plugin instance
*/
public VotingPluginWebhooks(JavaPlugin plugin) {
Objects.requireNonNull(plugin, "plugin");
this.service = new WebhookService(msg -> plugin.getLogger().warning(msg));
}
/**
* Reloads webhook definitions from config and ensures the service is started.
*
* @param webhooksRoot root section (usually config.getConfigurationSection("Webhooks"))
*/
public void reload(ConfigurationSection webhooksRoot) {
Map<String, WebhookDefinition> defs = WebhookConfigLoader.load(webhooksRoot);
this.definitions = defs;
service.setDefinitions(defs);
service.start();
}
/**
* Stops the webhook service.
*/
public void shutdown() {
service.stop();
}
/**
* Creates a reward executor using the current definitions.
*
* @return executor
*/
public WebhookRewardExecutor createExecutor() {
return new WebhookRewardExecutor(service, definitions);
}
}
@@ -1,133 +0,0 @@
package com.bencodez.votingplugin.webhook;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.bukkit.configuration.ConfigurationSection;
/**
* Loads webhook definitions from VotingPlugin's Config.yml section.
*
* Expected structure under the root section you pass:
* <pre>
* Enabled: true
* Definitions:
* myhook:
* Enabled: true
* Url: "https://..."
* Method: POST
* ContentType: "application/json"
* TimeoutMs: 8000
* Headers:
* Authorization: "Bearer {API_KEY}"
* HandleDiscordRateLimits: true
* Retry:
* Enabled: true
* MaxAttempts: 5
* BackoffMs: 1500
* MaxBackoffMs: 15000
* Signature:
* Enabled: false
* Algo: "HMAC_SHA256"
* Header: "X-Signature"
* Secret: "secret"
* </pre>
*
*/
public final class WebhookConfigLoader {
private WebhookConfigLoader() {
// utility
}
/**
* Loads webhook definitions.
*
* @param webhooksRoot root section for webhooks (e.g., config.getConfigurationSection("Webhooks"))
* @return definitions by id (never null)
*/
public static Map<String, WebhookDefinition> load(ConfigurationSection webhooksRoot) {
if (webhooksRoot == null) {
return Collections.emptyMap();
}
if (!webhooksRoot.getBoolean("Enabled", false)) {
return Collections.emptyMap();
}
ConfigurationSection defsSec = webhooksRoot.getConfigurationSection("Definitions");
if (defsSec == null) {
return Collections.emptyMap();
}
Map<String, WebhookDefinition> out = new LinkedHashMap<>();
for (String id : defsSec.getKeys(false)) {
ConfigurationSection s = defsSec.getConfigurationSection(id);
if (s == null) {
continue;
}
boolean defEnabled = s.getBoolean("Enabled", true);
String url = s.getString("Url", "");
if (url == null || url.isEmpty()) {
continue;
}
WebhookHttpMethod method = WebhookHttpMethod.parse(s.getString("Method", "POST"));
String contentType = s.getString("ContentType", "application/json");
int timeoutMs = s.getInt("TimeoutMs", 8000);
Map<String, String> headers = loadStringMap(s.getConfigurationSection("Headers"));
boolean handleDiscord = s.getBoolean("HandleDiscordRateLimits", true);
// Retry
boolean retryEnabled = true;
int maxAttempts = 5;
long backoffMs = 1500;
long maxBackoffMs = 15000;
ConfigurationSection retrySec = s.getConfigurationSection("Retry");
if (retrySec != null) {
retryEnabled = retrySec.getBoolean("Enabled", true);
maxAttempts = retrySec.getInt("MaxAttempts", 5);
backoffMs = retrySec.getLong("BackoffMs", 1500);
maxBackoffMs = retrySec.getLong("MaxBackoffMs", 15000);
}
// Signature
WebhookSignatureConfig signature = null;
ConfigurationSection sigSec = s.getConfigurationSection("Signature");
if (sigSec != null) {
boolean sigEnabled = sigSec.getBoolean("Enabled", false);
String algo = sigSec.getString("Algo", "HMAC_SHA256");
String header = sigSec.getString("Header", "X-Signature");
String secret = sigSec.getString("Secret", "");
signature = new WebhookSignatureConfig(sigEnabled, algo, header, secret);
}
WebhookDefinition def = new WebhookDefinition(id, defEnabled, url, method, contentType, timeoutMs, handleDiscord,
headers, signature, retryEnabled, maxAttempts, backoffMs, maxBackoffMs);
out.put(id, def);
}
return out;
}
private static Map<String, String> loadStringMap(ConfigurationSection sec) {
if (sec == null) {
return Collections.emptyMap();
}
Map<String, String> out = new LinkedHashMap<>();
for (String k : sec.getKeys(false)) {
Object v = sec.get(k);
if (v != null) {
out.put(k, String.valueOf(v));
}
}
return out;
}
}
@@ -1,173 +0,0 @@
package com.bencodez.votingplugin.webhook;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Webhook endpoint definition.
*
* <p>These are loaded from Config.yml under Webhooks.Definitions.*</p>
*/
public final class WebhookDefinition {
private final String id;
private final boolean enabled;
private final String url;
private final WebhookHttpMethod method;
private final String contentType;
private final int timeoutMs;
private final boolean handleDiscordRateLimits;
private final Map<String, String> headers;
private final WebhookSignatureConfig signature;
private final boolean retryEnabled;
private final int retryMaxAttempts;
private final long retryBackoffMs;
private final long retryMaxBackoffMs;
private volatile long lastRetryAfterMs;
/**
* Constructs a webhook definition.
*
* @param id id/key of this webhook definition
* @param enabled whether this definition is enabled
* @param url webhook URL
* @param method HTTP method
* @param contentType request content type (usually application/json)
* @param timeoutMs request timeout in milliseconds
* @param handleDiscordRateLimits whether to use Discord retry_after for HTTP 429
* @param headers extra headers (values may contain placeholders)
* @param signature signature config (optional)
* @param retryEnabled whether retry is enabled
* @param retryMaxAttempts max attempts including the first attempt
* @param retryBackoffMs initial backoff in milliseconds
* @param retryMaxBackoffMs maximum backoff in milliseconds
*/
public WebhookDefinition(String id, boolean enabled, String url, WebhookHttpMethod method, String contentType,
int timeoutMs, boolean handleDiscordRateLimits, Map<String, String> headers, WebhookSignatureConfig signature,
boolean retryEnabled, int retryMaxAttempts, long retryBackoffMs, long retryMaxBackoffMs) {
this.id = id;
this.enabled = enabled;
this.url = url;
this.method = method == null ? WebhookHttpMethod.POST : method;
this.contentType = (contentType == null || contentType.isEmpty()) ? "application/json" : contentType;
this.timeoutMs = timeoutMs <= 0 ? 8000 : timeoutMs;
this.handleDiscordRateLimits = handleDiscordRateLimits;
this.headers = headers == null ? Collections.emptyMap()
: Collections.unmodifiableMap(new LinkedHashMap<>(headers));
this.signature = signature;
this.retryEnabled = retryEnabled;
this.retryMaxAttempts = retryMaxAttempts <= 0 ? 1 : retryMaxAttempts;
this.retryBackoffMs = Math.max(0, retryBackoffMs);
this.retryMaxBackoffMs = Math.max(this.retryBackoffMs, retryMaxBackoffMs);
}
/** @return definition id */
public String getId() {
return id;
}
/** @return true if enabled */
public boolean isEnabled() {
return enabled;
}
/** @return webhook URL */
public String getUrl() {
return url;
}
/** @return HTTP method */
public WebhookHttpMethod getMethod() {
return method;
}
/** @return content type */
public String getContentType() {
return contentType;
}
/** @return timeout in milliseconds */
public int getTimeoutMs() {
return timeoutMs;
}
/** @return true if Discord 429 retry_after handling is enabled */
public boolean isHandleDiscordRateLimits() {
return handleDiscordRateLimits;
}
/** @return extra headers (unmodifiable) */
public Map<String, String> getHeaders() {
return headers;
}
/** @return signature config (may be null) */
public WebhookSignatureConfig getSignature() {
return signature;
}
/** @return true if retry is enabled */
public boolean isRetryEnabled() {
return retryEnabled;
}
/** @return max attempts */
public int getRetryMaxAttempts() {
return retryMaxAttempts;
}
/** @return initial backoff in ms */
public long getRetryBackoffMs() {
return retryBackoffMs;
}
/** @return max backoff in ms */
public long getRetryMaxBackoffMs() {
return retryMaxBackoffMs;
}
/**
* Returns a safe URL string for logs (removes sensitive token segments).
*
* @return sanitized URL for logging
*/
public String safeUrlForLog() {
if (url == null) {
return "null";
}
// Discord webhooks: https://discord.com/api/webhooks/{id}/{token}
int idx = url.indexOf("/api/webhooks/");
if (idx == -1) {
return url;
}
String prefix = url.substring(0, idx);
String rest = url.substring(idx);
String[] parts = rest.split("/");
if (parts.length >= 5) {
return prefix + "/api/webhooks/" + parts[3] + "/REDACTED";
}
return prefix + "/api/webhooks/REDACTED";
}
/** @return retry_after in ms, or 0 if none */
public long getLastRetryAfterMs() {
return lastRetryAfterMs;
}
/** @param lastRetryAfterMs retry delay in ms */
public void setLastRetryAfterMs(long lastRetryAfterMs) {
this.lastRetryAfterMs = lastRetryAfterMs;
}
}
@@ -1,25 +0,0 @@
package com.bencodez.votingplugin.webhook;
/**
* Supported HTTP methods for webhook requests.
*/
public enum WebhookHttpMethod {
POST, PUT, PATCH;
/**
* Parses a method string safely.
*
* @param s method string
* @return parsed method or POST if invalid/null
*/
public static WebhookHttpMethod parse(String s) {
if (s == null) {
return POST;
}
try {
return WebhookHttpMethod.valueOf(s.trim().toUpperCase());
} catch (Exception ignored) {
return POST;
}
}
}
@@ -1,17 +0,0 @@
package com.bencodez.votingplugin.webhook;
/**
* Logging abstraction for the webhook subsystem.
*
* <p>This avoids hard-coding Bukkit/SLF4J implementations into the core sender.</p>
*/
@FunctionalInterface
public interface WebhookLogger {
/**
* Logs a warning message.
*
* @param message message
*/
void warn(String message);
}
@@ -1,51 +0,0 @@
package com.bencodez.votingplugin.webhook;
import java.util.Map;
/**
* A queued webhook request.
*
* <p>All placeholders should be expanded before creating this object.</p>
*/
public final class WebhookRequest {
private final String definitionId;
private final String body;
private final Map<String, String> headers;
private final String signatureSecretOverride;
/**
* Creates a webhook request.
*
* @param definitionId definition id
* @param body request body (usually JSON)
* @param headers expanded headers (may be null)
* @param signatureSecretOverride expanded signature secret override (may be null)
*/
public WebhookRequest(String definitionId, String body, Map<String, String> headers, String signatureSecretOverride) {
this.definitionId = definitionId;
this.body = body;
this.headers = headers;
this.signatureSecretOverride = signatureSecretOverride;
}
/** @return definition id */
public String getDefinitionId() {
return definitionId;
}
/** @return body */
public String getBody() {
return body;
}
/** @return headers (may be null) */
public Map<String, String> getHeaders() {
return headers;
}
/** @return signature secret override (may be null) */
public String getSignatureSecretOverride() {
return signatureSecretOverride;
}
}
@@ -1,117 +0,0 @@
package com.bencodez.votingplugin.webhook;
/**
* Represents one webhook entry under a reward's "Webhooks:" list.
*
* Two payload styles:
* <ul>
* <li>Raw JSON: {@link #getJson()}</li>
* <li>Discord convenience payload: content + optional embed fields</li>
* </ul>
*
*
* All fields may contain placeholders and are expanded at execution time.
*/
public final class WebhookRewardEntry {
private final String webhookId;
// Generic mode
private final String json;
// Discord convenience mode
private final String content;
private final boolean embedEnabled;
private final String embedTitle;
private final String embedDescription;
private final String embedColorHex;
private final String embedThumbnailUrl;
private final boolean footerEnabled;
private final String footerText;
/**
* Creates a reward entry.
*
* @param webhookId webhook definition id
* @param json raw json payload (nullable)
* @param content discord content (nullable)
* @param embedEnabled embed enabled
* @param embedTitle embed title
* @param embedDescription embed description
* @param embedColorHex hex color #RRGGBB (nullable)
* @param embedThumbnailUrl thumbnail url (nullable)
* @param footerEnabled footer enabled
* @param footerText footer text (nullable)
*/
public WebhookRewardEntry(String webhookId, String json, String content, boolean embedEnabled, String embedTitle,
String embedDescription, String embedColorHex, String embedThumbnailUrl, boolean footerEnabled,
String footerText) {
this.webhookId = webhookId;
this.json = json;
this.content = content;
this.embedEnabled = embedEnabled;
this.embedTitle = embedTitle;
this.embedDescription = embedDescription;
this.embedColorHex = embedColorHex;
this.embedThumbnailUrl = embedThumbnailUrl;
this.footerEnabled = footerEnabled;
this.footerText = footerText;
}
/** @return webhook definition id */
public String getWebhookId() {
return webhookId;
}
/** @return raw json payload or null */
public String getJson() {
return json;
}
/** @return content string or null */
public String getContent() {
return content;
}
/** @return true if embed enabled */
public boolean isEmbedEnabled() {
return embedEnabled;
}
/** @return embed title */
public String getEmbedTitle() {
return embedTitle;
}
/** @return embed description */
public String getEmbedDescription() {
return embedDescription;
}
/** @return embed color hex */
public String getEmbedColorHex() {
return embedColorHex;
}
/** @return embed thumbnail url */
public String getEmbedThumbnailUrl() {
return embedThumbnailUrl;
}
/** @return footer enabled */
public boolean isFooterEnabled() {
return footerEnabled;
}
/** @return footer text */
public String getFooterText() {
return footerText;
}
/** @return true if using raw json mode */
public boolean isJsonMode() {
return json != null && !json.isEmpty();
}
}
@@ -1,129 +0,0 @@
package com.bencodez.votingplugin.webhook;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Executes webhook rewards using configured definitions.
*
*
* This class:
* <ul>
* <li>expands placeholders for payloads/headers/secrets</li>
* <li>builds either raw JSON payloads or Discord-friendly payloads</li>
* <li>queues requests on {@link WebhookService}</li>
* </ul>
*
*/
public final class WebhookRewardExecutor {
private final WebhookService service;
private final Map<String, WebhookDefinition> definitions;
/**
* Creates the executor.
*
* @param service webhook service
* @param definitions definitions by id
*/
public WebhookRewardExecutor(WebhookService service, Map<String, WebhookDefinition> definitions) {
this.service = Objects.requireNonNull(service, "service");
this.definitions = definitions == null ? Collections.emptyMap() : definitions;
}
/**
* Executes all entries.
*
* @param entries reward entries (may be empty)
* @param expander placeholder expander
*/
public void executeAll(List<WebhookRewardEntry> entries, PlaceholderExpander expander) {
if (entries == null || entries.isEmpty() || expander == null) {
return;
}
for (WebhookRewardEntry e : entries) {
executeOne(e, expander);
}
}
/**
* Executes one webhook entry.
*
* @param entry entry
* @param expander placeholder expander
*/
public void executeOne(WebhookRewardEntry entry, PlaceholderExpander expander) {
if (entry == null || expander == null) {
return;
}
WebhookDefinition def = definitions.get(entry.getWebhookId());
if (def == null || !def.isEnabled()) {
return;
}
String body = buildBody(entry, expander);
Map<String, String> expandedHeaders = expandHeaders(def.getHeaders(), expander);
String expandedSecret = expandSignatureSecret(def.getSignature(), expander);
service.submit(new WebhookRequest(def.getId(), body, expandedHeaders, expandedSecret));
}
private static String buildBody(WebhookRewardEntry entry, PlaceholderExpander expander) {
if (entry.isJsonMode()) {
return expander.expand(entry.getJson());
}
DiscordWebhookPayloadBuilder b = DiscordWebhookPayloadBuilder.create();
String content = expander.expand(entry.getContent());
if (content != null && !content.isEmpty()) {
b.content(content);
}
if (entry.isEmbedEnabled()) {
String title = expander.expand(entry.getEmbedTitle());
String desc = expander.expand(entry.getEmbedDescription());
String color = expander.expand(entry.getEmbedColorHex());
String thumb = expander.expand(entry.getEmbedThumbnailUrl());
String footer = expander.expand(entry.getFooterText());
b.embed(nullToEmpty(title), nullToEmpty(desc), nullToEmpty(color), nullToEmpty(thumb),
entry.isFooterEnabled(), nullToEmpty(footer));
}
return b.buildJson();
}
private static Map<String, String> expandHeaders(Map<String, String> headers, PlaceholderExpander expander) {
if (headers == null || headers.isEmpty()) {
return null;
}
Map<String, String> out = new HashMap<>();
for (Map.Entry<String, String> e : headers.entrySet()) {
if (e.getKey() == null || e.getKey().isEmpty()) {
continue;
}
out.put(e.getKey(), expander.expand(e.getValue()));
}
return out;
}
private static String expandSignatureSecret(WebhookSignatureConfig sig, PlaceholderExpander expander) {
if (sig == null || !sig.isEnabled()) {
return null;
}
String secret = sig.getSecret();
if (secret == null || secret.isEmpty()) {
return null;
}
return expander.expand(secret);
}
private static String nullToEmpty(String s) {
return s == null ? "" : s;
}
}
@@ -1,139 +0,0 @@
package com.bencodez.votingplugin.webhook;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.bukkit.configuration.ConfigurationSection;
/**
* Parses reward "Webhooks:" entries from a Bukkit {@link ConfigurationSection}.
*
*
* This is intentionally tolerant of missing fields, so presets won't hard-crash
* reward parsing.
*
*
*
* Supported YAML examples:
*
* <pre>
* Webhooks:
* - webhook: "my-api"
* json: |
* {"event":"vote","player":"{PLAYER}"}
*
* - webhook: "discord-staff"
* content:
* enabled: true
* message: "✅ {PLAYER} voted"
* embed:
* enabled: true
* title: "Vote"
* description: "{PLAYER} voted"
* color: "#2ecc71"
* thumbnail: "https://..."
* footer:
* enabled: true
* message: "{TIMESTAMP}"
* </pre>
*
*/
public final class WebhookRewardParser {
private WebhookRewardParser() {
// utility
}
/**
* Parses webhook reward entries from a reward section.
*
* @param rewardSection section that contains "Webhooks:" list
* @return parsed entries (never null)
*/
public static List<WebhookRewardEntry> parse(ConfigurationSection rewardSection) {
List<WebhookRewardEntry> out = new ArrayList<>();
if (rewardSection == null) {
return out;
}
List<Map<?, ?>> list = rewardSection.getMapList("Webhooks");
if (list == null || list.isEmpty()) {
return out;
}
for (Map<?, ?> raw : list) {
if (raw == null) {
continue;
}
String webhookId = string(raw.get("webhook"));
if (webhookId == null || webhookId.isEmpty()) {
continue;
}
String json = string(raw.get("json"));
String content = null;
boolean embedEnabled = false;
String embedTitle = null;
String embedDescription = null;
String embedColor = null;
String embedThumb = null;
boolean footerEnabled = false;
String footerText = null;
Object contentObj = raw.get("content");
if (contentObj instanceof Map) {
Map<?, ?> c = (Map<?, ?>) contentObj;
boolean enabled = bool(c.get("enabled"), true);
if (enabled) {
content = string(c.get("message"));
}
}
Object embedObj = raw.get("embed");
if (embedObj instanceof Map) {
Map<?, ?> e = (Map<?, ?>) embedObj;
embedEnabled = bool(e.get("enabled"), false);
if (embedEnabled) {
embedTitle = string(e.get("title"));
embedDescription = string(e.get("description"));
embedColor = string(e.get("color"));
embedThumb = string(e.get("thumbnail"));
Object footerObj = e.get("footer");
if (footerObj instanceof Map) {
Map<?, ?> f = (Map<?, ?>) footerObj;
footerEnabled = bool(f.get("enabled"), false);
if (footerEnabled) {
footerText = string(f.get("message"));
}
}
}
}
out.add(new WebhookRewardEntry(webhookId, json, content, embedEnabled, embedTitle, embedDescription,
embedColor, embedThumb, footerEnabled, footerText));
}
return out;
}
private static String string(Object o) {
if (o == null) {
return null;
}
return String.valueOf(o);
}
private static boolean bool(Object o, boolean def) {
if (o == null) {
return def;
}
if (o instanceof Boolean) {
return (Boolean) o;
}
return Boolean.parseBoolean(String.valueOf(o));
}
}
Loaded 100 of 278 files, more files were not shown because too many files have changed in this diff. Show more