mirror of
https://github.com/MeowLynxSea/MeowLogin.git
synced 2025-07-09 10:54:41 +00:00
first update for 1.0.0-SNAPSHOT
This commit is contained in:
parent
d53fcc13b8
commit
39d929d612
52
.gitignore
vendored
52
.gitignore
vendored
@ -1,24 +1,38 @@
|
|||||||
# Compiled class file
|
target/
|
||||||
*.class
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
# Log file
|
### IntelliJ IDEA ###
|
||||||
*.log
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
# BlueJ files
|
### Eclipse ###
|
||||||
*.ctxt
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
### NetBeans ###
|
||||||
.mtj.tmp/
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
# Package Files #
|
### VS Code ###
|
||||||
*.jar
|
.vscode/
|
||||||
*.war
|
|
||||||
*.nar
|
|
||||||
*.ear
|
|
||||||
*.zip
|
|
||||||
*.tar.gz
|
|
||||||
*.rar
|
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
### Mac OS ###
|
||||||
hs_err_pid*
|
.DS_Store
|
||||||
replay_pid*
|
|
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
7
.idea/encodings.xml
generated
Normal file
7
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
14
.idea/misc.xml
generated
Normal file
14
.idea/misc.xml
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="MavenProjectsManager">
|
||||||
|
<option name="originalFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
67
.idea/runConfigurations/MeowLogin_Run_.xml
generated
Normal file
67
.idea/runConfigurations/MeowLogin_Run_.xml
generated
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="MeowLogin Run " type="MavenRunConfiguration" factoryName="Maven">
|
||||||
|
<MavenSettings>
|
||||||
|
<option name="myGeneralSettings">
|
||||||
|
<MavenGeneralSettings>
|
||||||
|
<option name="alwaysUpdateSnapshots" value="false" />
|
||||||
|
<option name="checksumPolicy" value="NOT_SET" />
|
||||||
|
<option name="customMavenHome" />
|
||||||
|
<option name="emulateTerminal" value="false" />
|
||||||
|
<option name="failureBehavior" value="NOT_SET" />
|
||||||
|
<option name="localRepository" value="" />
|
||||||
|
<option name="mavenHome" value="已捆绑(Maven 3)" />
|
||||||
|
<option name="mavenHomeTypeForPersistence" value="BUNDLED3" />
|
||||||
|
<option name="nonRecursive" value="false" />
|
||||||
|
<option name="outputLevel" value="INFO" />
|
||||||
|
<option name="printErrorStackTraces" value="false" />
|
||||||
|
<option name="showDialogWithAdvancedSettings" value="false" />
|
||||||
|
<option name="threads" />
|
||||||
|
<option name="useMavenConfig" value="true" />
|
||||||
|
<option name="userSettingsFile" value="" />
|
||||||
|
<option name="workOffline" value="false" />
|
||||||
|
</MavenGeneralSettings>
|
||||||
|
</option>
|
||||||
|
<option name="myRunnerSettings">
|
||||||
|
<MavenRunnerSettings>
|
||||||
|
<option name="delegateBuildToMaven" value="false" />
|
||||||
|
<option name="environmentProperties">
|
||||||
|
<map />
|
||||||
|
</option>
|
||||||
|
<option name="jreName" value="#USE_PROJECT_JDK" />
|
||||||
|
<option name="mavenProperties">
|
||||||
|
<map />
|
||||||
|
</option>
|
||||||
|
<option name="passParentEnv" value="true" />
|
||||||
|
<option name="runMavenInBackground" value="true" />
|
||||||
|
<option name="skipTests" value="false" />
|
||||||
|
<option name="vmOptions" value="" />
|
||||||
|
</MavenRunnerSettings>
|
||||||
|
</option>
|
||||||
|
<option name="myRunnerParameters">
|
||||||
|
<MavenRunnerParameters>
|
||||||
|
<option name="cmdOptions" />
|
||||||
|
<option name="profiles">
|
||||||
|
<set />
|
||||||
|
</option>
|
||||||
|
<option name="goals">
|
||||||
|
<list>
|
||||||
|
<option value="clean" />
|
||||||
|
<option value="package" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="multimoduleDir" />
|
||||||
|
<option name="pomFileName" value="pom.xml" />
|
||||||
|
<option name="profilesMap">
|
||||||
|
<map />
|
||||||
|
</option>
|
||||||
|
<option name="projectsCmdOptionValues">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="resolveToWorkspace" value="false" />
|
||||||
|
<option name="workingDirPath" value="$PROJECT_DIR$" />
|
||||||
|
</MavenRunnerParameters>
|
||||||
|
</option>
|
||||||
|
</MavenSettings>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
124
.idea/uiDesigner.xml
generated
Normal file
124
.idea/uiDesigner.xml
generated
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Palette2">
|
||||||
|
<group name="Swing">
|
||||||
|
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="Button" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="RadioButton" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="CheckBox" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="Label" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||||
|
<preferred-size width="200" height="200" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||||
|
<preferred-size width="200" height="200" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||||
|
<preferred-size width="-1" height="20" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||||
|
</item>
|
||||||
|
</group>
|
||||||
|
</component>
|
||||||
|
</project>
|
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/MeowLogin" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
49
pom.xml
Normal file
49
pom.xml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>org.meowdream</groupId>
|
||||||
|
<artifactId>MeowLogin</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>17</source>
|
||||||
|
<target>17</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<!-- This adds the Spigot Maven repository to the build -->
|
||||||
|
<repository>
|
||||||
|
<id>spigot-repo</id>
|
||||||
|
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!--This adds the Spigot API artifact to the build -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot-api</artifactId>
|
||||||
|
<version>1.18.2-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
62
src/main/java/cn/meowdream/meowlogin/Main.java
Normal file
62
src/main/java/cn/meowdream/meowlogin/Main.java
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package cn.meowdream.meowlogin;
|
||||||
|
|
||||||
|
import cn.meowdream.meowlogin.listeners.Captcha;
|
||||||
|
import cn.meowdream.meowlogin.listeners.Auth;
|
||||||
|
import cn.meowdream.meowlogin.listeners.PluginCommand;
|
||||||
|
import cn.meowdream.meowlogin.utils.Metrics;
|
||||||
|
import org.bukkit.plugin.PluginDescriptionFile;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class Main extends JavaPlugin {
|
||||||
|
|
||||||
|
private Logger logger;
|
||||||
|
private PluginDescriptionFile descriptionFile;
|
||||||
|
private static Main instance;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
logger = getLogger();
|
||||||
|
instance = this;
|
||||||
|
registerConfig();
|
||||||
|
descriptionFile = getDescription();
|
||||||
|
|
||||||
|
Metrics metrics = new Metrics(this,21668);
|
||||||
|
|
||||||
|
getServer().getPluginManager().registerEvents(new Auth(),this);
|
||||||
|
getServer().getPluginManager().registerEvents(new Captcha(),this);
|
||||||
|
|
||||||
|
getCommand("mlreload").setExecutor(new PluginCommand());
|
||||||
|
logger.info("""
|
||||||
|
|
||||||
|
|
||||||
|
=====================================================================
|
||||||
|
|
||||||
|
__ ___ __ _ \s
|
||||||
|
/ |/ / ___ ___ _ __ / / ___ ___ _ (_) ___\s
|
||||||
|
/ /|_/ / / -_)/ _ \\| |/|/ / / /__/ _ \\ / _ `/ / / / _ \\
|
||||||
|
/_/ /_/ \\__/ \\___/|__,__/ /____/\\___/ \\_, / /_/ /_//_/
|
||||||
|
/___/ \s
|
||||||
|
|
||||||
|
Author: Mewn_Lynsi
|
||||||
|
|
||||||
|
=====================================================================
|
||||||
|
""");
|
||||||
|
logger.info("Enabled " + descriptionFile.getName() + " " + descriptionFile.getVersion() + "...");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
logger.info("Disabled " + descriptionFile.getName() + "...");
|
||||||
|
logger = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerConfig() {
|
||||||
|
saveDefaultConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Main getInstance(){
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
125
src/main/java/cn/meowdream/meowlogin/listeners/Auth.java
Normal file
125
src/main/java/cn/meowdream/meowlogin/listeners/Auth.java
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package cn.meowdream.meowlogin.listeners;
|
||||||
|
|
||||||
|
import cn.meowdream.meowlogin.Main;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
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.PlayerJoinEvent;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import static org.bukkit.Bukkit.getLogger;
|
||||||
|
|
||||||
|
public class Auth implements Listener {
|
||||||
|
|
||||||
|
private String apiUrl;
|
||||||
|
|
||||||
|
public static String getCurrentDateTime() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
return now.format(formatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String encodeURI(String input) {
|
||||||
|
return URLEncoder.encode(input, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
String playerName = player.getName(),
|
||||||
|
playerIP = player.getAddress().getAddress().toString();
|
||||||
|
|
||||||
|
FileConfiguration config = Main.getInstance().getConfig();
|
||||||
|
|
||||||
|
try {
|
||||||
|
apiUrl = config.getString("api-server");
|
||||||
|
String encodedName = URLEncoder.encode(playerName, StandardCharsets.UTF_8);
|
||||||
|
URL url = new URL(apiUrl + "/login/checkStatus?username=" + encodedName + "&&IP=" + playerIP + "&&app=" + encodeURI(config.getString("server-name")) + "&&desc=" + encodeURI(config.getString("server-desc")));
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
response.append(line);
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
|
||||||
|
int apiResponse = Integer.parseInt(response.toString());
|
||||||
|
if (apiResponse == 0) {
|
||||||
|
event.setJoinMessage("§a" + playerName + "§f 从沉睡中苏醒了");
|
||||||
|
} else if (apiResponse == -1) {
|
||||||
|
player.kickPlayer("""
|
||||||
|
无法连接到服务器
|
||||||
|
|
||||||
|
§c原因:您尚未登录
|
||||||
|
|
||||||
|
§8==================================
|
||||||
|
|
||||||
|
§7请前往本服务器QQ频道,使用频道机器人进行登录。
|
||||||
|
详细信息请私信§f“汐汐酱”§7发送“/帮助”指令获取或查阅本服务器Wiki
|
||||||
|
完成以上操作后,请重新连接服务器
|
||||||
|
|
||||||
|
频道号:""" + config.getString("channel-id") + """
|
||||||
|
|
||||||
|
§7更多帮助请参考:§f""" + config.getString("reference-page") + """
|
||||||
|
§7
|
||||||
|
|
||||||
|
|
||||||
|
""" + getCurrentDateTime());
|
||||||
|
} else {
|
||||||
|
player.kickPlayer("""
|
||||||
|
无法连接到服务器
|
||||||
|
|
||||||
|
§c原因:无效的登录响应
|
||||||
|
|
||||||
|
§8==================================
|
||||||
|
|
||||||
|
§7您的配置一切正常!
|
||||||
|
该故障可能由服务器配置错误引起,请联系管理员解决
|
||||||
|
|
||||||
|
频道号:""" + config.getString("channel-id") + """
|
||||||
|
|
||||||
|
§7更多帮助请参考:§f""" + config.getString("reference-page") + """
|
||||||
|
§7
|
||||||
|
|
||||||
|
|
||||||
|
""" + getCurrentDateTime());
|
||||||
|
getLogger().info("Unexpected response from server: " + apiResponse);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
player.kickPlayer("""
|
||||||
|
无法连接到服务器
|
||||||
|
|
||||||
|
§c原因:验证服务器离线
|
||||||
|
|
||||||
|
§8==================================
|
||||||
|
|
||||||
|
§7我们暂时无法连接到身份验证服务,请稍候再试
|
||||||
|
若长时间无法进入服务器,请联系管理员解决
|
||||||
|
|
||||||
|
频道号:""" + config.getString("channel-id") + """
|
||||||
|
|
||||||
|
§7更多帮助请参考:§f""" + config.getString("reference-page") + """
|
||||||
|
§7
|
||||||
|
|
||||||
|
|
||||||
|
""" + getCurrentDateTime());
|
||||||
|
getLogger().log(Level.SEVERE, "Error while sending player join event to API.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
583
src/main/java/cn/meowdream/meowlogin/listeners/Captcha.java
Normal file
583
src/main/java/cn/meowdream/meowlogin/listeners/Captcha.java
Normal file
@ -0,0 +1,583 @@
|
|||||||
|
package cn.meowdream.meowlogin.listeners;
|
||||||
|
|
||||||
|
import cn.meowdream.meowlogin.Main;
|
||||||
|
import cn.meowdream.meowlogin.utils.FancyCaptchaGenerator;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.boss.BarColor;
|
||||||
|
import org.bukkit.boss.BarStyle;
|
||||||
|
import org.bukkit.boss.BossBar;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||||
|
import org.bukkit.event.player.*;
|
||||||
|
import org.bukkit.event.server.PluginDisableEvent;
|
||||||
|
import org.bukkit.event.server.PluginEnableEvent;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.MapMeta;
|
||||||
|
import org.bukkit.map.*;
|
||||||
|
import org.bukkit.potion.PotionEffect;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static cn.meowdream.meowlogin.listeners.Auth.getCurrentDateTime;
|
||||||
|
import static org.bukkit.Bukkit.getLogger;
|
||||||
|
|
||||||
|
public class Captcha implements Listener {
|
||||||
|
|
||||||
|
private final HashMap<UUID, ItemStack> playerItems = new HashMap<>();
|
||||||
|
private final HashMap<UUID, MapView> playerMaps = new HashMap<>();
|
||||||
|
private final HashMap<UUID, String[]> playerCaptcha = new HashMap<>();
|
||||||
|
private final HashMap<UUID, Boolean> playerVerification = new HashMap<>();
|
||||||
|
// 用于存储玩家的BossBar
|
||||||
|
private final HashMap<UUID, BukkitTask> countdownTasks = new HashMap<>(); // 用于存储倒计时任务
|
||||||
|
private final HashMap<UUID, BossBar> playerBossBars = new HashMap<>();
|
||||||
|
private final File dataFile = new File("plugins/MeowLogin/data.yml");
|
||||||
|
private final FileConfiguration dataConfig = YamlConfiguration.loadConfiguration(dataFile);
|
||||||
|
FileConfiguration config = Main.getInstance().getConfig();
|
||||||
|
|
||||||
|
private final int COUNTDOWN_DURATION = config.getInt("captcha-countdown-duration");
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
if(!config.getBoolean("enable-captcha")){
|
||||||
|
playerVerification.put(player.getUniqueId(), true);
|
||||||
|
player.sendMessage("§a亲爱的" + player.getDisplayName() + ",欢迎来到" + config.getString("server-name") + "!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.setInvulnerable(true);
|
||||||
|
|
||||||
|
if (playerVerification.containsKey(player.getUniqueId())) {
|
||||||
|
if (playerVerification.get(player.getUniqueId())) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
playerVerification.put(player.getUniqueId(), false);
|
||||||
|
player.sendMessage(config.getString("captcha-needed"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playerVerification.put(player.getUniqueId(), false);
|
||||||
|
player.sendMessage(config.getString("captcha-needed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用BukkitScheduler安排一个同步任务来应用效果
|
||||||
|
Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
|
||||||
|
// 赋予失明效果
|
||||||
|
player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, Integer.MAX_VALUE, 1, false, false));
|
||||||
|
|
||||||
|
// 赋予夜视效果
|
||||||
|
player.addPotionEffect(new PotionEffect(PotionEffectType.NIGHT_VISION, Integer.MAX_VALUE, 1, false, false));
|
||||||
|
});
|
||||||
|
|
||||||
|
player.sendTitle(ChatColor.BLUE + "风控系统", ChatColor.WHITE + "请在聊天框中输入验证码", 10, 70, 20);
|
||||||
|
|
||||||
|
ItemStack item = player.getInventory().getItemInMainHand();
|
||||||
|
|
||||||
|
// 检查是否手持物品
|
||||||
|
if (!isCustomMap(item)) {
|
||||||
|
// 存储玩家手持的物品
|
||||||
|
playerItems.put(player.getUniqueId(), item);
|
||||||
|
|
||||||
|
// 删除玩家手持的物品
|
||||||
|
player.getInventory().setItemInMainHand(new ItemStack(Material.AIR));
|
||||||
|
|
||||||
|
item = new ItemStack(Material.FILLED_MAP, 1);
|
||||||
|
MapMeta meta = (MapMeta) item.getItemMeta();
|
||||||
|
|
||||||
|
// 创建新地图并添加到玩家手中
|
||||||
|
MapView mapView = null;
|
||||||
|
if (meta.hasMapView()) {
|
||||||
|
if (meta != null) {
|
||||||
|
mapView = meta.getMapView();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mapView = Bukkit.createMap(player.getWorld());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapView != null) {
|
||||||
|
mapView.getRenderers().clear(); // 清除默认渲染器
|
||||||
|
mapView.addRenderer(new MapRenderer() {
|
||||||
|
@Override
|
||||||
|
public void render(MapView map, MapCanvas canvas, Player player) {
|
||||||
|
String base64Image;
|
||||||
|
String[] captcha;
|
||||||
|
if (playerCaptcha.containsKey(player.getUniqueId())) {
|
||||||
|
captcha = playerCaptcha.get(player.getUniqueId());
|
||||||
|
} else {
|
||||||
|
FancyCaptchaGenerator fancyCaptchaGenerator = new FancyCaptchaGenerator();
|
||||||
|
captcha = fancyCaptchaGenerator.generateCaptcha();
|
||||||
|
playerCaptcha.put(player.getUniqueId(), captcha);
|
||||||
|
}
|
||||||
|
base64Image = captcha[0];
|
||||||
|
// 将BASE64编码的图片渲染到地图上
|
||||||
|
try {
|
||||||
|
byte[] imageBytes = Base64.getDecoder().decode(base64Image);
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes);
|
||||||
|
BufferedImage image = ImageIO.read(bis);
|
||||||
|
|
||||||
|
// 确保图片大小符合要求 (128x128)
|
||||||
|
if (image.getWidth() != 128 || image.getHeight() != 128) {
|
||||||
|
getLogger().warning("Image dimensions are not 128x128 pixels.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将图片像素绘制到地图画布上
|
||||||
|
for (int y = 0; y < 128; y++) {
|
||||||
|
for (int x = 0; x < 128; x++) {
|
||||||
|
int colorValue = image.getRGB(x, y);
|
||||||
|
Color color = new Color(colorValue, true); // 使用 true 表示保留 alpha 通道
|
||||||
|
canvas.setPixel(x, y, MapPalette.matchColor(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
getLogger().warning("Error decoding BASE64 image: " + e.getMessage());
|
||||||
|
}
|
||||||
|
// 在地图上添加文字
|
||||||
|
canvas.drawText(10, 10, MinecraftFont.Font, "MeowCaptcha");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
meta.setMapView(mapView);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta != null) {
|
||||||
|
meta.setCustomModelData(1); // 设置特殊的 CustomModelData,用于区分自定义地图
|
||||||
|
}
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
|
||||||
|
player.getInventory().setItem(player.getInventory().getHeldItemSlot(), item);
|
||||||
|
|
||||||
|
// 存储玩家地图
|
||||||
|
playerMaps.put(player.getUniqueId(), mapView);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个新的BossBar,并设置样式和颜色
|
||||||
|
BossBar bossBar = Bukkit.createBossBar("Verification Countdown", BarColor.BLUE, BarStyle.SOLID);
|
||||||
|
bossBar.setProgress(1.0); // 设置初始进度为满
|
||||||
|
bossBar.addPlayer(player); // 将玩家添加到BossBar中
|
||||||
|
|
||||||
|
// 将BossBar存储到Map中
|
||||||
|
playerBossBars.put(player.getUniqueId(), bossBar);
|
||||||
|
|
||||||
|
// 启动一个倒计时任务
|
||||||
|
BukkitTask countdownTask = new BukkitRunnable() {
|
||||||
|
int countdown = COUNTDOWN_DURATION;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// 计算剩余时间的百分比
|
||||||
|
double progress = (double) countdown / COUNTDOWN_DURATION;
|
||||||
|
|
||||||
|
// 更新BossBar的进度
|
||||||
|
if (progress < 0) {
|
||||||
|
progress = 0;
|
||||||
|
} else if (progress > 1) {
|
||||||
|
progress = 1;
|
||||||
|
}
|
||||||
|
bossBar.setProgress(progress);
|
||||||
|
|
||||||
|
// 更新BossBar的进度
|
||||||
|
bossBar.setProgress(progress);
|
||||||
|
|
||||||
|
// 更新BossBar显示的时间
|
||||||
|
bossBar.setTitle("验证倒计时: " + countdown + "s");
|
||||||
|
|
||||||
|
if (progress < 0.5) {
|
||||||
|
bossBar.setColor(BarColor.YELLOW);
|
||||||
|
}
|
||||||
|
if (progress < 0.2) {
|
||||||
|
bossBar.setColor(BarColor.RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果倒计时结束且玩家未验证通过,则踢出玩家
|
||||||
|
if (countdown <= 0 && !playerVerification.getOrDefault(player.getUniqueId(), false)) {
|
||||||
|
// 使用BukkitScheduler安排一个同步任务来移除效果
|
||||||
|
Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
|
||||||
|
do {
|
||||||
|
player.removePotionEffect(PotionEffectType.BLINDNESS);
|
||||||
|
} while (player.hasPotionEffect(PotionEffectType.BLINDNESS));
|
||||||
|
|
||||||
|
do {
|
||||||
|
player.removePotionEffect(PotionEffectType.NIGHT_VISION);
|
||||||
|
} while (player.hasPotionEffect(PotionEffectType.NIGHT_VISION));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
playerVerification.remove(player.getUniqueId());
|
||||||
|
playerCaptcha.remove(player.getUniqueId());
|
||||||
|
// 检查玩家是否存在存储的物品和地图
|
||||||
|
if (playerItems.containsKey(player.getUniqueId()) && playerMaps.containsKey(player.getUniqueId())) {
|
||||||
|
// 还原玩家手中的物品
|
||||||
|
player.getInventory().setItemInMainHand(playerItems.get(player.getUniqueId()));
|
||||||
|
|
||||||
|
// 删除玩家手中的地图
|
||||||
|
MapView mapView = playerMaps.get(player.getUniqueId());
|
||||||
|
if (mapView != null) {
|
||||||
|
mapView.removeRenderer(mapView.getRenderers().get(0));
|
||||||
|
player.getInventory().removeItem(new ItemStack(Material.FILLED_MAP, 1, (short) mapView.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
playerItems.remove(player.getUniqueId());
|
||||||
|
playerMaps.remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除玩家对应的BossBar和倒计时任务
|
||||||
|
BossBar bossBar = playerBossBars.remove(player.getUniqueId());
|
||||||
|
if (bossBar != null) {
|
||||||
|
bossBar.removeAll(); // 移除所有玩家
|
||||||
|
}
|
||||||
|
|
||||||
|
player.kickPlayer("""
|
||||||
|
被服务器踢出
|
||||||
|
|
||||||
|
§c原因:验证超时
|
||||||
|
|
||||||
|
§8==================================
|
||||||
|
|
||||||
|
§7进入服务器后,请在规定时间内通过人机验证。
|
||||||
|
|
||||||
|
频道号:""" + config.getString("channel-id") + """
|
||||||
|
|
||||||
|
§7更多帮助请参考:§f""" + config.getString("reference-page") + """
|
||||||
|
§7
|
||||||
|
|
||||||
|
|
||||||
|
""" + getCurrentDateTime());
|
||||||
|
|
||||||
|
cancel(); // 停止倒计时任务
|
||||||
|
}
|
||||||
|
|
||||||
|
countdown--;
|
||||||
|
}
|
||||||
|
}.runTaskTimer(Main.getInstance(), 0L, 20L); // 每秒更新一次
|
||||||
|
|
||||||
|
// 存储倒计时任务,以便后续取消
|
||||||
|
countdownTasks.put(player.getUniqueId(), countdownTask);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerDropItem(PlayerDropItemEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
ItemStack item = event.getItemDrop().getItemStack();
|
||||||
|
|
||||||
|
// 检查是否扔出的物品是地图,并且是自定义地图
|
||||||
|
if (item.getType() == Material.FILLED_MAP && isCustomMap(item) && playerMaps.containsKey(player.getUniqueId())) {
|
||||||
|
event.setCancelled(true); // 阻止玩家扔出地图
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
playerVerification.remove(playerId);
|
||||||
|
playerCaptcha.remove(playerId);
|
||||||
|
// 检查玩家是否存在存储的物品和地图
|
||||||
|
if (playerItems.containsKey(player.getUniqueId()) && playerMaps.containsKey(player.getUniqueId())) {
|
||||||
|
// 还原玩家手中的物品
|
||||||
|
player.getInventory().setItemInMainHand(playerItems.get(player.getUniqueId()));
|
||||||
|
|
||||||
|
// 删除玩家手中的地图
|
||||||
|
MapView mapView = playerMaps.get(player.getUniqueId());
|
||||||
|
if (mapView != null) {
|
||||||
|
mapView.removeRenderer(mapView.getRenderers().get(0));
|
||||||
|
player.getInventory().removeItem(new ItemStack(Material.FILLED_MAP, 1, (short) mapView.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
playerItems.remove(player.getUniqueId());
|
||||||
|
playerMaps.remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除玩家对应的BossBar和倒计时任务
|
||||||
|
BossBar bossBar = playerBossBars.remove(playerId);
|
||||||
|
if (bossBar != null) {
|
||||||
|
bossBar.removeAll(); // 移除所有玩家
|
||||||
|
}
|
||||||
|
|
||||||
|
BukkitTask countdownTask = countdownTasks.remove(playerId);
|
||||||
|
if (countdownTask != null) {
|
||||||
|
countdownTask.cancel(); // 取消倒计时任务
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用BukkitScheduler安排一个同步任务来移除效果
|
||||||
|
Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
|
||||||
|
// 取消失明效果
|
||||||
|
player.removePotionEffect(PotionEffectType.BLINDNESS);
|
||||||
|
|
||||||
|
// 取消夜视效果
|
||||||
|
player.removePotionEffect(PotionEffectType.NIGHT_VISION);
|
||||||
|
});
|
||||||
|
|
||||||
|
playerVerification.remove(playerId);
|
||||||
|
playerCaptcha.remove(playerId);
|
||||||
|
playerItems.remove(playerId);
|
||||||
|
playerMaps.remove(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerKick(PlayerKickEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
playerVerification.remove(playerId);
|
||||||
|
playerCaptcha.remove(playerId);
|
||||||
|
// 检查玩家是否存在存储的物品和地图
|
||||||
|
if (playerItems.containsKey(player.getUniqueId()) && playerMaps.containsKey(player.getUniqueId())) {
|
||||||
|
// 还原玩家手中的物品
|
||||||
|
player.getInventory().setItemInMainHand(playerItems.get(player.getUniqueId()));
|
||||||
|
|
||||||
|
// 删除玩家手中的地图
|
||||||
|
MapView mapView = playerMaps.get(player.getUniqueId());
|
||||||
|
if (mapView != null) {
|
||||||
|
mapView.removeRenderer(mapView.getRenderers().get(0));
|
||||||
|
player.getInventory().removeItem(new ItemStack(Material.FILLED_MAP, 1, (short) mapView.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
playerItems.remove(player.getUniqueId());
|
||||||
|
playerMaps.remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除玩家对应的BossBar和倒计时任务
|
||||||
|
BossBar bossBar = playerBossBars.remove(playerId);
|
||||||
|
if (bossBar != null) {
|
||||||
|
bossBar.removeAll(); // 移除所有玩家
|
||||||
|
}
|
||||||
|
|
||||||
|
BukkitTask countdownTask = countdownTasks.remove(playerId);
|
||||||
|
if (countdownTask != null) {
|
||||||
|
countdownTask.cancel(); // 取消倒计时任务
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用BukkitScheduler安排一个同步任务来移除效果
|
||||||
|
Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
|
||||||
|
// 取消失明效果
|
||||||
|
player.removePotionEffect(PotionEffectType.BLINDNESS);
|
||||||
|
|
||||||
|
// 取消夜视效果
|
||||||
|
player.removePotionEffect(PotionEffectType.NIGHT_VISION);
|
||||||
|
});
|
||||||
|
|
||||||
|
playerVerification.remove(playerId);
|
||||||
|
playerCaptcha.remove(playerId);
|
||||||
|
playerItems.remove(playerId);
|
||||||
|
playerMaps.remove(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为自定义地图
|
||||||
|
private boolean isCustomMap(ItemStack item) {
|
||||||
|
if (item.hasItemMeta() && item.getItemMeta().hasCustomModelData()) {
|
||||||
|
int customModelData = item.getItemMeta().getCustomModelData();
|
||||||
|
return customModelData == 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle player commands
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
|
||||||
|
UUID playerId = event.getPlayer().getUniqueId();
|
||||||
|
if (!playerVerification.getOrDefault(playerId, false)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
event.getPlayer().sendMessage(config.getString("captcha-not-authed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle player chat
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerChat(AsyncPlayerChatEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
if (!playerVerification.getOrDefault(playerId, false)) {
|
||||||
|
if (playerCaptcha.containsKey(player.getUniqueId())) {
|
||||||
|
String[] captcha;
|
||||||
|
captcha = playerCaptcha.get(player.getUniqueId());
|
||||||
|
if (Objects.equals(captcha[1], event.getMessage())) {
|
||||||
|
playerVerification.put(player.getUniqueId(), true);
|
||||||
|
player.sendTitle(ChatColor.GREEN + "验证成功", ChatColor.YELLOW + "您现在可正常游戏了", 10, 70, 20);
|
||||||
|
player.sendMessage("§a亲爱的" + player.getDisplayName() + ",欢迎来到" + config.getString("server-name") + "!");
|
||||||
|
if (playerItems.containsKey(player.getUniqueId()) && playerMaps.containsKey(player.getUniqueId())) {
|
||||||
|
// 如果玩家验证通过,则取消倒计时任务和移除 BossBar
|
||||||
|
cancelCountdown(player);
|
||||||
|
|
||||||
|
player.setInvulnerable(false);
|
||||||
|
|
||||||
|
// 还原玩家手中的物品
|
||||||
|
player.getInventory().setItemInMainHand(playerItems.get(player.getUniqueId()));
|
||||||
|
|
||||||
|
// 删除玩家手中的地图
|
||||||
|
MapView mapView = playerMaps.get(player.getUniqueId());
|
||||||
|
mapView.removeRenderer(mapView.getRenderers().get(0));
|
||||||
|
player.getInventory().removeItem(new ItemStack(Material.FILLED_MAP, 1, (short) mapView.getId()));
|
||||||
|
|
||||||
|
// 使用BukkitScheduler安排一个同步任务来移除效果
|
||||||
|
Bukkit.getScheduler().runTask(Main.getInstance(), () -> {
|
||||||
|
do {
|
||||||
|
player.removePotionEffect(PotionEffectType.BLINDNESS);
|
||||||
|
} while (player.hasPotionEffect(PotionEffectType.BLINDNESS));
|
||||||
|
|
||||||
|
do {
|
||||||
|
player.removePotionEffect(PotionEffectType.NIGHT_VISION);
|
||||||
|
} while (player.hasPotionEffect(PotionEffectType.NIGHT_VISION));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
playerItems.remove(player.getUniqueId());
|
||||||
|
playerMaps.remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event.getPlayer().sendMessage(config.getString("captcha-code-error"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event.getPlayer().sendMessage(config.getString("captcha-not-authed"));
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle player interactions
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
UUID playerId = event.getPlayer().getUniqueId();
|
||||||
|
if (!playerVerification.getOrDefault(playerId, false)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理玩家移动
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerMove(PlayerMoveEvent event) {
|
||||||
|
UUID playerId = event.getPlayer().getUniqueId();
|
||||||
|
|
||||||
|
// 检查玩家是否未验证
|
||||||
|
if (!playerVerification.getOrDefault(playerId, false)) {
|
||||||
|
// 获取当前位置和目标位置
|
||||||
|
Location from = event.getFrom();
|
||||||
|
Location to = event.getTo();
|
||||||
|
|
||||||
|
// 将水平方向上的移动分量设置为零
|
||||||
|
to.setX(from.getX());
|
||||||
|
to.setY(from.getY());
|
||||||
|
to.setZ(from.getZ());
|
||||||
|
|
||||||
|
// 将水平方向上的速度设置为零
|
||||||
|
event.getPlayer().setVelocity(new Vector(0, 0, 0));
|
||||||
|
|
||||||
|
// 更新玩家的目标位置
|
||||||
|
event.setTo(to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onItemSwitch(PlayerItemHeldEvent event) {
|
||||||
|
UUID playerId = event.getPlayer().getUniqueId();
|
||||||
|
if (!playerVerification.getOrDefault(playerId, false)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) {
|
||||||
|
UUID playerId = event.getPlayer().getUniqueId();
|
||||||
|
if (!playerVerification.getOrDefault(playerId, false)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
event.getPlayer().sendMessage(config.getString("captcha-not-authed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onInventoryOpenEvent(InventoryOpenEvent event) {
|
||||||
|
UUID playerId = event.getPlayer().getUniqueId();
|
||||||
|
if (!playerVerification.getOrDefault(playerId, false)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
event.getPlayer().sendMessage(config.getString("captcha-not-authed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onDisable(PluginDisableEvent event) {
|
||||||
|
saveData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onEnable(PluginEnableEvent event) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveData() {
|
||||||
|
dataConfig.set("playerItems", null);
|
||||||
|
dataConfig.set("playerMaps", null);
|
||||||
|
|
||||||
|
for (UUID playerId : playerItems.keySet()) {
|
||||||
|
dataConfig.set("playerItems." + playerId, playerItems.get(playerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UUID playerId : playerMaps.keySet()) {
|
||||||
|
dataConfig.set("playerMaps." + playerId, playerMaps.get(playerId).getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataConfig.save(dataFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
getLogger().severe(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadData() {
|
||||||
|
if (dataFile.exists()) {
|
||||||
|
ConfigurationSection itemsSection = dataConfig.getConfigurationSection("playerItems");
|
||||||
|
if (itemsSection != null) {
|
||||||
|
for (String playerIdStr : itemsSection.getKeys(false)) {
|
||||||
|
UUID playerId = UUID.fromString(playerIdStr);
|
||||||
|
ItemStack itemStack = (ItemStack) itemsSection.get(playerIdStr);
|
||||||
|
playerItems.put(playerId, itemStack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationSection mapsSection = dataConfig.getConfigurationSection("playerMaps");
|
||||||
|
if (mapsSection != null) {
|
||||||
|
for (String playerIdStr : mapsSection.getKeys(false)) {
|
||||||
|
UUID playerId = UUID.fromString(playerIdStr);
|
||||||
|
int mapViewId = mapsSection.getInt(playerIdStr);
|
||||||
|
MapView mapView = Bukkit.getMap(mapViewId);
|
||||||
|
if (mapView != null) {
|
||||||
|
playerMaps.put(playerId, mapView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelCountdown(Player player) {
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
BukkitTask countdownTask = countdownTasks.remove(playerId);
|
||||||
|
if (countdownTask != null) {
|
||||||
|
countdownTask.cancel(); // 取消倒计时任务
|
||||||
|
}
|
||||||
|
|
||||||
|
BossBar bossBar = playerBossBars.remove(playerId);
|
||||||
|
if (bossBar != null) {
|
||||||
|
bossBar.removeAll(); // 移除所有玩家
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package cn.meowdream.meowlogin.listeners;
|
||||||
|
|
||||||
|
import cn.meowdream.meowlogin.Main;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
|
||||||
|
public class PluginCommand implements CommandExecutor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command pluginCommand, String label, String[] args) {
|
||||||
|
if (sender.hasPermission("meowlogin.reloadconfig")) {
|
||||||
|
Main.getInstance().reloadConfig();
|
||||||
|
sender.sendMessage("配置文件已重新加载。");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
sender.sendMessage("你没有权限执行此命令。");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
package cn.meowdream.meowlogin.utils;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.QuadCurve2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class FancyCaptchaGenerator {
|
||||||
|
|
||||||
|
public String[] generateCaptcha() {
|
||||||
|
// Generate random captcha text
|
||||||
|
String captchaText = generateRandomCaptchaText();
|
||||||
|
|
||||||
|
// Generate image
|
||||||
|
BufferedImage image = createCaptchaImage(captchaText);
|
||||||
|
|
||||||
|
// Convert image to base64
|
||||||
|
String base64Image = encodeImageToBase64(image);
|
||||||
|
|
||||||
|
return new String[]{base64Image, captchaText};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateRandomCaptchaText() {
|
||||||
|
String characters = "0123456789@#%&";
|
||||||
|
Random random = new Random();
|
||||||
|
int length = random.nextInt(2) + 4; // Random length between 4 and 6
|
||||||
|
StringBuilder captchaText = new StringBuilder();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
captchaText.append(characters.charAt(random.nextInt(characters.length())));
|
||||||
|
}
|
||||||
|
return captchaText.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BufferedImage createCaptchaImage(String text) {
|
||||||
|
int width = 128;
|
||||||
|
int height = 128;
|
||||||
|
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D graphics = image.createGraphics();
|
||||||
|
|
||||||
|
// Set background gradient
|
||||||
|
Color startColor = getRandomLightColor();
|
||||||
|
Color endColor = getRandomLightColor();
|
||||||
|
int startX = new Random().nextInt(width);
|
||||||
|
int startY = new Random().nextInt(height);
|
||||||
|
int endX = new Random().nextInt(width);
|
||||||
|
int endY = new Random().nextInt(height);
|
||||||
|
GradientPaint gradient = new GradientPaint(startX, startY, startColor, endX, endY, endColor);
|
||||||
|
graphics.setPaint(gradient);
|
||||||
|
graphics.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Draw random curves
|
||||||
|
graphics.setColor(getRandomLightColor());
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
int x1 = new Random().nextInt(width);
|
||||||
|
int y1 = new Random().nextInt(height);
|
||||||
|
int x2 = new Random().nextInt(width);
|
||||||
|
int y2 = new Random().nextInt(height);
|
||||||
|
int ctrlx = new Random().nextInt(width);
|
||||||
|
int ctrly = new Random().nextInt(height);
|
||||||
|
graphics.draw(new QuadCurve2D.Float(x1, y1, ctrlx, ctrly, x2, y2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw random circles (outline only)
|
||||||
|
graphics.setStroke(new BasicStroke(2)); // Set stroke width
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
int circleX = new Random().nextInt(width);
|
||||||
|
int circleY = new Random().nextInt(height);
|
||||||
|
int circleSize = new Random().nextInt(20) + 10; // Random size between 10 and 30
|
||||||
|
graphics.setColor(getRandomLightColor());
|
||||||
|
graphics.drawOval(circleX, circleY, circleSize, circleSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw random triangles (outline only)
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
int[] xPoints = {new Random().nextInt(width), new Random().nextInt(width), new Random().nextInt(width)};
|
||||||
|
int[] yPoints = {new Random().nextInt(height), new Random().nextInt(height), new Random().nextInt(height)};
|
||||||
|
graphics.setColor(getRandomLightColor());
|
||||||
|
graphics.drawPolygon(xPoints, yPoints, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw text with random rotation
|
||||||
|
graphics.setFont(new Font("Arial", Font.BOLD, 24));
|
||||||
|
FontMetrics fm = graphics.getFontMetrics();
|
||||||
|
int textWidth = fm.stringWidth(text);
|
||||||
|
int x = (width - textWidth) / 2;
|
||||||
|
int y = height / 2 + fm.getAscent() / 2;
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
graphics.setColor(getRandomLightColor());
|
||||||
|
int rotation = new Random().nextInt(40) - 20; // Random rotation between -20 and 20 degrees
|
||||||
|
graphics.rotate(Math.toRadians(rotation), x, y); // Rotate around the center of the character
|
||||||
|
graphics.drawString(String.valueOf(text.charAt(i)), x, y);
|
||||||
|
x += fm.charWidth(text.charAt(i));
|
||||||
|
graphics.rotate(Math.toRadians(-rotation), x, y); // Reset rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
graphics.dispose();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color getRandomDarkColor() {
|
||||||
|
Random random = new Random();
|
||||||
|
int red = 128 + random.nextInt(128);
|
||||||
|
int green = 128 + random.nextInt(128);
|
||||||
|
int blue = 128 + random.nextInt(128);
|
||||||
|
return new Color(red, green, blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color getRandomLightColor() {
|
||||||
|
Random random = new Random();
|
||||||
|
int red = random.nextInt(128);
|
||||||
|
int green = random.nextInt(128);
|
||||||
|
int blue = random.nextInt(128);
|
||||||
|
return new Color(red, green, blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String encodeImageToBase64(BufferedImage image) {
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
ImageIO.write(image, "png", bos);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
byte[] imageBytes = bos.toByteArray();
|
||||||
|
return Base64.getEncoder().encodeToString(imageBytes);
|
||||||
|
}
|
||||||
|
}
|
881
src/main/java/cn/meowdream/meowlogin/utils/Metrics.java
Normal file
881
src/main/java/cn/meowdream/meowlogin/utils/Metrics.java
Normal file
@ -0,0 +1,881 @@
|
|||||||
|
/*
|
||||||
|
* This Metrics class was auto-generated and can be copied into your project if you are
|
||||||
|
* not using a build tool like Gradle or Maven for dependency management.
|
||||||
|
*
|
||||||
|
* IMPORTANT: You are not allowed to modify this class, except changing the package.
|
||||||
|
*
|
||||||
|
* Disallowed modifications include but are not limited to:
|
||||||
|
* - Remove the option for users to opt-out
|
||||||
|
* - Change the frequency for data submission
|
||||||
|
* - Obfuscate the code (every obfuscator should allow you to make an exception for specific files)
|
||||||
|
* - Reformat the code (if you use a linter, add an exception)
|
||||||
|
*
|
||||||
|
* Violations will result in a ban of your plugin and account from bStats.
|
||||||
|
*/
|
||||||
|
package cn.meowdream.meowlogin.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
public class Metrics {
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
|
||||||
|
private final MetricsBase metricsBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Metrics instance.
|
||||||
|
*
|
||||||
|
* @param plugin Your plugin instance.
|
||||||
|
* @param serviceId The id of the service. It can be found at <a
|
||||||
|
* href="https://bstats.org/what-is-my-plugin-id">What is my plugin id?</a>
|
||||||
|
*/
|
||||||
|
public Metrics(JavaPlugin plugin, int serviceId) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
// Get the config file
|
||||||
|
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
|
||||||
|
File configFile = new File(bStatsFolder, "config.yml");
|
||||||
|
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
|
||||||
|
if (!config.isSet("serverUuid")) {
|
||||||
|
config.addDefault("enabled", true);
|
||||||
|
config.addDefault("serverUuid", UUID.randomUUID().toString());
|
||||||
|
config.addDefault("logFailedRequests", false);
|
||||||
|
config.addDefault("logSentData", false);
|
||||||
|
config.addDefault("logResponseStatusText", false);
|
||||||
|
// Inform the server owners about bStats
|
||||||
|
config
|
||||||
|
.options()
|
||||||
|
.header(
|
||||||
|
"bStats (https://bStats.org) collects some basic information for plugin authors, like how\n"
|
||||||
|
+ "many people use their plugin and their total player count. It's recommended to keep bStats\n"
|
||||||
|
+ "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n"
|
||||||
|
+ "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n"
|
||||||
|
+ "anonymous.")
|
||||||
|
.copyDefaults(true);
|
||||||
|
try {
|
||||||
|
config.save(configFile);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Load the data
|
||||||
|
boolean enabled = config.getBoolean("enabled", true);
|
||||||
|
String serverUUID = config.getString("serverUuid");
|
||||||
|
boolean logErrors = config.getBoolean("logFailedRequests", false);
|
||||||
|
boolean logSentData = config.getBoolean("logSentData", false);
|
||||||
|
boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
|
||||||
|
metricsBase =
|
||||||
|
new MetricsBase(
|
||||||
|
"bukkit",
|
||||||
|
serverUUID,
|
||||||
|
serviceId,
|
||||||
|
enabled,
|
||||||
|
this::appendPlatformData,
|
||||||
|
this::appendServiceData,
|
||||||
|
submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
|
||||||
|
plugin::isEnabled,
|
||||||
|
(message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
|
||||||
|
(message) -> this.plugin.getLogger().log(Level.INFO, message),
|
||||||
|
logErrors,
|
||||||
|
logSentData,
|
||||||
|
logResponseStatusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shuts down the underlying scheduler service. */
|
||||||
|
public void shutdown() {
|
||||||
|
metricsBase.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a custom chart.
|
||||||
|
*
|
||||||
|
* @param chart The chart to add.
|
||||||
|
*/
|
||||||
|
public void addCustomChart(CustomChart chart) {
|
||||||
|
metricsBase.addCustomChart(chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendPlatformData(JsonObjectBuilder builder) {
|
||||||
|
builder.appendField("playerAmount", getPlayerAmount());
|
||||||
|
builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0);
|
||||||
|
builder.appendField("bukkitVersion", Bukkit.getVersion());
|
||||||
|
builder.appendField("bukkitName", Bukkit.getName());
|
||||||
|
builder.appendField("javaVersion", System.getProperty("java.version"));
|
||||||
|
builder.appendField("osName", System.getProperty("os.name"));
|
||||||
|
builder.appendField("osArch", System.getProperty("os.arch"));
|
||||||
|
builder.appendField("osVersion", System.getProperty("os.version"));
|
||||||
|
builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendServiceData(JsonObjectBuilder builder) {
|
||||||
|
builder.appendField("pluginVersion", plugin.getDescription().getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPlayerAmount() {
|
||||||
|
try {
|
||||||
|
// Around MC 1.8 the return type was changed from an array to a collection,
|
||||||
|
// This fixes java.lang.NoSuchMethodError:
|
||||||
|
// org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection;
|
||||||
|
Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers");
|
||||||
|
return onlinePlayersMethod.getReturnType().equals(Collection.class)
|
||||||
|
? ((Collection<?>) onlinePlayersMethod.invoke(Bukkit.getServer())).size()
|
||||||
|
: ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Just use the new method if the reflection failed
|
||||||
|
return Bukkit.getOnlinePlayers().size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MetricsBase {
|
||||||
|
|
||||||
|
/** The version of the Metrics class. */
|
||||||
|
public static final String METRICS_VERSION = "3.0.2";
|
||||||
|
|
||||||
|
private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
|
private final String platform;
|
||||||
|
|
||||||
|
private final String serverUuid;
|
||||||
|
|
||||||
|
private final int serviceId;
|
||||||
|
|
||||||
|
private final Consumer<JsonObjectBuilder> appendPlatformDataConsumer;
|
||||||
|
|
||||||
|
private final Consumer<JsonObjectBuilder> appendServiceDataConsumer;
|
||||||
|
|
||||||
|
private final Consumer<Runnable> submitTaskConsumer;
|
||||||
|
|
||||||
|
private final Supplier<Boolean> checkServiceEnabledSupplier;
|
||||||
|
|
||||||
|
private final BiConsumer<String, Throwable> errorLogger;
|
||||||
|
|
||||||
|
private final Consumer<String> infoLogger;
|
||||||
|
|
||||||
|
private final boolean logErrors;
|
||||||
|
|
||||||
|
private final boolean logSentData;
|
||||||
|
|
||||||
|
private final boolean logResponseStatusText;
|
||||||
|
|
||||||
|
private final Set<CustomChart> customCharts = new HashSet<>();
|
||||||
|
|
||||||
|
private final boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new MetricsBase class instance.
|
||||||
|
*
|
||||||
|
* @param platform The platform of the service.
|
||||||
|
* @param serviceId The id of the service.
|
||||||
|
* @param serverUuid The server uuid.
|
||||||
|
* @param enabled Whether or not data sending is enabled.
|
||||||
|
* @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
|
||||||
|
* appends all platform-specific data.
|
||||||
|
* @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
|
||||||
|
* appends all service-specific data.
|
||||||
|
* @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be
|
||||||
|
* used to delegate the data collection to a another thread to prevent errors caused by
|
||||||
|
* concurrency. Can be {@code null}.
|
||||||
|
* @param checkServiceEnabledSupplier A supplier to check if the service is still enabled.
|
||||||
|
* @param errorLogger A consumer that accepts log message and an error.
|
||||||
|
* @param infoLogger A consumer that accepts info log messages.
|
||||||
|
* @param logErrors Whether or not errors should be logged.
|
||||||
|
* @param logSentData Whether or not the sent data should be logged.
|
||||||
|
* @param logResponseStatusText Whether or not the response status text should be logged.
|
||||||
|
*/
|
||||||
|
public MetricsBase(
|
||||||
|
String platform,
|
||||||
|
String serverUuid,
|
||||||
|
int serviceId,
|
||||||
|
boolean enabled,
|
||||||
|
Consumer<JsonObjectBuilder> appendPlatformDataConsumer,
|
||||||
|
Consumer<JsonObjectBuilder> appendServiceDataConsumer,
|
||||||
|
Consumer<Runnable> submitTaskConsumer,
|
||||||
|
Supplier<Boolean> checkServiceEnabledSupplier,
|
||||||
|
BiConsumer<String, Throwable> errorLogger,
|
||||||
|
Consumer<String> infoLogger,
|
||||||
|
boolean logErrors,
|
||||||
|
boolean logSentData,
|
||||||
|
boolean logResponseStatusText) {
|
||||||
|
ScheduledThreadPoolExecutor scheduler =
|
||||||
|
new ScheduledThreadPoolExecutor(1, task -> new Thread(task, "bStats-Metrics"));
|
||||||
|
// We want delayed tasks (non-periodic) that will execute in the future to be
|
||||||
|
// cancelled when the scheduler is shutdown.
|
||||||
|
// Otherwise, we risk preventing the server from shutting down even when
|
||||||
|
// MetricsBase#shutdown() is called
|
||||||
|
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
this.platform = platform;
|
||||||
|
this.serverUuid = serverUuid;
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.enabled = enabled;
|
||||||
|
this.appendPlatformDataConsumer = appendPlatformDataConsumer;
|
||||||
|
this.appendServiceDataConsumer = appendServiceDataConsumer;
|
||||||
|
this.submitTaskConsumer = submitTaskConsumer;
|
||||||
|
this.checkServiceEnabledSupplier = checkServiceEnabledSupplier;
|
||||||
|
this.errorLogger = errorLogger;
|
||||||
|
this.infoLogger = infoLogger;
|
||||||
|
this.logErrors = logErrors;
|
||||||
|
this.logSentData = logSentData;
|
||||||
|
this.logResponseStatusText = logResponseStatusText;
|
||||||
|
checkRelocation();
|
||||||
|
if (enabled) {
|
||||||
|
// WARNING: Removing the option to opt-out will get your plugin banned from
|
||||||
|
// bStats
|
||||||
|
startSubmitting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCustomChart(CustomChart chart) {
|
||||||
|
this.customCharts.add(chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startSubmitting() {
|
||||||
|
final Runnable submitTask =
|
||||||
|
() -> {
|
||||||
|
if (!enabled || !checkServiceEnabledSupplier.get()) {
|
||||||
|
// Submitting data or service is disabled
|
||||||
|
scheduler.shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (submitTaskConsumer != null) {
|
||||||
|
submitTaskConsumer.accept(this::submitData);
|
||||||
|
} else {
|
||||||
|
this.submitData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven
|
||||||
|
// distribution of requests on the
|
||||||
|
// bStats backend. To circumvent this problem, we introduce some randomness into
|
||||||
|
// the initial and second delay.
|
||||||
|
// WARNING: You must not modify and part of this Metrics class, including the
|
||||||
|
// submit delay or frequency!
|
||||||
|
// WARNING: Modifying this code will get your plugin banned on bStats. Just
|
||||||
|
// don't do it!
|
||||||
|
long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
|
||||||
|
long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
|
||||||
|
scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitData() {
|
||||||
|
final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder();
|
||||||
|
appendPlatformDataConsumer.accept(baseJsonBuilder);
|
||||||
|
final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder();
|
||||||
|
appendServiceDataConsumer.accept(serviceJsonBuilder);
|
||||||
|
JsonObjectBuilder.JsonObject[] chartData =
|
||||||
|
customCharts.stream()
|
||||||
|
.map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toArray(JsonObjectBuilder.JsonObject[]::new);
|
||||||
|
serviceJsonBuilder.appendField("id", serviceId);
|
||||||
|
serviceJsonBuilder.appendField("customCharts", chartData);
|
||||||
|
baseJsonBuilder.appendField("service", serviceJsonBuilder.build());
|
||||||
|
baseJsonBuilder.appendField("serverUUID", serverUuid);
|
||||||
|
baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION);
|
||||||
|
JsonObjectBuilder.JsonObject data = baseJsonBuilder.build();
|
||||||
|
scheduler.execute(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
// Send the data
|
||||||
|
sendData(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Something went wrong! :(
|
||||||
|
if (logErrors) {
|
||||||
|
errorLogger.accept("Could not submit bStats metrics data", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendData(JsonObjectBuilder.JsonObject data) throws Exception {
|
||||||
|
if (logSentData) {
|
||||||
|
infoLogger.accept("Sent bStats metrics data: " + data.toString());
|
||||||
|
}
|
||||||
|
String url = String.format(REPORT_URL, platform);
|
||||||
|
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
|
||||||
|
// Compress the data to save bandwidth
|
||||||
|
byte[] compressedData = compress(data.toString());
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.addRequestProperty("Accept", "application/json");
|
||||||
|
connection.addRequestProperty("Connection", "close");
|
||||||
|
connection.addRequestProperty("Content-Encoding", "gzip");
|
||||||
|
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
|
connection.setRequestProperty("User-Agent", "Metrics-Service/1");
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
|
||||||
|
outputStream.write(compressedData);
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
try (BufferedReader bufferedReader =
|
||||||
|
new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = bufferedReader.readLine()) != null) {
|
||||||
|
builder.append(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (logResponseStatusText) {
|
||||||
|
infoLogger.accept("Sent data to bStats and received response: " + builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks that the class was properly relocated. */
|
||||||
|
private void checkRelocation() {
|
||||||
|
// You can use the property to disable the check in your test environment
|
||||||
|
if (System.getProperty("bstats.relocatecheck") == null
|
||||||
|
|| !System.getProperty("bstats.relocatecheck").equals("false")) {
|
||||||
|
// Maven's Relocate is clever and changes strings, too. So we have to use this
|
||||||
|
// little "trick" ... :D
|
||||||
|
final String defaultPackage =
|
||||||
|
new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'});
|
||||||
|
final String examplePackage =
|
||||||
|
new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
|
||||||
|
// We want to make sure no one just copy & pastes the example and uses the wrong
|
||||||
|
// package names
|
||||||
|
if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
|
||||||
|
|| MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
|
||||||
|
throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gzips the given string.
|
||||||
|
*
|
||||||
|
* @param str The string to gzip.
|
||||||
|
* @return The gzipped string.
|
||||||
|
*/
|
||||||
|
private static byte[] compress(final String str) throws IOException {
|
||||||
|
if (str == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
|
||||||
|
gzip.write(str.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SimplePie extends CustomChart {
|
||||||
|
|
||||||
|
private final Callable<String> callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @param chartId The id of the chart.
|
||||||
|
* @param callable The callable which is used to request the chart data.
|
||||||
|
*/
|
||||||
|
public SimplePie(String chartId, Callable<String> callable) {
|
||||||
|
super(chartId);
|
||||||
|
this.callable = callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||||
|
String value = callable.call();
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new JsonObjectBuilder().appendField("value", value).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MultiLineChart extends CustomChart {
|
||||||
|
|
||||||
|
private final Callable<Map<String, Integer>> callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @param chartId The id of the chart.
|
||||||
|
* @param callable The callable which is used to request the chart data.
|
||||||
|
*/
|
||||||
|
public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||||
|
super(chartId);
|
||||||
|
this.callable = callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||||
|
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||||
|
Map<String, Integer> map = callable.call();
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean allSkipped = true;
|
||||||
|
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||||
|
if (entry.getValue() == 0) {
|
||||||
|
// Skip this invalid
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
allSkipped = false;
|
||||||
|
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
if (allSkipped) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AdvancedPie extends CustomChart {
|
||||||
|
|
||||||
|
private final Callable<Map<String, Integer>> callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @param chartId The id of the chart.
|
||||||
|
* @param callable The callable which is used to request the chart data.
|
||||||
|
*/
|
||||||
|
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
|
||||||
|
super(chartId);
|
||||||
|
this.callable = callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||||
|
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||||
|
Map<String, Integer> map = callable.call();
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean allSkipped = true;
|
||||||
|
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||||
|
if (entry.getValue() == 0) {
|
||||||
|
// Skip this invalid
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
allSkipped = false;
|
||||||
|
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
if (allSkipped) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SimpleBarChart extends CustomChart {
|
||||||
|
|
||||||
|
private final Callable<Map<String, Integer>> callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @param chartId The id of the chart.
|
||||||
|
* @param callable The callable which is used to request the chart data.
|
||||||
|
*/
|
||||||
|
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||||
|
super(chartId);
|
||||||
|
this.callable = callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||||
|
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||||
|
Map<String, Integer> map = callable.call();
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||||
|
valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()});
|
||||||
|
}
|
||||||
|
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AdvancedBarChart extends CustomChart {
|
||||||
|
|
||||||
|
private final Callable<Map<String, int[]>> callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @param chartId The id of the chart.
|
||||||
|
* @param callable The callable which is used to request the chart data.
|
||||||
|
*/
|
||||||
|
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
|
||||||
|
super(chartId);
|
||||||
|
this.callable = callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||||
|
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||||
|
Map<String, int[]> map = callable.call();
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean allSkipped = true;
|
||||||
|
for (Map.Entry<String, int[]> entry : map.entrySet()) {
|
||||||
|
if (entry.getValue().length == 0) {
|
||||||
|
// Skip this invalid
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
allSkipped = false;
|
||||||
|
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
if (allSkipped) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DrilldownPie extends CustomChart {
|
||||||
|
|
||||||
|
private final Callable<Map<String, Map<String, Integer>>> callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @param chartId The id of the chart.
|
||||||
|
* @param callable The callable which is used to request the chart data.
|
||||||
|
*/
|
||||||
|
public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
|
||||||
|
super(chartId);
|
||||||
|
this.callable = callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||||
|
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||||
|
Map<String, Map<String, Integer>> map = callable.call();
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean reallyAllSkipped = true;
|
||||||
|
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
|
||||||
|
JsonObjectBuilder valueBuilder = new JsonObjectBuilder();
|
||||||
|
boolean allSkipped = true;
|
||||||
|
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
|
||||||
|
valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue());
|
||||||
|
allSkipped = false;
|
||||||
|
}
|
||||||
|
if (!allSkipped) {
|
||||||
|
reallyAllSkipped = false;
|
||||||
|
valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reallyAllSkipped) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract static class CustomChart {
|
||||||
|
|
||||||
|
private final String chartId;
|
||||||
|
|
||||||
|
protected CustomChart(String chartId) {
|
||||||
|
if (chartId == null) {
|
||||||
|
throw new IllegalArgumentException("chartId must not be null");
|
||||||
|
}
|
||||||
|
this.chartId = chartId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObjectBuilder.JsonObject getRequestJsonObject(
|
||||||
|
BiConsumer<String, Throwable> errorLogger, boolean logErrors) {
|
||||||
|
JsonObjectBuilder builder = new JsonObjectBuilder();
|
||||||
|
builder.appendField("chartId", chartId);
|
||||||
|
try {
|
||||||
|
JsonObjectBuilder.JsonObject data = getChartData();
|
||||||
|
if (data == null) {
|
||||||
|
// If the data is null we don't send the chart.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
builder.appendField("data", data);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (logErrors) {
|
||||||
|
errorLogger.accept("Failed to get data for custom chart with id " + chartId, t);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SingleLineChart extends CustomChart {
|
||||||
|
|
||||||
|
private final Callable<Integer> callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class constructor.
|
||||||
|
*
|
||||||
|
* @param chartId The id of the chart.
|
||||||
|
* @param callable The callable which is used to request the chart data.
|
||||||
|
*/
|
||||||
|
public SingleLineChart(String chartId, Callable<Integer> callable) {
|
||||||
|
super(chartId);
|
||||||
|
this.callable = callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||||
|
int value = callable.call();
|
||||||
|
if (value == 0) {
|
||||||
|
// Null = skip the chart
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new JsonObjectBuilder().appendField("value", value).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extremely simple JSON builder.
|
||||||
|
*
|
||||||
|
* <p>While this class is neither feature-rich nor the most performant one, it's sufficient enough
|
||||||
|
* for its use-case.
|
||||||
|
*/
|
||||||
|
public static class JsonObjectBuilder {
|
||||||
|
|
||||||
|
private StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
private boolean hasAtLeastOneField = false;
|
||||||
|
|
||||||
|
public JsonObjectBuilder() {
|
||||||
|
builder.append("{");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a null field to the JSON.
|
||||||
|
*
|
||||||
|
* @param key The key of the field.
|
||||||
|
* @return A reference to this object.
|
||||||
|
*/
|
||||||
|
public JsonObjectBuilder appendNull(String key) {
|
||||||
|
appendFieldUnescaped(key, "null");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a string field to the JSON.
|
||||||
|
*
|
||||||
|
* @param key The key of the field.
|
||||||
|
* @param value The value of the field.
|
||||||
|
* @return A reference to this object.
|
||||||
|
*/
|
||||||
|
public JsonObjectBuilder appendField(String key, String value) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalArgumentException("JSON value must not be null");
|
||||||
|
}
|
||||||
|
appendFieldUnescaped(key, "\"" + escape(value) + "\"");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends an integer field to the JSON.
|
||||||
|
*
|
||||||
|
* @param key The key of the field.
|
||||||
|
* @param value The value of the field.
|
||||||
|
* @return A reference to this object.
|
||||||
|
*/
|
||||||
|
public JsonObjectBuilder appendField(String key, int value) {
|
||||||
|
appendFieldUnescaped(key, String.valueOf(value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends an object to the JSON.
|
||||||
|
*
|
||||||
|
* @param key The key of the field.
|
||||||
|
* @param object The object.
|
||||||
|
* @return A reference to this object.
|
||||||
|
*/
|
||||||
|
public JsonObjectBuilder appendField(String key, JsonObject object) {
|
||||||
|
if (object == null) {
|
||||||
|
throw new IllegalArgumentException("JSON object must not be null");
|
||||||
|
}
|
||||||
|
appendFieldUnescaped(key, object.toString());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a string array to the JSON.
|
||||||
|
*
|
||||||
|
* @param key The key of the field.
|
||||||
|
* @param values The string array.
|
||||||
|
* @return A reference to this object.
|
||||||
|
*/
|
||||||
|
public JsonObjectBuilder appendField(String key, String[] values) {
|
||||||
|
if (values == null) {
|
||||||
|
throw new IllegalArgumentException("JSON values must not be null");
|
||||||
|
}
|
||||||
|
String escapedValues =
|
||||||
|
Arrays.stream(values)
|
||||||
|
.map(value -> "\"" + escape(value) + "\"")
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends an integer array to the JSON.
|
||||||
|
*
|
||||||
|
* @param key The key of the field.
|
||||||
|
* @param values The integer array.
|
||||||
|
* @return A reference to this object.
|
||||||
|
*/
|
||||||
|
public JsonObjectBuilder appendField(String key, int[] values) {
|
||||||
|
if (values == null) {
|
||||||
|
throw new IllegalArgumentException("JSON values must not be null");
|
||||||
|
}
|
||||||
|
String escapedValues =
|
||||||
|
Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(","));
|
||||||
|
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends an object array to the JSON.
|
||||||
|
*
|
||||||
|
* @param key The key of the field.
|
||||||
|
* @param values The integer array.
|
||||||
|
* @return A reference to this object.
|
||||||
|
*/
|
||||||
|
public JsonObjectBuilder appendField(String key, JsonObject[] values) {
|
||||||
|
if (values == null) {
|
||||||
|
throw new IllegalArgumentException("JSON values must not be null");
|
||||||
|
}
|
||||||
|
String escapedValues =
|
||||||
|
Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(","));
|
||||||
|
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a field to the object.
|
||||||
|
*
|
||||||
|
* @param key The key of the field.
|
||||||
|
* @param escapedValue The escaped value of the field.
|
||||||
|
*/
|
||||||
|
private void appendFieldUnescaped(String key, String escapedValue) {
|
||||||
|
if (builder == null) {
|
||||||
|
throw new IllegalStateException("JSON has already been built");
|
||||||
|
}
|
||||||
|
if (key == null) {
|
||||||
|
throw new IllegalArgumentException("JSON key must not be null");
|
||||||
|
}
|
||||||
|
if (hasAtLeastOneField) {
|
||||||
|
builder.append(",");
|
||||||
|
}
|
||||||
|
builder.append("\"").append(escape(key)).append("\":").append(escapedValue);
|
||||||
|
hasAtLeastOneField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the JSON string and invalidates this builder.
|
||||||
|
*
|
||||||
|
* @return The built JSON string.
|
||||||
|
*/
|
||||||
|
public JsonObject build() {
|
||||||
|
if (builder == null) {
|
||||||
|
throw new IllegalStateException("JSON has already been built");
|
||||||
|
}
|
||||||
|
JsonObject object = new JsonObject(builder.append("}").toString());
|
||||||
|
builder = null;
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt.
|
||||||
|
*
|
||||||
|
* <p>This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'.
|
||||||
|
* Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n").
|
||||||
|
*
|
||||||
|
* @param value The value to escape.
|
||||||
|
* @return The escaped value.
|
||||||
|
*/
|
||||||
|
private static String escape(String value) {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
for (int i = 0; i < value.length(); i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
if (c == '"') {
|
||||||
|
builder.append("\\\"");
|
||||||
|
} else if (c == '\\') {
|
||||||
|
builder.append("\\\\");
|
||||||
|
} else if (c <= '\u000F') {
|
||||||
|
builder.append("\\u000").append(Integer.toHexString(c));
|
||||||
|
} else if (c <= '\u001F') {
|
||||||
|
builder.append("\\u00").append(Integer.toHexString(c));
|
||||||
|
} else {
|
||||||
|
builder.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A super simple representation of a JSON object.
|
||||||
|
*
|
||||||
|
* <p>This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not
|
||||||
|
* allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String,
|
||||||
|
* JsonObject)}.
|
||||||
|
*/
|
||||||
|
public static class JsonObject {
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
private JsonObject(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/main/resources/config.yml
Normal file
15
src/main/resources/config.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
api-server: http://localhost:30030/api
|
||||||
|
|
||||||
|
server-name: Alset 1.18.2
|
||||||
|
server-desc: Confirm to connect to Alset Minecraft Server
|
||||||
|
|
||||||
|
channel-id: §f8356nc42vy
|
||||||
|
reference-page: https://meowdream.cn/wiki
|
||||||
|
|
||||||
|
enable-captcha: true
|
||||||
|
|
||||||
|
captcha-not-authed: §9你还没有完成验证呐
|
||||||
|
captcha-code-error: §c验证码错误,请重试
|
||||||
|
captcha-needed: §9请输入验证码以继续
|
||||||
|
|
||||||
|
captcha-countdown-duration: 30
|
11
src/main/resources/plugin.yml
Normal file
11
src/main/resources/plugin.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
name: MeowLogin
|
||||||
|
author: Mewn_Lynsi
|
||||||
|
version: 1.0.0-SNAPSHOT
|
||||||
|
main: cn.meowdream.meowlogin.Main
|
||||||
|
api-version: "1.18"
|
||||||
|
|
||||||
|
commands:
|
||||||
|
mlreload:
|
||||||
|
description: 重载MeowLogin配置文件
|
||||||
|
usage: /mlreload
|
||||||
|
permission: meowlogin.reload
|
Loading…
Reference in New Issue
Block a user