Compare commits

..
209 changed files with 5309 additions and 5743 deletions

No files matched your search

+18 -12
View File
@@ -10,28 +10,34 @@ on:
- ".forgejo/workflows/deploy_wiki.yml"
- "docs/**"
- "domains"
- "properdocs.yml"
- "mkdocs.yml"
- "requirements.txt"
jobs:
deployWiki:
runs-on: codeberg-small
runs-on: codeberg-tiny
steps:
- name: "Checkout Repository"
uses: https://code.forgejo.org/actions/checkout@v7
- name: "Checkout Source Repository"
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: "${{ secrets.BOT_TOKEN }}"
- name: "Checkout Target Repository"
run: "git clone -b pages https://codeberg.org/Andre601/asl-docs site"
- name: "Setup Python 3.x"
uses: https://code.forgejo.org/actions/setup-python@v7
uses: https://code.forgejo.org/actions/setup-python@v5
with:
python-version: 3.x
- name: "Install Dependencies"
run: "pip install -r requirements.txt"
- name: "Build Website"
run: "properdocs build"
- name: "Deploy Website"
uses: https://code.forgejo.org/actions/git-pages@v2
with:
site: https://asl.andre601.ch
token: ${{ secrets.FORGEJO_TOKEN }}
source: site/
run: "mkdocs build"
- name: "Commit and Push"
run: |
git config --global user.email "andre_601-bot@noreply.codeberg.org"
git config --global user.name "ASL Wiki Builder"
mv domains site/.domains
cd site
git remote set-url origin https://${{ secrets.BOT_TOKEN }}@codeberg.org/Andre601/asl-docs
git add --all
git commit -m "Update Wiki ($( env TZ=Europe/Zurich date +"%d.%m.%Y %Z" ))"
git push
+10 -10
View File
@@ -9,7 +9,7 @@ on:
paths:
- ".forgejo/workflows/pr_preview.yml"
- "docs/**"
- "properdocs.yml"
- "mkdocs.yml"
- "preview_domains"
- "requirements.txt"
workflow_dispatch:
@@ -20,7 +20,7 @@ env:
jobs:
createPreview:
runs-on: codeberg-small
runs-on: codeberg-tiny
steps:
#
# Finding and updating comment in Pull request.
@@ -28,13 +28,13 @@ jobs:
#
- name: "Find Comment"
id: find-comment
uses: https://github.com/peter-evans/find-comment@v4
uses: https://github.com/peter-evans/find-comment@v3
with:
issue-number: "${{ github.event.pull_request.number }}"
comment-author: "Andre_601-Bot"
- name: "Create or update Comment"
id: comment
uses: https://github.com/peter-evans/create-or-update-comment@v5
uses: https://github.com/peter-evans/create-or-update-comment@v4
with:
body: |-
## Pull request Preview
@@ -49,7 +49,7 @@ jobs:
# Build website and push.
#
- name: "Checkout Source Repository"
uses: https://code.forgejo.org/actions/checkout@v7
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ secrets.BOT_TOKEN }}
fetch-depth: 0
@@ -60,13 +60,13 @@ jobs:
cd site
git switch ${{ env.BRANCH_NAME }} 2>/dev/null || git switch -c ${{ env.BRANCH_NAME }}
- name: "Setup Python 3.x"
uses: https://code.forgejo.org/actions/setup-python@v7
uses: https://code.forgejo.org/actions/setup-python@v5
with:
python-version: 3.x
- name: "Install Dependencies"
run: "pip install -r requirements.txt"
- name: "Build Website"
run: "properdocs build"
run: "mkdocs build"
- name: "Commit and Push"
run: |
git config --global user.email "andre_601-bot@noreply.codeberg.org"
@@ -81,7 +81,7 @@ jobs:
# Update comment on success or failure.
#
- name: "Comment Success"
uses: https://github.com/peter-evans/create-or-update-comment@v5
uses: https://github.com/peter-evans/create-or-update-comment@v4
with:
body: |-
## Pull request Preview
@@ -99,7 +99,7 @@ jobs:
edit-mode: replace
- name: "Comment Failure"
if: ${{ failure() }}
uses: https://github.com/peter-evans/create-or-update-comment@v5
uses: https://github.com/peter-evans/create-or-update-comment@v4
with:
body: |-
## Pull request Preview
@@ -109,7 +109,7 @@ jobs:
| Name | Link |
|---------|-------------------------------------------|
| Commit | ${{ github.event.pull_request.head.sha }} |
| Logs | ${{ env.RUN_URL }} |
| Logs | ${{ RUN_URL }} |
comment-id: "${{ steps.comment.outputs.comment-id }}"
issue-number: "${{ github.event.pull_request.number }}"
token: "${{ secrets.BOT_TOKEN }}"
+5 -35
View File
@@ -16,14 +16,13 @@ body:
label: Platform
description: On what platform do you use the plugin?
options:
- BungeeCord
- Other (Please specify)
- Paper
- Purpur
- Folia
- Spigot
- Velocity
- Other (Please specify)
- Spigot (DISCONTINUED)
- BungeeCord (DISCONTINUED)
- Waterfall (DISCONTINUED)
- Waterfall
validations:
required: true
- type: input
@@ -33,35 +32,6 @@ body:
Please provide the name of the platform (Proxy/Server) you use, if you selected `Other (Please specify)` in the above section.
Leave empty otherwise.
placeholder: SomeProxy
- type: input
attributes:
label: Server/Proxy version
description: |-
Provide the Version of your Server/Proxy.
For Paper and its forks, run `/version` and for Velocity run `/velocity info`
placeholder: This server is running Paper version ...
validations:
required: true
- type: input
attributes:
label: Plugin version
description: |-
Provide the Version of the Plugin.
On Paper and its forks, run `/version AdvancedServerList` and on Velocity, run `/velocity plugins` and find the AdvancedServerList entry.
placeholder: 5.9.0
validations:
required: true
- type: input
attributes:
label: Logs
description: |-
Provide Logs from your Server/Proxy, uploaded to [mclo.gs](https://mclo.gs).
**DO NOT SKIP THIS!**
Skipping this by writing "Not needed", "private", etc. will get your issue closed without warning!
placeholder: 'https://mclo.gs/...'
validations:
required: true
- type: textarea
attributes:
label: What happened?
@@ -79,7 +49,7 @@ body:
placeholder: |-
1. Do this
2. ...
3. Prof-... Bug!
3. Profit!
validations:
required: true
- type: textarea
+64
View File
@@ -0,0 +1,64 @@
when:
- event: pull_request
path:
include: [".woodpecker/pr_preview.yml", "docs/**", "mkdocs.yml", "requirements.txt"]
variables:
setup_git: &setup_git
- apk add git
- git config --global user.email "andre_601-bot@noreply.codeberg.org"
- git config --global user.name "PR Docs Preview Builder"
steps:
- name: "Clone Preview Repository"
image: alpine:3.20.3
commands:
- chmod -R a+w .
- <<: *setup_git
- git clone https://codeberg.org/Andre601/preview.git site
- chmod -R a+w site
- cd site
- git switch ${CI_REPO_NAME,,}/${CI_COMMIT_PULL_REQUEST} 2>/dev/null || git switch -c ${CI_REPO_NAME,,}/${CI_COMMIT_PULL_REQUEST}
- name: "Build Docs"
image: woodpeckerci/plugin-mkdocs:latest
settings:
pip_install_file: requirements.txt
- name: "Commit and Push"
image: alpine:3.20.3
secrets:
- bot_token
commands:
- <<: *setup_git
- mv preview-domains site/.domains
- cd site
- git remote add upstream https://$BOT_TOKEN@codeberg.org/Andre601/preview.git
- git add --all
- git commit -m "Update Preview for ${CI_REPO_NAME}/${CI_COMMIT_PULL_REQUEST} ($( env TZ=Europe/Zurich date +"%d.%m.%Y %Z" )) [SKIP CI]"
- git push --set-upstream upstream ${CI_REPO_NAME,,}/${CI_COMMIT_PULL_REQUEST} -f
- name: "Comment on Success"
image: mcs94/gitea-comment
when:
- status: ["success"]
settings:
gitea_address: https://codeberg.org
gitea_token:
from_secret: BOT_TOKEN
comment: |-
Pull request Preview Ready!
| Name | Value |
|--------|--------------------------------------------------------------------------|
| Commit | ${CI_COMMIT_SHA} |
| Link | https://preview.andre601.ch/@${CI_REPO_NAME,,}~${CI_COMMIT_PULL_REQUEST} |
- name: "Comment on Failure"
image: mcs94/gitea-comment
when:
- status: ["failure"]
settings:
gitea_address: https://codeberg.org
gitea_token:
from_secret: BOT_TOKEN
comment: |-
Pull request Preview **failed**!
Please check the CI logs for details: ${CI_PIPELINE_URL}
+4 -19
View File
@@ -4,7 +4,7 @@ when:
steps:
- name: "Build Jar and Upload"
image: maven:3.10.0-eclipse-temurin-25
image: maven:3.9.9-eclipse-temurin-21
environment:
CODEBERG_API_TOKEN:
from_secret: cb_token
@@ -19,16 +19,16 @@ steps:
- cd version-uploader/target
- mv VersionUploader.jar ../../
- cd ../../
- java -jar VersionUploader.jar upload all
- java -jar VersionUploader.jar --all
- name: "Upload Jars to Codeberg"
image: woodpeckerci/plugin-gitea-release
settings:
base_url: https://codeberg.org
files:
- "bungeecord/target/AdvancedServerList-*.jar"
- "paper/target/AdvancedServerList-*.jar"
- "velocity/target/AdvancedServerList-*.jar"
- "addons/banplugins/target/AdvancedServerList-*.jar"
- "addons/luckperms/target/AdvancedServerList-*.jar"
api_key:
from_secret: cb_token
- name: "Create Post on Mastodon"
@@ -45,19 +45,4 @@ steps:
Modrinth - https://modrinth.com/plugin/advancedserverlist
Hangar - https://hangar.papermc.io/Andre_601/AdvancedServerList
Codeberg - https://codeberg.org/Andre601/AdvancedServerList
visibility: public
- name: "Create Post on Bluesky"
image: woodpeckerci/plugin-bluesky-post
when:
- status: ["success"]
settings:
identifier: andre601.ch
app_password:
from_secret: bluesky_app_password
languages: en-US
text: |-
#AdvancedServerList was updated to ${CI_COMMIT_TAG} and can be downloaded from #Modrinth, #Hangar and #Codeberg!
Modrinth - https://modrinth.com/plugin/advancedserverlist
Hangar - https://hangar.papermc.io/Andre_601/AdvancedServerList
Codeberg - https://codeberg.org/Andre601/AdvancedServerList
visibility: public
+36
View File
@@ -0,0 +1,36 @@
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
path:
include: [".woodpecker/publish_docs.yml", "docs/**", "mkdocs.yml", "requirements.txt"]
variables:
setup_git: &setup_git
- apk add git
- git config --global user.email "andre_601-bot@noreply.codeberg.org"
- git config --global user.name "ASL Docs Builder"
steps:
- name: "Clone target Repository"
image: alpine:3.20.3
commands:
- chmod -R a+w .
- <<: *setup_git
- git clone -b pages https://codeberg.org/Andre601/asl-docs.git site
- chmod -R a+w site
- name: "Build Docs"
image: woodpeckerci/plugin-mkdocs:latest
settings:
pip_install_file: requirements.txt
- name: "Commit and Push"
image: alpine:3.20.3
secrets:
- bot_token
commands:
- <<: *setup_git
- mv domains site/.domains
- cd site
- git remote set-url origin https://$BOT_TOKEN@codeberg.org/Andre601/asl-docs.git
- git add --all
- git commit -m "Update AdvancedServerList Docs ($( env TZ=Europe/Zurich date +"%d.%m.%Y %Z" )) [SKIP CI]"
- git push
+2 -8
View File
@@ -40,7 +40,7 @@ A Server List Profile is a single YAML file located in the plugin's `profiles` f
Each file has a priority and optional condition. The priority is used to determine what file AdvancedServerList should use first - starting from highest number to lowest - with conditions being used to set when the profile should be used.
Once a profile has been found with a condition returning true - which is the default should there be no condition - will AdvancedServerList use said profile.
> *Read more about [Priority](https://asl.andre601.ch/profiles/options/priority) and [Conditions](https://asl.andre601.ch/profiles/options/condition)*
> *Read more about [Priority](https://asl.andre601.ch/profiles/#priority) and [Conditions](https://asl.andre601.ch/profiles/#condition)*
### Placeholders
@@ -63,7 +63,7 @@ AdvancedServerList soft-depends on the following plugins, meaning their inclusio
| Plugin | Supported Platforms |
|-------------------------------------------------------------------------|----------------------------------------|
| [PlaceholderAPI](https://modrinth.com/plugin/PlaceholderAPI) | Paper |
| [PlaceholderAPI](https://hangar.papermc.io/HelpChat/PlaceholderAPI) | Paper |
| [ViaVersion](https://hangar.papermc.io/ViaVersion/ViaVersion) | Paper |
| [PAPIProxyBridge](https://hangar.papermc.io/William278/PAPIProxyBridge) | BungeeCord, Waterfall, Velocity |
| [Maintenance](https://hangar.papermc.io/kennytv/Maintenance) | Paper, BungeeCord, Waterfall, Velocity |
@@ -92,12 +92,6 @@ English Video by KasaiSora
Spanish Video by Ajneb97
[![video thumbnail](https://img.youtube.com/vi/rIbljm_4HVI/0.jpg)](https://youtube.com/watch?v=rIbljm_4HVI)
Indonesian Video by faajrikha
[![video thumbnail](https://img.youtube.com/vi/-p938rOmqy4/0.jpg)](https://www.youtube.com/watch?v=-p938rOmqy4)
German Video by Finni Gamer
[![video thumbnail](https://img.youtube.com/vi/1Kh3cPMDd0c/0.jpg)](https://www.youtube.com/watch?v=1Kh3cPMDd0c)
</p>
</details>
+5 -5
View File
@@ -19,8 +19,6 @@ To ensure proper safety and support are only certain versions and Platforms of A
| Server/Proxy Version* | Supported |
|-----------------------|-----------|
| 26.2 | Yes |
| 26.1.x | Yes |
| 1.21.x | Yes |
| 1.20.x | Yes |
| 1.19.x | No |
@@ -55,6 +53,8 @@ To ensure proper safety and support are only certain versions and Platforms of A
| Proxy | Supported |
|------------|-----------|
| BungeeCord | No |
| Waterfall | No |
| Velocity | Yes |
| BungeeCord | Yes** |
| Waterfall | Yes** |
| Velocity | Yes |
\*\*Platform version of this plugin is maintenance only, not receiving any bug fixes for major breaking changes anymore.
+28 -7
View File
@@ -40,8 +40,8 @@
<version>${plugin.version}</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@@ -62,16 +62,30 @@
<dependencies>
<!-- Proxy/Server -->
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.20-R0.2</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.20-R0.2</version>
<type>javadoc</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>[26.1.2.build,)</version>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>4.3.1</version>
<version>3.3.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
@@ -85,13 +99,13 @@
<dependency>
<groupId>space.arim.libertybans</groupId>
<artifactId>bans-api</artifactId>
<version>1.1.4</version>
<version>1.1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.gitlab.ruany</groupId>
<artifactId>LiteBansAPI</artifactId>
<version>0.6.1</version>
<version>0.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -103,12 +117,19 @@
</dependencies>
<build>
<finalName>AdvancedServerList-BanPlugins-Addon-${plugin.version}${project.build.build_id}</finalName>
<finalName>AdvancedServerList-BanPlugins-Addon-${plugin.version}</finalName>
<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,68 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.banplugins.bungeecord;
import ch.andre601.advancedserverlist.api.AdvancedServerListAPI;
import ch.andre601.advancedserverlist.banplugins.BanPluginsList;
import net.md_5.bungee.api.plugin.Plugin;
public class BungeeCordBanPluginsAddon extends Plugin{
@Override
public void onEnable(){
if(getProxy().getPluginManager().getPlugin("AdvancedServerList") == null){
getLogger().warning("AdvancedServerList is not enabled. This plugin requires it to function!");
return;
}
int loadedPlaceholders = loadPlaceholderProviders();
if(loadedPlaceholders == 0){
getLogger().warning("No compatible Ban plugin was found to register placeholders for.");
}else{
getLogger().info("Loaded " + loadedPlaceholders + " Placeholder Set(s) for AdvancedServerList!");
}
}
private int loadPlaceholderProviders(){
int loaded = 0;
AdvancedServerListAPI api = AdvancedServerListAPI.get();
for(BanPluginsList entry : BanPluginsList.values()){
if(!entry.supportsBungeeCord())
continue;
if(getProxy().getPluginManager().getPlugin(entry.getName()) != null){
getLogger().info("Registering Placeholders for " + entry.getName() + "...");
api.addPlaceholderProvider(entry.getPlaceholderProvider());
getLogger().info("Placeholders successfully registered!");
loaded++;
}
}
return loaded;
}
}
@@ -0,0 +1,12 @@
name: 'ASLBanPluginsAddon'
author: 'Andre_601'
version: '${plugin.version}'
description: 'AdvancedBan Add-on for AdvancedServerList to add placeholders to use.'
softDepends:
- AdvancedServerList
- AdvancedBan
- LibertyBans
- LiteBans
main: 'ch.andre601.advancedserverlist.banplugins.bungeecord.BungeeCordBanPluginsAddon'
-112
View File
@@ -1,112 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ MIT License
~
~ Copyright (c) 2022-2024 Andre_601
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
~
-->
<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">
<parent>
<artifactId>parent</artifactId>
<groupId>ch.andre601.advancedserverlist</groupId>
<version>parent</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>luckperms</artifactId>
<packaging>jar</packaging>
<version>${plugin.version}</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</repository>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<!-- Proxy/Server -->
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.21-R0.4</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.21-R0.4</version>
<type>javadoc</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>[26.1.2.build,)</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>4.3.1</version>
<scope>provided</scope>
</dependency>
<!-- Plugin APIs -->
<dependency>
<groupId>ch.andre601.asl-api</groupId>
<artifactId>api</artifactId>
<version>${api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.luckperms</groupId>
<artifactId>api</artifactId>
<version>5.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>AdvancedServerList-LuckPerms-Addon-${plugin.version}${project.build.build_id}</finalName>
<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>
@@ -1,44 +0,0 @@
package ch.andre601.advancedserverlist.luckperms.paper;
import ch.andre601.advancedserverlist.api.AdvancedServerListAPI;
import ch.andre601.advancedserverlist.api.exceptions.InvalidPlaceholderProviderException;
import ch.andre601.advancedserverlist.luckperms.placeholders.LuckPermsPlaceholder;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import org.bukkit.plugin.java.JavaPlugin;
public class LuckPermsAddon extends JavaPlugin{
@Override
public void onEnable(){
if(!this.getServer().getPluginManager().isPluginEnabled("AdvancedServerList")){
this.getSLF4JLogger().warn("AdvancedServerList not enabled. This plugin is required!");
this.getServer().getPluginManager().disablePlugin(this);
return;
}
if(!this.getServer().getPluginManager().isPluginEnabled("LuckPerms")){
this.getSLF4JLogger().warn("LuckPerms not enabled. This plugin is required!");
this.getServer().getPluginManager().disablePlugin(this);
return;
}
LuckPerms luckPerms;
try{
luckPerms = LuckPermsProvider.get();
}catch(IllegalStateException ex){
this.getSLF4JLogger().error("Unable to obtain LuckPerms API instance! Disabling plugin...");
this.getServer().getPluginManager().disablePlugin(this);
return;
}
this.getSLF4JLogger().info("Registering LuckPerms Placeholder...");
try{
AdvancedServerListAPI.get().addPlaceholderProvider(new LuckPermsPlaceholder(luckPerms));
this.getSLF4JLogger().info("Successfully registered Placeholder!");
}catch(InvalidPlaceholderProviderException ex){
this.getSLF4JLogger().warn("Couldn't register Placeholder. Disabling Plugin...");
this.getServer().getPluginManager().disablePlugin(this);
}
}
}
@@ -1,66 +0,0 @@
package ch.andre601.advancedserverlist.luckperms.placeholders;
import ch.andre601.advancedserverlist.api.PlaceholderProvider;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.model.user.User;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.UUID;
public class LuckPermsPlaceholder extends PlaceholderProvider{
private final LuckPerms luckPerms;
private final HashMap<UUID, CacheValue> cache = new HashMap<>();
public LuckPermsPlaceholder(LuckPerms luckPerms){
super("luckperms");
this.luckPerms = luckPerms;
}
@Override
public @Nullable String parsePlaceholder(@NonNull String placholder, @NonNull GenericPlayer player, @NonNull GenericServer server){
if(luckPerms == null)
return "ERR: LuckPerms not available.";
CacheValue cacheValue = cache.get(player.getUUID());
User user;
if(cacheValue == null || cacheValue.expired()){
// Can cause lag if on main thread.
// But the alternative would be a lot more annoying and error-prone.
user = luckPerms.getUserManager().loadUser(player.getUUID()).join();
cache.put(player.getUUID(), new CacheValue(System.currentTimeMillis(), user));
}else{
user = cacheValue.user();
}
if(user == null)
return "ERR: Player not found.";
String[] parts = placholder.split("\\s+");
if(parts.length == 1)
return "ERR: Missing argument for placeholder " + parts[0] + ".";
return switch(parts[0]){
case "hasGroup" -> String.valueOf(
user.getCachedData().getPermissionData().checkPermission("group." + parts[1])
);
case "hasPermission" -> String.valueOf(
user.getCachedData().getPermissionData().checkPermission(parts[1]).asBoolean()
);
case "primaryGroup" -> user.getPrimaryGroup();
default -> null;
};
}
private record CacheValue(long timestamp, User user){
public boolean expired(){
// Expired if current timestamp minus old one is more than 60k milliseconds (1 minute)
return (timestamp < 0L) || System.currentTimeMillis() - timestamp >= 60_000;
}
}
}
@@ -1,53 +0,0 @@
package ch.andre601.advancedserverlist.luckperms.velocity;
import ch.andre601.advancedserverlist.api.AdvancedServerListAPI;
import ch.andre601.advancedserverlist.api.exceptions.InvalidPlaceholderProviderException;
import ch.andre601.advancedserverlist.luckperms.placeholders.LuckPermsPlaceholder;
import com.google.inject.Inject;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.proxy.ProxyServer;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LuckPermsAddon{
private final ProxyServer proxy;
private final Logger logger = LoggerFactory.getLogger("ASLLuckPermsAddon");
@Inject
public LuckPermsAddon(ProxyServer proxy){
this.proxy = proxy;
}
@Subscribe
public void init(ProxyInitializeEvent event){
if(!proxy.getPluginManager().isLoaded("advancedserverlist")){
logger.warn("AdvancedServerList is not enabled. This plugin is required.");
return;
}
if(!proxy.getPluginManager().isLoaded("luckperms")){
logger.warn("LuckPerms is not enabled. This plugin is required.");
return;
}
LuckPerms luckPerms;
try{
luckPerms = LuckPermsProvider.get();
}catch(IllegalStateException ex){
logger.warn("Unable to obtain LuckPerms API instance. The plugin won't work.");
return;
}
logger.info("Registering LuckPerms Placeholder...");
try{
AdvancedServerListAPI.get().addPlaceholderProvider(new LuckPermsPlaceholder(luckPerms));
logger.info("Successfully registered Placeholder!");
}catch(InvalidPlaceholderProviderException ex){
logger.warn("Unable to register LuckPerms Placeholder! The plugin won't work.");
}
}
}
@@ -1,16 +0,0 @@
name: 'ASLLuckPermsAddon'
author: 'Andre_601'
version: '${plugin.version}'
main: 'ch.andre601.advancedserverlist.luckperms.paper.LuckPermsAddon'
api-version: '1.19'
dependencies:
server:
AdvancedServerList:
required: false
load: BEFORE
LuckPerms:
required: false
load: BEFORE
@@ -1,20 +0,0 @@
{
"id": "aslluckpermsaddon",
"name": "ASLLuckPermsAddon",
"version": "${plugin.version}",
"description": "LuckPerms Add-on for AdvancedServerList to add placeholders to use.",
"authors": [
"Andre_601"
],
"dependencies": [
{
"id": "advancedserverlist",
"optional": true
},
{
"id": "luckperms",
"optional": true
}
],
"main": "ch.andre601.advancedserverlist.luckperms.velocity.LuckPermsAddon"
}
+65
View File
@@ -0,0 +1,65 @@
<?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">
<parent>
<artifactId>parent</artifactId>
<groupId>ch.andre601.advancedserverlist</groupId>
<version>parent</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>asl-placeholderapi</artifactId>
<packaging>jar</packaging>
<version>${plugin.version}</version>
<properties>
<maven.compiler.source>16</maven.compiler.source>
<maven.compuler.target>16</maven.compuler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>placeholderapi-repo</id>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>AdvancedServerList-PlaceholderAPI-Addon-${plugin.version}</finalName>
<resources>
<resource>
<filtering>true</filtering>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,70 @@
package ch.andre601.placeholderapi;
import ch.andre601.placeholderapi.messenger.ASLPluginMessageListener;
import ch.andre601.placeholderapi.messenger.BungeeCordMessageListener;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.concurrent.Callable;
import java.util.logging.Level;
public final class ASLPlaceholderAPI extends JavaPlugin{
private String serverName = null;
@Override
public void onEnable(){
// Own Plugin Channel
getServer().getMessenger().registerOutgoingPluginChannel(this, pluginChannel());
getServer().getMessenger().registerIncomingPluginChannel(this, pluginChannel(), new ASLPluginMessageListener(this));
// BungeeCord Plugin Channel
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
getServer().getMessenger().registerIncomingPluginChannel(this, "BungeeCord", new BungeeCordMessageListener(this));
requestServerName();
}
@Override
public void onDisable(){
getServer().getMessenger().unregisterOutgoingPluginChannel(this);
getServer().getMessenger().unregisterIncomingPluginChannel(this);
}
public void serverName(String serverName){
this.serverName = serverName;
}
public String pluginChannel(){
return "advancedserverlist:action";
}
public String serverName(){
return serverName;
}
public void waitUntil(Callable<Boolean> condition, Runnable action){
Bukkit.getScheduler().runTaskTimer(this, (task) -> {
try{
if(condition.call()){
action.run();
task.cancel();
}
}catch(Exception ex){
getLogger().log(Level.WARNING, "Encountered Exception while waiting for a task to finish.", ex);
}
}, 0L, 1L);
}
private void requestServerName(){
waitUntil(() -> !getServer().getOnlinePlayers().isEmpty(), () -> {
Player plmSender = Bukkit.getOnlinePlayers().iterator().next();
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("GetServer");
plmSender.sendPluginMessage(this, "BungeeCord", out.toByteArray());
});
}
}
@@ -0,0 +1,78 @@
package ch.andre601.placeholderapi.messenger;
import ch.andre601.placeholderapi.ASLPlaceholderAPI;
import me.clip.placeholderapi.PlaceholderAPI;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.util.UUID;
import java.util.logging.Level;
public class ASLPluginMessageListener implements PluginMessageListener{
private final ASLPlaceholderAPI plugin;
public ASLPluginMessageListener(ASLPlaceholderAPI plugin){
this.plugin = plugin;
}
@Override
public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte[] message){
if(!channel.equals(plugin.pluginChannel()))
return;
if(plugin.serverName() == null)
return;
ByteArrayInputStream stream = new ByteArrayInputStream(message);
DataInputStream in = new DataInputStream(stream);
try{
String action = in.readUTF();
switch(action){
case "findPlugins" -> sendPluginMessage("hasPlugin", plugin.serverName());
case "parsePlaceholder" -> {
UUID playerUuid = UUID.fromString(in.readUTF());
String text = in.readUTF();
if(!Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")){
sendPluginMessage("parsed", playerUuid.toString(), text);
return;
}
OfflinePlayer targetPlayer = Bukkit.getOfflinePlayer(playerUuid);
String parsed = PlaceholderAPI.setPlaceholders(targetPlayer, text);
sendPluginMessage("parsed", playerUuid.toString(), parsed);
}
default -> plugin.getLogger().warning("Received unknown plugin Message sub channel: " + action);
}
}catch(Exception ex){
plugin.getLogger().log(Level.WARNING, "Encountered an Exception while handling a plugin message.", ex);
}
}
private void sendPluginMessage(String channel, String... values){
plugin.waitUntil(
() -> !Bukkit.getOnlinePlayers().isEmpty() && plugin.serverName() != null,
() -> {
Player plmSender = Bukkit.getOnlinePlayers().iterator().next();
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(outStream);
try{
dataOut.writeUTF(channel);
for(String value : values){
dataOut.writeUTF(value);
}
}catch(Exception ex){
plugin.getLogger().log(Level.WARNING, "Encountered an Exception while writiing data for plugin message.", ex);
}
plmSender.sendPluginMessage(plugin, plugin.pluginChannel(), outStream.toByteArray());
}
);
}
}
@@ -0,0 +1,34 @@
package ch.andre601.placeholderapi.messenger;
import ch.andre601.placeholderapi.ASLPlaceholderAPI;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.util.logging.Level;
public class BungeeCordMessageListener implements PluginMessageListener{
private final ASLPlaceholderAPI plugin;
public BungeeCordMessageListener(ASLPlaceholderAPI plugin){
this.plugin = plugin;
}
@Override
public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte[] message){
if(!channel.equals("BungeeCord"))
return;
DataInputStream stream = new DataInputStream(new ByteArrayInputStream(message));
try{
String subChannel = stream.readUTF();
if("GetServer".equals(subChannel))
plugin.serverName(stream.readUTF());
}catch(Exception ex){
plugin.getLogger().log(Level.WARNING, "Encountered Exception while reading plugin message from Bungeecord channel.", ex);
}
}
}
@@ -0,0 +1,10 @@
name: ASL-PlaceholderAPI
version: '${plugin.version}'
main: ch.andre601.placeholderapi.ASLPlaceholderAPI
api-version: '1.21'
dependencies:
server:
PlaceholderAPI:
load: BEFORE
authors: [ Andre601 ]
description: Addon to allow PlaceholderAPI placeholders on your Velocity/BungeeCord proxy.
+206
View File
@@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ MIT License
~
~ Copyright (c) 2022-2023 Andre_601
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
~
-->
<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">
<parent>
<artifactId>parent</artifactId>
<groupId>ch.andre601.advancedserverlist</groupId>
<version>parent</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>bungeecord</artifactId>
<packaging>jar</packaging>
<version>${plugin.version}</version>
<description>${plugin.description}</description>
<properties>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</repository>
<repository>
<id>repo-sayandevelopment</id>
<url>https://repo.sayandev.org/snapshots</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>ch.andre601.advancedserverlist</groupId>
<artifactId>core</artifactId>
<version>${plugin.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.andre601.asl-api</groupId>
<artifactId>platform-bungeecord</artifactId>
<version>${api.version}</version>
<scope>compile</scope>
</dependency>
<!-- BungeeCord API -->
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<type>javadoc</type>
<scope>provided</scope>
</dependency>
<!-- Adventure Library -->
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>${dependency.adventure-api}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<version>${dependency.adventure-text-minimessage}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bungeecord</artifactId>
<version>${dependency.adventure-platform-bungeecord}</version>
<scope>provided</scope>
</dependency>
<!-- Metrics -->
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bungeecord</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>
<!-- Configurate -->
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>configurate-yaml</artifactId>
<version>${dependency.configurate-yaml}</version>
<scope>provided</scope>
</dependency>
<!-- Plugins -->
<dependency>
<groupId>com.github.LeonMangler</groupId>
<artifactId>PremiumVanishAPI</artifactId>
<version>2.9.0-4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.sayandev</groupId>
<artifactId>sayanvanish-api</artifactId>
<version>1.6.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.sayandev</groupId>
<artifactId>sayanvanish-proxy-bungeecord</artifactId>
<version>1.6.1</version>
<scope>provided</scope>
</dependency>
<!-- Other -->
<dependency>
<groupId>com.alessiodp.libby</groupId>
<artifactId>libby-bungee</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<finalName>AdvancedServerList-BungeeCord-${plugin.version}</finalName>
<resources>
<resource>
<filtering>true</filtering>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>ch.andre601.advancedserverlist.bungeecord.depends.bstats</shadedPattern>
</relocation>
<relocation>
<pattern>net.byteflux.libby</pattern>
<shadedPattern>ch.andre601.advancedserverlist.bungeecord.depends.libby</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,264 @@
/*
* MIT License
*
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord;
import ch.andre601.advancedserverlist.bungeecord.commands.CmdAdvancedServerList;
import ch.andre601.advancedserverlist.bungeecord.listeners.JoinListener;
import ch.andre601.advancedserverlist.bungeecord.listeners.PingListener;
import ch.andre601.advancedserverlist.bungeecord.listeners.PluginMessageListener;
import ch.andre601.advancedserverlist.bungeecord.logging.BungeeLogger;
import ch.andre601.advancedserverlist.bungeecord.objects.placeholders.BungeePlayerPlaceholders;
import ch.andre601.advancedserverlist.bungeecord.objects.placeholders.BungeeProxyPlaceholders;
import ch.andre601.advancedserverlist.bungeecord.objects.placeholders.BungeeServerPlaceholders;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.PluginLogger;
import ch.andre601.advancedserverlist.core.interfaces.core.PluginCore;
import ch.andre601.advancedserverlist.core.objects.PluginMessageUtil;
import ch.andre601.advancedserverlist.core.profiles.handlers.FaviconHandler;
import com.alessiodp.libby.BungeeLibraryManager;
import com.alessiodp.libby.Library;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import de.myzelyam.api.vanish.BungeeVanishAPI;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.Favicon;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.sayandev.sayanvanish.bungeecord.api.SayanVanishBungeeAPI;
import java.awt.image.BufferedImage;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class BungeeCordCore extends Plugin implements PluginCore<Favicon>{
public static final String ASL_PLUGIN_CHANNEL = "advancedserverlist:action";
private final PluginLogger logger = new BungeeLogger(this);
private final Map<String, ServerPing> fetchedServers = new ConcurrentHashMap<>();
private boolean successfulLoad = false;
private AdvancedServerList<Favicon> core;
private FaviconHandler<Favicon> faviconHandler = null;
private BungeeAudiences audiences = null;
private BungeeLibraryManager libraryManager = null;
@Override
public void onLoad(){
// Using getLogger() here, because PluginLogger requires MiniMessage, which isn't loaded at this stage.
getLogger().info("Loading libraries. This may take a while...");
successfulLoad = loadLibs();
if(successfulLoad){
getLogger().info("Library loading completed successfully!");
}else{
getLogger().warning("Library loading completed with errors!");
}
}
@Override
public void onEnable(){
if(!successfulLoad){
logger.warn("There were issues during the plugins Loading-phase! Please see the console for possible reasons.");
logger.warn("Aborting further enabling of the plugin to avoid errors.");
return;
}
this.audiences = BungeeAudiences.create(this);
this.core = AdvancedServerList.init(this,
BungeePlayerPlaceholders.init(), BungeeServerPlaceholders.init(this), BungeeProxyPlaceholders.init(this));
}
@Override
public void onDisable(){
core.disable();
getProxy().getScheduler().cancel(this);
audiences.close();
}
@Override
public void loadCommands(){
getProxy().getPluginManager().registerCommand(this, new CmdAdvancedServerList(this));
getPluginLogger().success("Registered <white>/advancedserverlist</white>!");
}
@Override
public void loadEvents(){
new JoinListener(this);
new PingListener(this);
new PluginMessageListener(this);
}
@Override
public void loadMetrics(){
new Metrics(this, 15585).addCustomChart(new SimplePie("profiles",
() -> String.valueOf(core.getFileHandler().getProfiles().size())
));
}
@Override
public void loadFaviconHandler(AdvancedServerList<Favicon> core){
faviconHandler = new FaviconHandler<>(core);
}
@Override
public void clearFaviconCache(){
if(faviconHandler == null)
return;
faviconHandler.cleanCache();
}
@Override
public void downloadLibrary(String groupId, String artifactId, String version){
if(libraryManager == null){
libraryManager = new BungeeLibraryManager(this);
libraryManager.addRepository("https://repo.papermc.io/repository/maven-public");
}
Library lib = Library.builder()
.groupId(groupId)
.artifactId(artifactId)
.version(version)
.resolveTransitiveDependencies(true)
.build();
libraryManager.loadLibrary(lib);
}
@Override
public void startScheduler(){
getProxy().getScheduler().schedule(this, this::fetchServers, 0, 10, TimeUnit.SECONDS);
}
@Override
public AdvancedServerList<Favicon> getCore(){
return core;
}
@Override
public Path getFolderPath(){
return getDataFolder().toPath();
}
@Override
public PluginLogger getPluginLogger(){
return logger;
}
@Override
public FaviconHandler<Favicon> getFaviconHandler(){
if(faviconHandler == null)
faviconHandler = new FaviconHandler<>(core);
return faviconHandler;
}
@Override
public String getPlatformInfo(){
return getProxy().getName() + " " + getProxy().getVersion();
}
@Override
public String getLoader(){
return "bungeecord";
}
@Override
public boolean isPluginEnabled(String plugin){
return getProxy().getPluginManager().getPlugin(plugin) != null;
}
@Override
public Favicon createFavicon(BufferedImage image) throws IllegalArgumentException{
return Favicon.create(image);
}
public BungeeAudiences getAudiences(){
return audiences;
}
public int getOnlinePlayers(ServerInfo server){
List<ProxiedPlayer> players = new ArrayList<>(server == null ? getProxy().getPlayers() : server.getPlayers());
// Exclude players when PremiumVanish is enabled and player is hidden.
if(getProxy().getPluginManager().getPlugin("PremiumVanish") != null){
players.removeIf(BungeeVanishAPI::isInvisible);
}else // Do the same if SayanVanish is enabled.
if(getProxy().getPluginManager().getPlugin("SayanVanish") != null){
players.removeIf(player -> SayanVanishBungeeAPI.getInstance().isVanished(player.getUniqueId()));
}
return players.size();
}
public Map<String, ServerPing> getFetchedServers(){
return fetchedServers;
}
private boolean loadLibs(){
try{
if(libraryManager == null){
libraryManager = new BungeeLibraryManager(this);
libraryManager.addRepository("https://repo.papermc.io/repository/maven-public");
}
libraryManager.configureFromJSON("dependencies.json");
return true;
}catch(Exception ex){
getLogger().log(Level.WARNING, "Encountered an Exception while trying to load dependencies.", ex);
return false;
}
}
private void fetchServers(){
fetchedServers.clear();
PluginMessageUtil.get().clear();
getProxy().getServers().forEach((name, server) -> server.ping((ping, throwable) -> {
if(throwable != null)
return;
fetchedServers.put(name, ping);
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("findPlugins");
server.sendData(ASL_PLUGIN_CHANNEL, out.toByteArray());
}));
}
}
@@ -0,0 +1,52 @@
/*
* MIT License
*
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.commands;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.parsing.ComponentParser;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
public class BungeeCmdSender implements CmdSender{
private final CommandSender sender;
private final BungeeAudiences bungeeAudiences;
public BungeeCmdSender(CommandSender sender, BungeeAudiences bungeeAudiences){
this.sender = sender;
this.bungeeAudiences = bungeeAudiences;
}
@Override
public boolean hasPermission(String permission){
return sender.hasPermission(permission) || sender.hasPermission("advancedserverlist.admin");
}
@Override
public void sendMsg(String msg, Object... args){
bungeeAudiences.sender(sender).sendMessage(ComponentParser.text(String.format(msg, args)).toComponent());
}
}
@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.commands;
import ch.andre601.advancedserverlist.bungeecord.BungeeCordCore;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.plugin.Command;
public class CmdAdvancedServerList extends Command{
private final BungeeCordCore plugin;
public CmdAdvancedServerList(BungeeCordCore plugin){
super("advancedserverlist", "advancedserverlist.command.asl", "asl");
this.plugin = plugin;
}
@Override
public void execute(CommandSender sender, String[] args){
plugin.getCore().getCommandHandler().handle(new BungeeCmdSender(sender, plugin.getAudiences()), args);
}
}
@@ -0,0 +1,220 @@
/*
* MIT License
*
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.listeners;
import ch.andre601.advancedserverlist.api.events.GenericServerListEvent;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.advancedserverlist.api.profiles.ProfileEntry;
import ch.andre601.advancedserverlist.bungeecord.BungeeCordCore;
import ch.andre601.advancedserverlist.bungeecord.objects.impl.BungeePlayerImpl;
import ch.andre601.advancedserverlist.bungeecord.objects.impl.BungeeProxyImpl;
import ch.andre601.advancedserverlist.core.compat.maintenance.MaintenanceUtil;
import ch.andre601.advancedserverlist.core.interfaces.core.PluginCore;
import ch.andre601.advancedserverlist.core.interfaces.events.GenericEventWrapper;
import ch.andre601.advancedserverlist.core.objects.CachedPlayer;
import ch.andre601.advancedserverlist.core.objects.PluginMessageUtil;
import ch.andre601.advancedserverlist.core.parsing.ComponentParser;
import ch.andre601.advancedserverlist.core.profiles.replacer.StringReplacer;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import net.md_5.bungee.api.Favicon;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.event.ProxyPingEvent;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class BungeeEventWrapper implements GenericEventWrapper<Favicon, BungeePlayerImpl>{
private final BungeeCordCore plugin;
private final ProxyPingEvent event;
private final ServerPing.Protocol protocol;
private final ServerPing ping;
public BungeeEventWrapper(BungeeCordCore plugin, ProxyPingEvent event){
this.plugin = plugin;
this.event = event;
this.protocol = event.getResponse().getVersion();
this.ping = event.getResponse();
}
@Override
public GenericServerListEvent callEvent(ProfileEntry entry){
PreServerListSetEventImpl event = new PreServerListSetEventImpl(entry);
plugin.getProxy().getPluginManager().callEvent(event);
return event;
}
@Override
public void setMaxPlayers(int maxPlayers){
ping.getPlayers().setMax(maxPlayers);
}
@Override
public void setOnlinePlayers(int onlinePlayers){
ping.getPlayers().setOnline(onlinePlayers);
}
@Override
public void setMotd(Component component){
ping.setDescriptionComponent(new TextComponent(BungeeComponentSerializer.get().serialize(component)));
}
@Override
public void hidePlayers(){
ping.setPlayers(null);
}
@Override
public void setPlayerCount(String name){
protocol.setName(name);
protocol.setProtocol(-1);
}
@Override
public void setPlayers(List<String> lines, BungeePlayerImpl player, GenericServer server){
ServerPing.PlayerInfo[] players = new ServerPing.PlayerInfo[lines.size()];
for(int i = 0; i < players.length; i++){
String parsed = ComponentParser.text(lines.get(i))
.modifyText(text -> StringReplacer.replace(text, player, server))
.modifyText(text -> parsePAPIPlaceholders(text, player))
.toString();
ServerPing.PlayerInfo pi = new ServerPing.PlayerInfo(parsed, UUID.randomUUID());
players[i] = pi;
}
if(players.length > 0)
ping.getPlayers().setSample(players);
}
@Override
public void setPlayersHidden(){
ping.getPlayers().setSample(new ServerPing.PlayerInfo[0]);
}
@Override
public void setFavicon(Favicon favicon){
ping.setFavicon(favicon);
}
@Override
public void setDefaultFavicon(){
ping.setFavicon(ping.getFaviconObject());
}
@Override
public void updateEvent(){
this.ping.setVersion(this.protocol);
this.event.setResponse(this.ping);
}
@Override
public boolean isInvalidProtocol(){
return protocol == null;
}
@Override
public boolean isMaintenanceModeActive(){
if(plugin.getProxy().getPluginManager().getPlugin("Maintenance") == null)
return false;
return MaintenanceUtil.get().isMaintenanceEnabled();
}
@Override
public int getProtocolVersion(){
return event.getConnection().getVersion();
}
@Override
public int getOnlinePlayers(){
return plugin.getOnlinePlayers(null);
}
@Override
public int getMaxPlayers(){
return ping.getPlayers().getMax();
}
@Override
public String getPlayerIP(){
return ((InetSocketAddress)event.getConnection().getSocketAddress()).getHostString();
}
@Override
public String parsePAPIPlaceholders(String text, BungeePlayerImpl player){
return PluginMessageUtil.get().getOrExecute(player.getUUID().toString(), text, (uuid, textValue) -> {
List<String> servers = PluginMessageUtil.get().getKnownServers();
if(servers.isEmpty())
return textValue;
ServerInfo serverInfo = plugin.getProxy().getServerInfo(servers.get(0));
if(serverInfo == null)
return textValue;
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("parse");
out.writeUTF(uuid);
out.writeUTF(textValue);
serverInfo.sendData(BungeeCordCore.ASL_PLUGIN_CHANNEL, out.toByteArray());
PluginMessageUtil.get().queue(uuid, textValue, false);
return PluginMessageUtil.get().getOrDefault(uuid, textValue);
});
}
@Override
public String getVirtualHost(){
return this.resolveHost(event.getConnection().getVirtualHost());
}
@Override
public PluginCore<Favicon> getPlugin(){
return plugin;
}
@Override
public BungeePlayerImpl createPlayer(CachedPlayer player, int protocol){
return new BungeePlayerImpl(player, protocol);
}
@Override
public GenericServer createGenericServer(int playersOnline, int playersMax, String host){
Map<String, ServerInfo> servers = plugin.getProxy().getServers();
return new BungeeProxyImpl(servers, playersOnline, playersMax, host);
}
}
@@ -0,0 +1,61 @@
/*
* MIT License
*
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.listeners;
import ch.andre601.advancedserverlist.bungeecord.BungeeCordCore;
import ch.andre601.advancedserverlist.bungeecord.commands.BungeeCmdSender;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import java.net.InetSocketAddress;
public class JoinListener implements Listener{
private final BungeeCordCore plugin;
public JoinListener(BungeeCordCore plugin){
this.plugin = plugin;
plugin.getProxy().getPluginManager().registerListener(plugin, this);
}
@EventHandler
public void onJoin(PostLoginEvent event){
InetSocketAddress address = (InetSocketAddress)event.getPlayer().getPendingConnection().getSocketAddress();
ProxiedPlayer player = event.getPlayer();
plugin.getCore().getPlayerHandler().addPlayer(address.getHostString(), player.getName(), player.getUniqueId());
if(player.hasPermission("advancedserverlist.admin") || player.hasPermission("advancedserverlist.updatecheck")){
if(plugin.getAudiences() == null || plugin.getCore().getUpdateChecker() == null)
return;
BungeeCmdSender sender = new BungeeCmdSender(player, plugin.getAudiences());
plugin.getCore().getUpdateChecker().performCachedUpdateCheck(sender);
}
}
}
@@ -0,0 +1,51 @@
/*
* MIT License
*
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.listeners;
import ch.andre601.advancedserverlist.api.bungeecord.events.PostServerListSetEvent;
import ch.andre601.advancedserverlist.api.profiles.ProfileEntry;
import ch.andre601.advancedserverlist.bungeecord.BungeeCordCore;
import ch.andre601.advancedserverlist.core.events.PingEventHandler;
import net.md_5.bungee.api.event.ProxyPingEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
public class PingListener implements Listener{
private final BungeeCordCore plugin;
public PingListener(BungeeCordCore plugin){
this.plugin = plugin;
plugin.getProxy().getPluginManager().registerListener(plugin, this);
}
@EventHandler(priority = 90) // Maintenance has Event Priority 80, so we need to be AFTER it.
public void onProxyPing(ProxyPingEvent event){
ProfileEntry entry = PingEventHandler.handleEvent(new BungeeEventWrapper(plugin, event));
plugin.getProxy().getPluginManager().callEvent(new PostServerListSetEvent(entry));
}
}
@@ -0,0 +1,37 @@
package ch.andre601.advancedserverlist.bungeecord.listeners;
import ch.andre601.advancedserverlist.bungeecord.BungeeCordCore;
import ch.andre601.advancedserverlist.core.objects.PluginMessageUtil;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
public class PluginMessageListener implements Listener{
public PluginMessageListener(BungeeCordCore plugin){
plugin.getProxy().getPluginManager().registerListener(plugin, this);
}
@EventHandler
public void onPluginMessage(PluginMessageEvent event){
if(!event.getTag().equals(BungeeCordCore.ASL_PLUGIN_CHANNEL))
return;
ByteArrayDataInput input = ByteStreams.newDataInput(event.getData());
String subChannel = input.readUTF();
switch(subChannel){
case "hasPlugin" -> {
String serverName = input.readUTF();
PluginMessageUtil.get().addServer(serverName);
}
case "parsed" -> {
String uuid = input.readUTF();
String parsed = input.readUTF();
PluginMessageUtil.get().queue(uuid, parsed, true);
}
}
}
}
@@ -1,7 +1,7 @@
/*
* MIT License
*
* Copyright (c) 2022-2025 Andre_601
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -20,15 +20,17 @@
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.interfaces.commands;
package ch.andre601.advancedserverlist.bungeecord.listeners;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.SenderMapper;
import ch.andre601.advancedserverlist.api.bungeecord.events.PreServerListSetEvent;
import ch.andre601.advancedserverlist.api.profiles.ProfileEntry;
public interface CommandManagerInterface<S>{
CommandManager<CmdSender> commandHandler();
public class PreServerListSetEventImpl extends PreServerListSetEvent{
SenderMapper<S, CmdSender> senderMapper();
public PreServerListSetEventImpl(ProfileEntry entry){
super(entry);
}
}
@@ -0,0 +1,76 @@
/*
* MIT License
*
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.logging;
import ch.andre601.advancedserverlist.bungeecord.BungeeCordCore;
import ch.andre601.advancedserverlist.core.interfaces.PluginLogger;
import ch.andre601.advancedserverlist.core.parsing.ComponentParser;
import java.util.logging.Level;
import java.util.logging.Logger;
public class BungeeLogger implements PluginLogger{
private final BungeeCordCore plugin;
private final Logger logger;
public BungeeLogger(BungeeCordCore plugin){
this.plugin = plugin;
this.logger = plugin.getLogger();
}
@Override
public void debug(Class<?> clazz, String msg, Object... args){
if(plugin.isDebugEnabled())
info("[DEBUG] [" + clazz.getSimpleName() + "] " + msg, args);
}
@Override
public void debugWarn(Class<?> clazz, String msg, Object... args){
if(plugin.isDebugEnabled())
warn("[DEBUG] [" + clazz.getSimpleName() + "] " + msg, args);
}
@Override
public void info(String msg, Object... args){
logger.info(ComponentParser.textFormatted(msg, args).toStringStriped());
}
@Override
public void warn(String msg, Object... args){
logger.warning(ComponentParser.textFormatted(msg, args).toStringStriped());
}
@Override
public void warn(String msg, Throwable throwable){
logger.log(Level.WARNING, ComponentParser.text(msg).toStringStriped(), throwable);
}
@Override
public void warn(String msg, Throwable throwable, Object... args){
logger.log(Level.WARNING, ComponentParser.textFormatted(msg, args).toStringStriped(), throwable);
}
}
@@ -0,0 +1,59 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.objects.impl;
import ch.andre601.advancedserverlist.api.bungeecord.objects.BungeePlayer;
import ch.andre601.advancedserverlist.core.objects.CachedPlayer;
import java.util.UUID;
public class BungeePlayerImpl implements BungeePlayer{
private final String name;
private final int protocol;
private final UUID uuid;
public BungeePlayerImpl(CachedPlayer player, int protocol){
this.name = player.name();
this.protocol = protocol;
this.uuid = player.uuid();
}
@Override
public String getName(){
return name;
}
@Override
public int getProtocol(){
return protocol;
}
@Override
public UUID getUUID(){
return uuid;
}
}
@@ -23,30 +23,31 @@
*
*/
package ch.andre601.advancedserverlist.core.objects;
package ch.andre601.advancedserverlist.bungeecord.objects.impl;
import java.time.Duration;
import java.util.function.Supplier;
import ch.andre601.advancedserverlist.api.bungeecord.objects.BungeeProxy;
import net.md_5.bungee.api.config.ServerInfo;
public class ValueCache<T>{
private final Duration duration;
private CacheValue<T> cache = null;
public ValueCache(Duration duration){
this.duration = duration;
import java.util.Map;
public record BungeeProxyImpl(Map<String, ServerInfo> servers, int playersOnline, int playersMax, String host) implements BungeeProxy{
@Override
public Map<String, ServerInfo> getServers(){
return servers;
}
public T get(Supplier<T> supplier){
if(cache == null || cache.expired())
cache = new CacheValue<>(System.currentTimeMillis(), duration, supplier.get());
return cache.value();
@Override
public int getPlayersOnline(){
return playersOnline;
}
private record CacheValue<T>(long timestamp, Duration duration, T value){
public boolean expired(){
return (timestamp < 0L) || System.currentTimeMillis() - timestamp >= duration.toMillis();
}
@Override
public int getPlayersMax(){
return playersMax;
}
@Override
public String getHost(){
return host;
}
}
@@ -0,0 +1,53 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.objects.placeholders;
import ch.andre601.advancedserverlist.api.PlaceholderProvider;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import java.util.Locale;
public class BungeePlayerPlaceholders extends PlaceholderProvider{
private BungeePlayerPlaceholders(){
super("player");
}
public static BungeePlayerPlaceholders init(){
return new BungeePlayerPlaceholders();
}
@Override
public String parsePlaceholder(String placeholder, GenericPlayer player, GenericServer server){
return switch(placeholder.toLowerCase(Locale.ROOT)){
case "name" -> player.getName();
case "protocol" -> String.valueOf(player.getProtocol());
case "uuid" -> String.valueOf(player.getUUID());
default -> null;
};
}
}
@@ -0,0 +1,61 @@
package ch.andre601.advancedserverlist.bungeecord.objects.placeholders;
import ch.andre601.advancedserverlist.api.PlaceholderProvider;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.advancedserverlist.bungeecord.BungeeCordCore;
import ch.andre601.advancedserverlist.core.parsing.ComponentParser;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.chat.BaseComponent;
public class BungeeProxyPlaceholders extends PlaceholderProvider{
private final BungeeCordCore plugin;
private BungeeProxyPlaceholders(BungeeCordCore plugin){
super("proxy");
this.plugin = plugin;
}
public static BungeeProxyPlaceholders init(BungeeCordCore plugin){
return new BungeeProxyPlaceholders(plugin);
}
@Override
public String parsePlaceholder(String placeholder, GenericPlayer player, GenericServer server){
String[] args = placeholder.split("\\s", 2);
if(args.length < 2)
return null;
ServerPing ping = plugin.getFetchedServers().get(args[1]);
return switch(args[0]){
case "status" -> ping == null ? "offline" : "online";
case "motd" -> {
if(ping == null)
yield "none";
BaseComponent[] components = new BaseComponent[]{ping.getDescriptionComponent()};
Component component = BungeeComponentSerializer.get().deserialize(components);
yield ComponentParser.toMMString(component);
}
case "players" -> {
if(ping == null)
yield "0";
yield String.valueOf(ping.getPlayers().getOnline());
}
case "maxPlayers" -> {
if(ping == null)
yield "0";
yield String.valueOf(ping.getPlayers().getMax());
}
default -> null;
};
}
}
@@ -0,0 +1,103 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.bungeecord.objects.placeholders;
import ch.andre601.advancedserverlist.api.PlaceholderProvider;
import ch.andre601.advancedserverlist.api.bungeecord.objects.BungeeProxy;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.advancedserverlist.bungeecord.BungeeCordCore;
import net.md_5.bungee.api.config.ServerInfo;
import java.net.InetSocketAddress;
public class BungeeServerPlaceholders extends PlaceholderProvider{
private final BungeeCordCore plugin;
private BungeeServerPlaceholders(BungeeCordCore plugin){
super("server");
this.plugin = plugin;
}
public static BungeeServerPlaceholders init(BungeeCordCore plugin){
return new BungeeServerPlaceholders(plugin);
}
@Override
public String parsePlaceholder(String placeholder, GenericPlayer player, GenericServer server){
if(!(server instanceof BungeeProxy proxy))
return null;
String[] args = placeholder.split("\\s", 2);
return switch(args[0]){
case "playersOnline" -> {
if(args.length >= 2){
String[] servers = args[1].split(",");
int players = 0;
for(String serverName : servers){
if(serverName.isEmpty())
continue;
ServerInfo info = proxy.getServers().get(serverName.strip());
if(info == null)
continue;
players += plugin.getOnlinePlayers(info);
}
yield String.valueOf(players);
}
yield String.valueOf(proxy.getPlayersOnline());
}
case "playersMax" -> {
// ServerInfo doesn't provide the max players that could join, so we won't allow an extra argument
if(args.length >= 2)
yield "";
yield String.valueOf(proxy.getPlayersMax());
}
case "host" -> {
if(args.length > 2)
yield "";
if(args.length == 2 && args[1] != null && !args[1].isEmpty()){
ServerInfo info = proxy.getServers().get(args[1]);
if(info == null)
yield "";
yield String.valueOf(((InetSocketAddress)info.getSocketAddress()).getHostString());
}
yield proxy.getHost();
}
default -> null;
};
}
}
+11
View File
@@ -0,0 +1,11 @@
name: 'AdvancedServerList'
author: 'Andre_601'
version: '${plugin.version}'
description: '${plugin.description}'
softDepends:
- PAPIProxyBridge
- Maintenance
- ServerListPlus
main: 'ch.andre601.advancedserverlist.bungeecord.BungeeCordCore'
@@ -0,0 +1,28 @@
{
"libraries": [
{
"groupId": "net{}kyori",
"artifactId": "adventure-api",
"version": "${dependency.adventure-api}",
"resolveTransitiveDependencies": true
},
{
"groupId": "net{}kyori",
"artifactId": "adventure-text-minimessage",
"version": "${dependency.adventure-text-minimessage}",
"resolveTransitiveDependencies": true
},
{
"groupId": "net{}kyori",
"artifactId": "adventure-platform-bungeecord",
"version": "${dependency.adventure-platform-bungeecord}",
"resolveTransitiveDependencies": true
},
{
"groupId": "org{}spongepowered",
"artifactId": "configurate-yaml",
"version": "${dependency.configurate-yaml}",
"resolveTransitiveDependencies": true
}
]
}
+46 -50
View File
@@ -39,11 +39,9 @@
<version>${plugin.version}</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<plugin.config.version>10</plugin.config.version>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
<plugin.config.version>8</plugin.config.version>
</properties>
<repositories>
@@ -79,7 +77,7 @@
<dependency>
<groupId>ch.andre601</groupId>
<artifactId>expressionparser</artifactId>
<version>1.7.0-b1</version>
<version>1.5.0</version>
<scope>compile</scope>
</dependency>
@@ -93,7 +91,7 @@
<dependency>
<groupId>org.spongepowered</groupId>
<artifactId>configurate-hocon</artifactId>
<version>4.2.0</version>
<version>4.1.2</version>
<scope>provided</scope>
</dependency>
@@ -121,7 +119,7 @@
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.15.0</version>
<version>2.11.0</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -133,48 +131,21 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.8.0-jre</version>
<version>33.4.0-jre</version>
<scope>provided</scope>
</dependency>
<!-- Command Framework -->
<dependency>
<groupId>org.incendo</groupId>
<artifactId>cloud-annotations</artifactId>
<version>${dependency.cloud-annotations}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.incendo</groupId>
<artifactId>cloud-minecraft-extras</artifactId>
<version>${dependency.cloud-minecraft-extras}</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>net.kyori</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Annotations -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.1</version>
</dependency>
<!-- Plugins -->
<dependency>
<groupId>net.william278</groupId>
<artifactId>papiproxybridge</artifactId>
<version>1.8.4</version>
<version>1.7.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>eu.kennytv.maintenance</groupId>
<artifactId>maintenance-api</artifactId>
<version>5.0.0</version>
<version>4.2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -183,24 +154,18 @@
<version>3.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.github.miniplaceholders</groupId>
<artifactId>miniplaceholders-api</artifactId>
<version>3.2.0</version>
<scope>provided</scope>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.14.4</version>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>AdvancedServerList-Core-${plugin.version}${project.build.build_id}</finalName>
<finalName>AdvancedServerList-Core-${plugin.version}</finalName>
<resources>
<resource>
<filtering>true</filtering>
@@ -221,12 +186,43 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<relocations>
<relocation>
<pattern>com.unascribed</pattern>
<shadedPattern>ch.andre601.advancedserverlist.core.depends.flexver</shadedPattern>
</relocation>
<relocation>
<pattern>ch.andre601.expressionparser</pattern>
<shadedPattern>ch.andre601.advancedserverlist.core.depends.expressionparser</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -28,58 +28,30 @@ package ch.andre601.advancedserverlist.core;
import ch.andre601.advancedserverlist.api.AdvancedServerListAPI;
import ch.andre601.advancedserverlist.api.PlaceholderProvider;
import ch.andre601.advancedserverlist.core.check.UpdateChecker;
import ch.andre601.advancedserverlist.core.commands.*;
import ch.andre601.advancedserverlist.core.commands.CommandHandler;
import ch.andre601.advancedserverlist.core.compat.maintenance.MaintenancePlaceholder;
import ch.andre601.advancedserverlist.core.file.FileHandler;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.interfaces.commands.CommandType;
import ch.andre601.advancedserverlist.core.interfaces.core.PluginCore;
import ch.andre601.advancedserverlist.core.parsing.TextCenterUtil;
import ch.andre601.advancedserverlist.core.placeholders.DatePlaceholder;
import ch.andre601.advancedserverlist.core.profiles.ServerListProfile;
import ch.andre601.advancedserverlist.core.profiles.conditions.reader.PlaceholderTokenReader;
import ch.andre601.advancedserverlist.core.profiles.conditions.reader.PlaceholderValueReader;
import ch.andre601.advancedserverlist.core.profiles.conditions.ProfileConditionParser;
import ch.andre601.advancedserverlist.core.profiles.handlers.PlayerHandler;
import ch.andre601.expressionparser.DefaultExpressionParserEngine;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.annotations.AnnotationParser;
import org.incendo.cloud.description.Description;
import org.incendo.cloud.parser.ParserDescriptor;
import org.incendo.cloud.parser.standard.BooleanParser;
import org.incendo.cloud.parser.standard.IntegerParser;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.permission.Permission;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
public class AdvancedServerList<F>{
public static <F> AdvancedServerList<F> init(PluginCore<F> plugin, PlaceholderProvider... placeholders){
List<PlaceholderProvider> providers = new ArrayList<>(Arrays.asList(placeholders));
providers.add(new DatePlaceholder());
return new AdvancedServerList<>(plugin, providers);
}
private final PluginCore<F> plugin;
private final FileHandler fileHandler;
private final CommandHandler commandHandler;
private final PlayerHandler playerHandler;
private final TextCenterUtil textCenterUtil;
private static final AdvancedServerListAPI api = AdvancedServerListAPI.get();
private final DefaultExpressionParserEngine parser = new DefaultExpressionParserEngine.DefaultBuilder()
.addDefaults()
.addTokenReader(new PlaceholderTokenReader())
.addValueReader(new PlaceholderValueReader())
.build();
private final ProfileConditionParser parser = ProfileConditionParser.create();
private UpdateChecker updateChecker;
@@ -88,10 +60,11 @@ public class AdvancedServerList<F>{
private AdvancedServerList(PluginCore<F> plugin, List<PlaceholderProvider> placeholders){
this.plugin = plugin;
this.fileHandler = new FileHandler(this);
this.commandHandler = new CommandHandler(this);
this.playerHandler = new PlayerHandler(this);
this.textCenterUtil = new TextCenterUtil(this);
plugin.pluginLogger().info("Registering internal Placeholders...");
plugin.getPluginLogger().info("Registering internal Placeholders...");
placeholders.forEach(this::addPlaceholder);
@@ -100,52 +73,60 @@ public class AdvancedServerList<F>{
}
resolveVersion();
plugin().pluginLogger().info("Starting <white>AdvancedServerList v%s</white>...", version);
getPlugin().getPluginLogger().info("Starting <white>AdvancedServerList v%s</white>...", version);
load();
}
public static AdvancedServerListAPI api(){
public static <F> AdvancedServerList<F> init(PluginCore<F> plugin, PlaceholderProvider... placeholders){
return new AdvancedServerList<>(plugin, Arrays.asList(placeholders));
}
public static AdvancedServerListAPI getApi(){
return api;
}
public PluginCore<F> plugin(){
public PluginCore<F> getPlugin(){
return plugin;
}
public FileHandler fileHandler(){
public FileHandler getFileHandler(){
return fileHandler;
}
public PlayerHandler playerHandler(){
public CommandHandler getCommandHandler(){
return commandHandler;
}
public PlayerHandler getPlayerHandler(){
return playerHandler;
}
public String version(){
public String getVersion(){
return version;
}
public DefaultExpressionParserEngine parser(){
public ProfileConditionParser getParser(){
return parser;
}
public UpdateChecker updateChecker(){
public UpdateChecker getUpdateChecker(){
return updateChecker;
}
public TextCenterUtil textCenterUtil(){
public TextCenterUtil getTextCenterUtil(){
return textCenterUtil;
}
public void disable(){
plugin().pluginLogger().info("[-] Saving <white>playercache.json</white> file...");
playerHandler().save();
getPlugin().getPluginLogger().info("[-] Saving <white>playercache.json</white> file...");
getPlayerHandler().save();
if(updateChecker != null){
plugin().pluginLogger().info("[-] Disabling Update Checker...");
getPlugin().getPluginLogger().info("[-] Disabling Update Checker...");
updateChecker.disable();
}
plugin().pluginLogger().success("AdvancedServerList disabled!");
getPlugin().getPluginLogger().success("AdvancedServerList disabled!");
}
public void clearFaviconCache(){
@@ -153,69 +134,68 @@ public class AdvancedServerList<F>{
}
public void clearPlayerCache(){
playerHandler().clearCache();
getPlayerHandler().clearCache();
}
private void load(){
if(!fileHandler().loadConfig()){
plugin().pluginLogger().failure("Unable to load <white>config.yml</white>! Check previous lines for errors.");
if(!getFileHandler().loadConfig()){
getPlugin().getPluginLogger().failure("Unable to load <white>config.yml</white>! Check previous lines for errors.");
return;
}
if(fileHandler().isOldConfig()){
plugin().pluginLogger().info("[-] Detected old <white>config.yml</white>. Attempting to migrate...");
if(fileHandler().migrateConfig()){
plugin().pluginLogger().success("Migration completed successfully!");
if(getFileHandler().isOldConfig()){
getPlugin().getPluginLogger().info("[-] Detected old <white>config.yml</white>. Attempting to migrate...");
if(getFileHandler().migrateConfig()){
getPlugin().getPluginLogger().success("Migration completed successfully!");
}else{
plugin().pluginLogger().failure("Couldn't migrate <white>config.yml</white>! Check previous lines for errors.");
getPlugin().getPluginLogger().failure("Couldn't migrate <white>config.yml</white>! Check previous lines for errors.");
return;
}
}
if(fileHandler().getBool(true, "printBanner")){
if(getFileHandler().getBool(true, "printBanner")){
printBanner();
}
plugin().pluginLogger().info("Platform: <white>%s</white>", plugin.platformInfo());
plugin().pluginLogger().info("");
getPlugin().getPluginLogger().info("Platform: <white>%s</white>", plugin.getPlatformInfo());
getPlugin().getPluginLogger().info("");
if(fileHandler().loadProfiles()){
plugin().pluginLogger().success("Successfully loaded <white>%d</white> profile(s)!", fileHandler().profiles().size());
if(getFileHandler().loadProfiles()){
getPlugin().getPluginLogger().success("Successfully loaded <white>%d</white> profile(s)!", getFileHandler().getProfiles().size());
}else{
plugin().pluginLogger().failure("Unable to load profiles! Check previous lines for errors.");
getPlugin().getPluginLogger().failure("Unable to load profiles! Check previous lines for errors.");
return;
}
loadCommands();
getPlugin().loadFaviconHandler(this);
plugin.loadCommands();
plugin.loadEvents();
playerHandler().load();
getPlayerHandler().load();
plugin.loadMetrics();
plugin.startScheduler();
plugin().pluginLogger().success("<green>AdvancedServerList is ready!");
getPlugin().getPluginLogger().success("<green>AdvancedServerList is ready!");
if(fileHandler().getBoolean("checkUpdates"))
if(getFileHandler().getBoolean("checkUpdates"))
this.updateChecker = new UpdateChecker(this);
}
private void printBanner(){
plugin().pluginLogger().info("<#3b90ff> __ _______ ___");
plugin().pluginLogger().info("<#3b90ff> /\\ \\ / ____|_| | |");
plugin().pluginLogger().info("<#3b90ff> / \\ \\ | (_(___ | | |");
plugin().pluginLogger().info("<#3b90ff> / /\\ \\ \\ \\___ \\ \\| | |");
plugin().pluginLogger().info("<#3b90ff> / ____ \\ \\ ____) | | |_|____");
plugin().pluginLogger().info("<#3b90ff>/_/_/ \\_\\_\\_____/_/|______|_|");
plugin().pluginLogger().info("");
seasonalText();
getPlugin().getPluginLogger().info("<#3b90ff> __ _______ ___");
getPlugin().getPluginLogger().info("<#3b90ff> /\\ \\ / ____|_| | |");
getPlugin().getPluginLogger().info("<#3b90ff> / \\ \\ | (_(___ | | |");
getPlugin().getPluginLogger().info("<#3b90ff> / /\\ \\ \\ \\___ \\ \\| | |");
getPlugin().getPluginLogger().info("<#3b90ff> / ____ \\ \\ ____) | | |_|____");
getPlugin().getPluginLogger().info("<#3b90ff>/_/_/ \\_\\_\\_____/_/|______|_|");
getPlugin().getPluginLogger().info("");
}
private void resolveVersion(){
try(InputStream stream = getClass().getResourceAsStream("/version.properties")){
try(InputStream is = getClass().getResourceAsStream("/version.properties")){
Properties properties = new Properties();
properties.load(stream);
properties.load(is);
version = properties.getProperty("version");
}catch(IOException ex){
@@ -224,124 +204,6 @@ public class AdvancedServerList<F>{
}
private void addPlaceholder(PlaceholderProvider placeholderProvider){
AdvancedServerList.api().addPlaceholderProvider(placeholderProvider);
}
private void loadCommands(){
plugin().loadFaviconHandler(this);
CommandManager<CmdSender> commandManager = plugin().commandManager();
commandManager.registerCommandPostProcessor(new CommandTypePostprocessor());
AnnotationParser<CmdSender> annotationParser = new AnnotationParser<>(commandManager, CmdSender.class);
annotationParser.registerBuilderModifier(
CommandType.class,
(annotation, builder) -> builder.meta(CommandHandler.COMMAND_TYPE, annotation.value())
);
annotationParser.parse(
CmdClearCache.create(this),
CmdHelp.create(commandManager),
CmdMigrate.create(this),
CmdProfiles.create(this),
CmdReload.create(this)
);
for(String option : CmdProfiles.ProfileOption.list()){
commandManager.command(
commandManager.commandBuilder("advancedserverlist", "asl")
.literal("profiles")
.literal("set")
.required(
"profile",
StringParser.stringParser(),
Description.of("The profile to edit."),
SuggestionProvider.suggesting(
fileHandler().profiles().stream()
.map(ServerListProfile::file)
.map(Suggestion::suggestion)
.toList()
)
)
.literal(option)
.optional("value", descriptor(option), description(option))
.commandDescription(Description.of("Sets or resets profile values."))
.handler(context -> CommandHandler.handleSet(this, context))
.meta(CommandHandler.OPTION, option)
.permission(
Permission.anyOf(
Permission.permission("advancedserverlist.admin"),
Permission.permission("advancedserverlist.command.profiles"),
Permission.permission("advancedserverlist.command.profiles.set")
)
)
.meta(CommandHandler.COMMAND_TYPE, CommandType.Type.ALL)
);
}
plugin().pluginLogger().success("Loaded Command <white>/advancedserverlist</white>!");
}
private ParserDescriptor<CmdSender, ?> descriptor(String option){
return switch(option){
case "playercount.hideplayers", "playercount.hideplayershover", "playercount.extraplayers.enabled",
"playercount.maxplayers.enabled", "playercount.onlineplayers.enabled" -> BooleanParser.booleanParser();
case "priority" -> IntegerParser.integerParser();
default -> StringParser.greedyStringParser();
};
}
private Description description(String option){
return switch(option){
case "playercount.hideplayers", "playercount.hideplayershover", "playercount.extraplayers.enabled",
"playercount.maxplayers.enabled", "playercount.onlineplayers.enabled" -> Description.of(
"Whether this option should be enabled or not. Leave empty to unset it."
);
case "priority" -> Description.of("Priority this Profile should have. Leave empty to reset.");
case "motd", "playercount.hover" -> Description.of(
"Lines to set. Use \\n or <newline> to set a linebreak. Leave empty to reset."
);
default -> Description.of("The Value to use for this option. Leave empty to reset.");
};
}
// Adding some seasonal messages
private void seasonalText(){
LocalDate date = LocalDate.now();
switch(date.getMonth()){
case JANUARY -> {
if(date.getDayOfMonth() == 1){
plugin().pluginLogger().info("Happy new Year!");
plugin().pluginLogger().info("");
}
}
case JUNE -> {
plugin().pluginLogger().info("Happy Pride Month!");
plugin().pluginLogger().info("");
}
case AUGUST -> {
if(date.getDayOfMonth() == 1){
plugin().pluginLogger().info("Happy Birthday Switzerland!");
plugin().pluginLogger().info("");
}
}
case OCTOBER -> {
if(date.getDayOfMonth() == 31){
plugin().pluginLogger().info("Happy Halloween!");
plugin().pluginLogger().info("");
}
}
case DECEMBER -> {
if(date.getDayOfMonth() == 13){
plugin().pluginLogger().info("Happy Birthday Andre_601!");
plugin().pluginLogger().info("");
}else
if(date.getDayOfMonth() > 23 && date.getDayOfMonth() < 27){
plugin().pluginLogger().info("Merry Christmas!");
plugin().pluginLogger().info("");
}
}
}
AdvancedServerList.getApi().addPlaceholderProvider(placeholderProvider);
}
}
@@ -25,13 +25,13 @@
package ch.andre601.advancedserverlist.core.check;
import org.jspecify.annotations.NonNull;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ThreadFactory;
public class UpdateCheckThread implements ThreadFactory{
@Override
public Thread newThread(@NonNull Runnable r){
public Thread newThread(@NotNull Runnable r){
Thread t = new Thread(r, "AdvancedServerList-UpdateThread");
t.setDaemon(true);
return t;
@@ -28,7 +28,7 @@ package ch.andre601.advancedserverlist.core.check;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.PluginLogger;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.objects.ValueCache;
import ch.andre601.advancedserverlist.core.objects.CacheUtil;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -59,7 +59,7 @@ public class UpdateChecker{
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(new UpdateCheckThread());
private final ValueCache<CompletableFuture<ModrinthVersion>> cache = new ValueCache<>(Duration.ofMinutes(5));
private final CacheUtil.SingletonCache<CompletableFuture<ModrinthVersion>> cache = new CacheUtil.SingletonCache<>(Duration.ofMinutes(5));
private final AdvancedServerList<?> core;
private final PluginLogger logger;
@@ -68,8 +68,8 @@ public class UpdateChecker{
public UpdateChecker(AdvancedServerList<?> core){
this.core = core;
this.logger = core.plugin().pluginLogger();
this.loader = core.plugin().loader();
this.logger = core.getPlugin().getPluginLogger();
this.loader = core.getPlugin().getLoader();
this.client = HttpClient.newBuilder().executor(Executors.newSingleThreadExecutor(new UpdateCheckThread())).build();
startUpdateChecker();
@@ -88,10 +88,10 @@ public class UpdateChecker{
if(version == null)
return;
int result = version.compare(core.version());
int result = version.compare(core.getVersion());
if(result == -1){
sender.sendPrefixedMsg("<green>A new Version of AdvancedServerList is available!");
sender.sendPrefixedMsg("<green>Your version: <white>%s", core.version());
sender.sendPrefixedMsg("<green>Your version: <white>%s", core.getVersion());
sender.sendPrefixedMsg("<green>Version on Modrinth: <white>%s", version.versionNumber());
sender.sendPrefixedMsg(
"<click:open_url:'https://modrinth.com/plugin/advancedserverlist/version/%s'>" +
@@ -101,12 +101,8 @@ public class UpdateChecker{
);
}else
if(result == 1){
sender.sendPrefixedMsg(
"Your version (<white>%</white>) is newer than what is available on Modrinth (<white>%s</white>).",
core.version(),
version.versionNumber()
);
sender.sendPrefixedMsg("Are you using a Development Version?");
sender.sendPrefixedMsg("You seem to run a newer Version (<white>%</white>) than what is available on Modrinth (<white>%s</white>).", core.getVersion(), version.versionNumber());
sender.sendPrefixedMsg("Are you running a development build?");
}
});
}
@@ -137,18 +133,14 @@ public class UpdateChecker{
logger.debug(UpdateChecker.class, "Comparing versions...");
int result = version.compare(core.version());
int result = version.compare(core.getVersion());
switch(result){
case -1 -> printUpdateBanner(version);
case 0 -> logger.success("You are running the latest version!");
case 1 -> {
logger.info(
"[?] Your version (<white>%s</white>) is newer than what is available on Modrint (<white>%s</white>).",
core.version(),
version.versionNumber()
);
logger.info(" Are you using a Development Version?");
}
case 1 -> logger.info(
"[?] Your version (<white>%s</white>) seems to be newer than the latest release (<white>%s</white>). Are you running a dev build?",
core.getVersion(), version.versionNumber()
);
}
});
}, 0L, 12L, TimeUnit.HOURS);
@@ -163,7 +155,7 @@ public class UpdateChecker{
logger.debug(UpdateChecker.class, "Checking '%s' for updates...", finalUrl);
if(core.version().equals("UNKNOWN")){
if(core.getVersion().equals("UNKNOWN")){
logger.warn("Cannot perform update check. Plugin version is 'UNKNOWN'!");
return CompletableFuture.completedFuture(null);
}
@@ -173,7 +165,7 @@ public class UpdateChecker{
request = HttpRequest.newBuilder()
.uri(new URI(finalUrl))
.timeout(Duration.ofSeconds(5))
.header("User-Agent", "AdvancedServerList-" + loader + "/" + core.version())
.header("User-Agent", "AdvancedServerList-" + loader + "/" + core.getVersion())
.build();
}catch(URISyntaxException ex){
logger.warn("Cannot perform update check. URL '%s' is not a valid URI.", ex, finalUrl);
@@ -193,7 +185,7 @@ public class UpdateChecker{
logger.warn("=======================================================================");
logger.warn("You are running an outdated version of AdvancedServerList!");
logger.warn("");
logger.warn("Your version: <white>%s</white>", core.version());
logger.warn("Your version: <white>%s</white>", core.getVersion());
logger.warn("Modrinth: <white>%s</white>", version.versionNumber());
logger.warn("");
@@ -1,54 +0,0 @@
package ch.andre601.advancedserverlist.core.commands;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.interfaces.commands.CommandType;
import org.incendo.cloud.annotations.Argument;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.CommandDescription;
import org.incendo.cloud.annotations.Permission;
import org.jspecify.annotations.Nullable;
@Command("advancedserverlist|asl")
@CommandDescription("Main Command of the plugin")
@Permission("advancedserverlist.admin")
@CommandType(CommandType.Type.ALL)
public record CmdClearCache(AdvancedServerList<?> core){
public static CmdClearCache create(AdvancedServerList<?> core){
return new CmdClearCache(core);
}
@Command("clearcache [target]")
@CommandDescription("Cleares the Favicon and/or Player Cache.")
@Permission({"advancedserverlist.admin", "advancedserverlist.command.clearcache"})
@CommandType(CommandType.Type.ALL)
public void handle(
CmdSender sender,
@Nullable @Argument(description = "The type of Cache to clean. Defaults to ALL.") CacheTarget target
){
if(target == null)
target = CacheTarget.ALL;
sender.sendPrefixedMsg("Clearing cache...");
if(target == CacheTarget.ALL || target == CacheTarget.FAVICON){
core().clearFaviconCache();
sender.sendPrefixedMsg("Cleared Favicon Cache.");
}
if(target == CacheTarget.ALL || target == CacheTarget.PLAYER){
core().clearPlayerCache();
sender.sendPrefixedMsg("Cleared Player Cache.");
}
sender.sendPrefixedMsg("<green>Cache clearing complete!");
}
public enum CacheTarget{
ALL,
FAVICON,
PLAYER
}
}
@@ -1,65 +0,0 @@
package ch.andre601.advancedserverlist.core.commands;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.interfaces.commands.CommandType;
import net.kyori.adventure.text.format.NamedTextColor;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.annotation.specifier.Greedy;
import org.incendo.cloud.annotations.Argument;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.CommandDescription;
import org.incendo.cloud.annotations.Permission;
import org.incendo.cloud.annotations.suggestion.Suggestions;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.help.result.CommandEntry;
import org.incendo.cloud.minecraft.extras.MinecraftHelp;
import org.jspecify.annotations.Nullable;
import java.util.List;
@Command("advancedserverlist|asl")
@CommandDescription("Main Command of the plugin")
@Permission("advancedserverlist.admin")
@CommandType(CommandType.Type.ALL)
public record CmdHelp(CommandManager<CmdSender> manager, MinecraftHelp<CmdSender> help){
public static CmdHelp create(CommandManager<CmdSender> manager){
MinecraftHelp<CmdSender> help = MinecraftHelp.<CmdSender>builder()
.commandManager(manager)
.audienceProvider(CmdSender::audience)
.commandPrefix("/asl help")
.colors(MinecraftHelp.helpColors(
NamedTextColor.WHITE, NamedTextColor.AQUA, NamedTextColor.WHITE, NamedTextColor.GRAY, NamedTextColor.DARK_GRAY
))
.build();
return new CmdHelp(manager, help);
}
@Command("help [query]")
@CommandDescription("Displays all commands or provides info about a specific one.")
@Permission({"advancedserverlist.admin", "advancedserverlist.command.help"})
@CommandType(CommandType.Type.ALL)
public void handle(
CmdSender sender,
@Nullable @Argument(description = "Search Query for a specific command", suggestions = "help") @Greedy String query
){
this.help.queryCommands(query == null ? "" : query, sender);
}
@Suggestions("help")
public List<String> helpQueries(CommandContext<CmdSender> context) {
return this.manager.createHelpHandler()
.queryRootIndex(context.sender())
.entries()
.stream()
.filter(entry -> context.hasPermission(entry.command().commandPermission()))
.filter(entry -> {
CommandType.Type type = entry.command().commandMeta().getOrDefault(CommandHandler.COMMAND_TYPE, CommandType.Type.ALL);
return type == CommandType.Type.ALL || type == CommandType.Type.PLAYER_ONLY && context.sender().isPlayer();
})
.map(CommandEntry::syntax)
.toList();
}
}
@@ -1,78 +0,0 @@
package ch.andre601.advancedserverlist.core.commands;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.interfaces.commands.CommandType;
import ch.andre601.advancedserverlist.core.migration.minimotd.MiniMOTDConfigMigrator;
import ch.andre601.advancedserverlist.core.migration.serverlistplus.SLPConfigMigrator;
import org.incendo.cloud.annotations.Argument;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.CommandDescription;
import org.incendo.cloud.annotations.Permission;
import org.jspecify.annotations.NonNull;
import java.util.Locale;
@Command("advancedserverlist|asl")
@CommandDescription("Main Command of the plugin")
@Permission("advancedserverlist.admin")
@CommandType(CommandType.Type.ALL)
public record CmdMigrate(AdvancedServerList<?> core){
public static CmdMigrate create(AdvancedServerList<?> core){
return new CmdMigrate(core);
}
@Command("migrate <plugin>")
@CommandDescription("Migrates Profiles from supported Plugins to ASL.")
@Permission({"advancedserverlist.admin", "advancedserverlist.command.migrate"})
@CommandType(CommandType.Type.ALL)
public void handle(
CmdSender sender,
@NonNull @Argument(description = "Name of the plugin to migrate from") MigratablePlugin plugin
){
if(plugin == MigratablePlugin.SERVERLISTPLUS){
if(!core().plugin().isPluginEnabled("ServerListPlus")){
sender.sendErrorMsg("<red>ServerListPlus not enabled.");
sender.sendErrorMsg("<red>The plugin needs to be enabled for the migration to work.");
return;
}
sender.sendPrefixedMsg("Migrating from ServerListPlus...");
int migrated = SLPConfigMigrator.migrate(core(), sender);
if(migrated == 0){
sender.sendErrorMsg("<red>Couldn't migrate any profiles from ServerListPlus.");
sender.sendErrorMsg("<red>See the server console for any errors.");
return;
}else{
sender.sendPrefixedMsg("<green>Successfully migrated </green>%d<green> Profile(s)!", migrated);
}
}else
if(plugin == MigratablePlugin.MINIMOTD){
sender.sendPrefixedMsg("Migrating from MiniMOTD...");
if(MiniMOTDConfigMigrator.migrate(core(), sender)){
sender.sendPrefixedMsg("<green>Successfully migrated config from MiniMOTD!");
}else{
sender.sendErrorMsg("<red>Couldn't migrate config from MiniMOTD.");
sender.sendErrorMsg("<red>See the server console for any errors.");
return;
}
}else{
sender.sendErrorMsg("<red>Received invalid Plugin </red>%s<red>!", plugin.toString());
sender.sendErrorMsg("<red>Only the following plugins are supported:");
for(MigratablePlugin pl : MigratablePlugin.values()){
sender.sendErrorMsg("- <click:suggest_command:/asl migrate %s>%s</click>", pl.name().toLowerCase(Locale.ROOT), pl.name().toLowerCase(Locale.ROOT));
}
}
sender.sendPrefixedMsg("<green>Run <white><click:suggest_command:/asl reload>/asl reload</click></white> to load the new Profile(s).");
}
public enum MigratablePlugin{
SERVERLISTPLUS,
MINIMOTD
}
}
@@ -1,483 +0,0 @@
package ch.andre601.advancedserverlist.core.commands;
import ch.andre601.advancedserverlist.api.profiles.ProfileEntry;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.interfaces.commands.CommandType;
import ch.andre601.advancedserverlist.core.interfaces.dialogs.IDialog;
import ch.andre601.advancedserverlist.core.interfaces.dialogs.IDialogList;
import ch.andre601.advancedserverlist.core.profiles.ServerListProfile;
import ch.andre601.advancedserverlist.core.profiles.profile.ProfileSerializer;
import org.incendo.cloud.annotations.Argument;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.CommandDescription;
import org.incendo.cloud.annotations.Permission;
import org.incendo.cloud.annotations.suggestion.Suggestions;
import org.incendo.cloud.context.CommandInput;
import org.jspecify.annotations.NonNull;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.yaml.NodeStyle;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@Command("advancedserverlist|asl")
@CommandDescription("Main Command of the plugin.")
@Permission("advancedserverlist.admin")
@CommandType(CommandType.Type.ALL)
public record CmdProfiles(AdvancedServerList<?> core){
public static CmdProfiles create(AdvancedServerList<?> core){
return new CmdProfiles(core);
}
@Command("profiles add <name>")
@CommandDescription("Creates a new Profile with default values applied.")
@Permission({"advancedserverlist.admin", "advancedserverlist.command.profiles", "advancedserverlist.command.profiles.add"})
@CommandType(CommandType.Type.ALL)
public void add(
CmdSender sender,
@NonNull @Argument(description = "Name of the profile to create.") String name
){
if(name.isEmpty()){
sender.sendErrorMsg("<red>Insufficient arguments.");
sender.sendErrorMsg("<red>A profile name needs to be provided.");
return;
}
if(CommandHandler.profile(core(), name) != null){
sender.sendErrorMsg("<red>A profile with the name </red>%s<red> already exists.", name);
return;
}
String file = CommandHandler.profileName(name);
if(core().fileHandler().createFile(file)){
sender.sendPrefixedMsg("<green>Successfully created </green>%s<green>!", file);
sender.sendPrefixedMsg("<green>Use <white><click:suggest_command:/asl reload>/asl reload</click></white> to load the file.");
}else{
sender.sendErrorMsg("<red>Error while creating new profile.");
sender.sendErrorMsg("<red>Check the server console for details.");
}
}
@Command("profiles copy <profile> <name>")
public void copy(
CmdSender sender,
@NonNull @Argument(description = "Name of the profile to copy.", suggestions = "profiles") String profile,
@NonNull @Argument(description = "Name of the copy to have.") String name
){
if(profile.isEmpty() || name.isEmpty()){
sender.sendErrorMsg("<red>Insufficient arguments.");
sender.sendErrorMsg("<red>A profile name and name for the copy need to be provided.");
return;
}
String profileName = CommandHandler.profileName(profile);
String copyName = CommandHandler.profileName(name);
ServerListProfile original = CommandHandler.profile(core(), profileName);
if(original == null){
sender.sendErrorMsg("<red>A profile with name </red>%s<red> does not exist.", profile);
return;
}
boolean copyExists = CommandHandler.profile(core(), copyName) != null;
if(copyExists){
sender.sendErrorMsg("<red>A profile with name </red>%s<red> already exists.", name);
return;
}
Path profilePath = core().plugin().folderPath().resolve("profiles").resolve(copyName);
if(!Files.exists(profilePath)){
try{
Files.createFile(profilePath);
}catch(IOException ex){
sender.sendErrorMsg("<red>Encountered an IOException while creating </red>%s<red>.", copyName);
sender.sendErrorMsg("<red>Please see the server console for further details.");
core.plugin().pluginLogger().warn("Encountered IOException while creating file <white>%s</white>", ex, copyName);
return;
}
YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
.path(profilePath)
.indent(2)
.nodeStyle(NodeStyle.BLOCK)
.defaultOptions(opt -> opt.serializers(builder -> builder.register(ProfileEntry.class, ProfileSerializer.INSTANCE)))
.build();
ConfigurationNode node;
try{
node = loader.load();
}catch(IOException ex){
sender.sendErrorMsg("<red>Encountered an IOException while loading file </red>%s<red>.", copyName);
sender.sendErrorMsg("<red>Please see the server console for further details.");
core().plugin().pluginLogger().warn("Encountered IOException while loading file <white>%s</white>.", ex, copyName);
return;
}
try{
node.node("priority").set(original.priority());
node.node("condition").set(original.condition());
node.node("profiles").setList(ProfileEntry.class, original.profiles());
node.set(ProfileEntry.class, original.defaultProfile());
}catch(SerializationException ex){
sender.sendErrorMsg("<red>Encountered a SerializationException while copying values from </red>%s<red> to </red>%s<red>.", profileName, copyName);
sender.sendErrorMsg("<red>Please see the server console for further details.");
core().plugin().pluginLogger().warn("<red>Encountered a SerializationException while copying values from </red>%s<red> to </red>%s<red>.", ex, profileName, copyName);
return;
}
try{
loader.save(node);
sender.sendPrefixedMsg("<green>Successfully created copy of </green>%s<green> as </green>%s<green>!", profileName, copyName);
sender.sendPrefixedMsg("<green>Use <white><click:suggest_command:/asl reload>/asl reload</click></white> to load the file.");
}catch(IOException ex){
sender.sendErrorMsg("<red>There was an IOException while loading file </red>%s<red>!", copyName);
sender.sendErrorMsg("<red>Please see the server console for further details.");
core.plugin().pluginLogger().warn("Encountered an IOException while loading file <white>%s</white>", ex, copyName);
}
}
}
@Command("profiles edit <profile>")
@CommandDescription("Opens a Dialog to edit a specific profile ingame.")
@Permission({"advancedserverlist.admin", "advancedserverlist.command.profiles", "advancedserverlist.command.profiles.edit"})
@CommandType(CommandType.Type.PLAYER_ONLY)
public void edit(
CmdSender sender,
@NonNull @Argument(description = "Name of the profile to edit.", suggestions = "profiles") String profile
){
if(!core().plugin().isDialogsSupported()){
sender.sendErrorMsg("Dialogs are not supported on this server or proxy version.");
return;
}
ServerListProfile slp = CommandHandler.profile(core(), profile);
if(slp == null){
sender.sendErrorMsg("<red>A profile with name </red>%s<red> does not exist.", profile);
return;
}
IDialogList.IDialogListBuilder listBuilder = core().plugin().createDialogListBuilder(slp.file());
String warning = """
<gold><bold>WARNING!</bold></gold>
Dialog-based Profile editing is currently an <italic>experimental feature</italic>!
Please create backups of your profiles before using this feature and report any bugs and/or glitches you encounter.""";
listBuilder.appendBody(warning);
listBuilder.appendDialog(
createDialog("Main Settings", warning, slp, slp.defaultProfile(), slp.priority(), slp.condition(), -1)
);
for(int i = 0; i < slp.profiles().size(); i++){
listBuilder.appendDialog(
createDialog("Profiles #" + (i + 1), warning, slp, slp.profiles().get(i), -1, null, i)
);
}
listBuilder.build().show(sender);
}
@Command("profiles info <profile>")
@CommandDescription("Provides info about a specific profile.")
@Permission({"advancedserverlist.admin", "advancedserverlist.command.profiles", "advancedserverlist.command.profiles.info"})
@CommandType(CommandType.Type.PLAYER_ONLY)
public void info(
CmdSender sender,
@NonNull @Argument(description = "Name of the profile to view.", suggestions = "profiles") String profile
){
if(profile.isEmpty()){
sender.sendErrorMsg("<red>Insufficient arguments.");
sender.sendErrorMsg("<red>A profile name needs to be provided.");
return;
}
ServerListProfile slp = CommandHandler.profile(core(), profile);
if(slp == null){
sender.sendErrorMsg("<red>Unable to find profile with name </red>%s<red>.", profile);
sender.sendErrorMsg("<red>Make sure you typed it correctly.");
return;
}
ProfileOptionsSender optionsSender = new ProfileOptionsSender(slp.file());
optionsSender
.append(
ProfileOptionsSender.OptionStringBuilder.of("Priority", slp.priority())
.option(ProfileOption.PRIORITY)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Condition", slp.condition())
.option(ProfileOption.CONDITION)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Profiles")
.color(slp.profiles().isEmpty() ? ProfileOptionsSender.ColorName.RED : ProfileOptionsSender.ColorName.GREEN)
);
if(!slp.profiles().isEmpty()){
for(int i = 0; i < slp.profiles().size(); i++){
optionsSender.append(
ProfileOptionsSender.OptionStringBuilder.of("#" + (i + 1), profileHover(slp.profiles().get(i)))
.prefix("│ %s─".formatted((i + 1) == slp.profiles().size() ? "└" : "├"))
.color(ProfileOptionsSender.ColorName.AQUA)
);
}
}
ProfileEntry entry = slp.defaultProfile();
optionsSender
.append(
ProfileOptionsSender.OptionStringBuilder.of("MOTD", entry.motd())
.option(ProfileOption.MOTD)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Favicon", entry.favicon())
.option(ProfileOption.FAVICON)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Player count")
.prefix("└─")
.color(ProfileOptionsSender.ColorName.AQUA)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Hide Player count?", entry.hidePlayersEnabled())
.prefix(" ├─")
.option(ProfileOption.PLAYERCOUNT_HIDEPLAYERS)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Hide Player count hover?", entry.hidePlayersHoverEnabled())
.prefix(" ├─")
.option(ProfileOption.PLAYERCOUNT_HIDEPLAYERSHOVER)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Player count hover", entry.players())
.prefix(" ├─")
.option(ProfileOption.PLAYERCOUNT_HOVER)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Player count text", entry.playerCountText())
.prefix(" ├─")
.option(ProfileOption.PLAYERCOUNT_TEXT)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Extra Players")
.prefix(" ├─")
.color(ProfileOptionsSender.ColorName.AQUA)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Enabled?", entry.extraPlayersEnabled())
.prefix(" │ ├─")
.option(ProfileOption.PLAYERCOUNT_EXTRAPLAYERS_ENABLED)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Amount", entry.extraPlayersCount())
.prefix(" │ └─")
.option(ProfileOption.PLAYERCOUNT_EXTRAPLAYERS_AMOUNT)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Max Players")
.prefix(" ├─")
.color(ProfileOptionsSender.ColorName.AQUA)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Enabled?", entry.maxPlayersEnabled())
.prefix(" │ ├─")
.option(ProfileOption.PLAYERCOUNT_MAXPLAYERS_ENABLED)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Amount", entry.maxPlayersCount())
.prefix(" │ └─")
.option(ProfileOption.PLAYERCOUNT_MAXPLAYERS_AMOUNT)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Online Players")
.prefix(" └─")
.color(ProfileOptionsSender.ColorName.AQUA)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Enabled?", entry.onlinePlayersEnabled())
.prefix(" ├─")
.option(ProfileOption.PLAYERCOUNT_ONLINEPLAYERS_ENABLED)
).append(
ProfileOptionsSender.OptionStringBuilder.of("Amount", entry.onlinePlayersCount())
.prefix(" └─")
.option(ProfileOption.PLAYERCOUNT_ONLINEPLAYERS_AMOUNT)
).send(sender);
}
@Command("profiles list")
@CommandDescription("Lists all available profiles with priority, conditions and if they are valid.")
@Permission({"advancedserverlist.admin", "advancedserverlist.command.profiles", "advancedserverlist.command.profiles.list"})
@CommandType(CommandType.Type.ALL)
public void list(CmdSender sender){
sender.sendMsg();
sender.sendPrefixedMsg("Available Profiles:");
sender.sendMsg("<dark_grey>|");
List<ServerListProfile> profiles = core().fileHandler().profiles().stream()
.sorted(Comparator.comparingInt(ServerListProfile::priority).reversed())
.toList();
if(profiles.isEmpty()){
sender.sendMsg("<dark_grey></dark_grey> <i>No profiles found.</i>");
}else{
for(int i = 0; i < profiles.size(); i++){
sender.sendMsg(
"<dark_grey>%s</dark_grey> <aqua>" +
"<hover:show_text:\"%s\">" +
"<click:suggest_command:/asl profiles info %s>" +
"%s" +
"</click>" +
"</hover>" +
"</aqua>",
(i + 1) == profiles.size() ? "" : "-",
hover(profiles.get(i)),
profiles.get(i).file(),
profiles.get(i).file()
);
}
}
sender.sendMsg();
sender.sendMsg("<grey><i>Hover</i></grey> = Show details.");
sender.sendMsg("<grey><i>Click</i></grey> = Get Command.");
}
@Suggestions("profiles")
public List<String> profilesSuggestions(CommandInput input){
String name = input.readString();
List<String> profiles = core().fileHandler().profiles().stream()
.map(profile -> profile.file().substring(0, profile.file().lastIndexOf('.')))
.toList();
if(name.isEmpty())
return profiles;
List<String> matches = new ArrayList<>(profiles.size());
for(String str : profiles){
if(str.length() >= name.length() && str.regionMatches(true, 0, name, 0, name.length()))
matches.add(str);
}
return matches;
}
private String hover(ServerListProfile profile){
return """
<grey>Priority: <white>%d</white>
Condition: <white>%s</white>
Valid? %s""".formatted(
profile.priority(),
profile.condition() == null || profile.condition().isEmpty() ? "<i>None</i>" : profile.condition(),
profile.isInvalidProfile() ? "<red>No</red>" : "<green>Yes</green>"
);
}
private String profileHover(ProfileEntry entry){
return """
<grey>MOTD:
%s<reset>
<grey>Favicon: %s<reset>
<grey>Hide Player count? %s
Hide Player count Hover? %s
Player count Hover:
%s<reset>
<grey>Player count Text: %s<reset>
<grey>Extra Players: %s<reset>
<grey>Max Players: %s<reset>
<grey>Online Players: %s""".formatted(
entry.motd() == null || entry.motd().isEmpty() ?
"<i><white>None</white></i>" : String.join("<reset>\n", entry.motd()),
entry.favicon() == null || entry.favicon().isEmpty() ?
"<i><white>None</white></i>" : entry.favicon(),
entry.hidePlayersEnabled().getOrDefault(false) ? "<green>Enabled</green>" : "<red>Disabled</red>",
entry.hidePlayersHoverEnabled().getOrDefault(false) ? "<green>Enabled</green>" : "<red>Disabled</red>",
entry.players().isEmpty() ? "<i><white>None</white></i>" : String.join("<reset>\n", entry.players()),
entry.playerCountText() == null || entry.playerCountText().isEmpty() ?
"<i><white>None</white></i>" : entry.playerCountText(),
entry.extraPlayersEnabled().getOrDefault(false) ? entry.extraPlayersCount() : "<red>Disabled</red>",
entry.maxPlayersEnabled().getOrDefault(false) ? entry.maxPlayersCount() : "<red>Disabled</red>",
entry.onlinePlayersEnabled().getOrDefault(false) ? entry.onlinePlayersCount() : "<red>Disabled</red>"
);
}
private IDialog<?> createDialog(
String title,
String warning,
ServerListProfile slp,
ProfileEntry entry,
int priority,
String condition,
int index
){
IDialog.IDialogBuilder builder = core.plugin().createDialogBuilder(title, slp, index);
builder.appendBody(warning);
if(index < 0){
builder.appendNumberRange(ProfileOption.PRIORITY, "Priority", priority)
.appendTextInput(ProfileOption.CONDITION, "Condition", condition, 100);
}
builder.appendTextbox(ProfileOption.MOTD, "MOTD", entry.motd(), 200, 2)
.appendTextInput(ProfileOption.FAVICON, "Favicon", entry.favicon(), 100)
.appendBoolean(ProfileOption.PLAYERCOUNT_HIDEPLAYERS, "Hide Player Count", entry.hidePlayersEnabled())
.appendBoolean(ProfileOption.PLAYERCOUNT_HIDEPLAYERSHOVER, "Hide Player Count Hover", entry.hidePlayersHoverEnabled())
.appendTextbox(ProfileOption.PLAYERCOUNT_HOVER, "Player Count Hover", entry.players(), 200, 10)
.appendTextInput(ProfileOption.PLAYERCOUNT_TEXT, "Player Count Text", entry.playerCountText(), 100)
.appendBoolean(ProfileOption.PLAYERCOUNT_EXTRAPLAYERS_ENABLED, "Enable Extra Players", entry.extraPlayersEnabled())
.appendTextInput(ProfileOption.PLAYERCOUNT_EXTRAPLAYERS_AMOUNT, "Extra Players Count", entry.extraPlayersCount(), 100)
.appendBoolean(ProfileOption.PLAYERCOUNT_MAXPLAYERS_ENABLED, "Enable Max Players", entry.maxPlayersEnabled())
.appendTextInput(ProfileOption.PLAYERCOUNT_MAXPLAYERS_AMOUNT, "Max Players Count", entry.maxPlayersCount(), 100)
.appendBoolean(ProfileOption.PLAYERCOUNT_ONLINEPLAYERS_ENABLED, "Enable Online Players", entry.onlinePlayersEnabled())
.appendTextInput(ProfileOption.PLAYERCOUNT_ONLINEPLAYERS_AMOUNT, "Online Players Count", entry.onlinePlayersCount(), 100);
return builder.build();
}
public enum ProfileOption{
PRIORITY,
CONDITION,
MOTD,
FAVICON,
PLAYERCOUNT_HIDEPLAYERS,
PLAYERCOUNT_HIDEPLAYERSHOVER,
PLAYERCOUNT_HOVER,
PLAYERCOUNT_TEXT,
PLAYERCOUNT_EXTRAPLAYERS_ENABLED,
PLAYERCOUNT_EXTRAPLAYERS_AMOUNT,
PLAYERCOUNT_MAXPLAYERS_ENABLED,
PLAYERCOUNT_MAXPLAYERS_AMOUNT,
PLAYERCOUNT_ONLINEPLAYERS_ENABLED,
PLAYERCOUNT_ONLINEPLAYERS_AMOUNT;
public static List<String> list(){
return Arrays.stream(values())
.map(ProfileOption::toString)
.toList();
}
public static ProfileOption fromString(String input){
for(ProfileOption option : values()){
if(option.toString().equalsIgnoreCase(input))
return option;
}
return null;
}
@Override
public String toString(){
return this.name().toLowerCase(Locale.ROOT).replace("_", ".");
}
public String toDialogKey(){
return this.name().toLowerCase(Locale.ROOT);
}
}
}
@@ -1,44 +0,0 @@
package ch.andre601.advancedserverlist.core.commands;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.interfaces.commands.CommandType;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.CommandDescription;
import org.incendo.cloud.annotations.Permission;
@Command("advancedserverlist|asl")
@CommandDescription("Main Command of the plugin")
@Permission("advancedserverlist.admin")
@CommandType(CommandType.Type.ALL)
public record CmdReload(AdvancedServerList<?> core){
public static CmdReload create(AdvancedServerList<?> core){
return new CmdReload(core);
}
@Command("reload")
@CommandDescription("Reloads the plugin's main config and its profiles.")
@Permission({"advancedserverlist.admin", "advancedserverlist.command.reload"})
@CommandType(CommandType.Type.ALL)
public void handle(CmdSender sender){
sender.sendPrefixedMsg("Reloading plugin...");
if(core().fileHandler().reloadConfig()){
sender.sendPrefixedMsg("<green>Reloaded </green>config.yml<green>!");
}else{
sender.sendErrorMsg("<red>Encountered an error while reloading the </red>config.yml<red>.");
sender.sendErrorMsg("<red>See the server console for details.");
}
if(core().fileHandler().reloadProfiles()){
sender.sendPrefixedMsg("<green>(Re)loaded <white>%d</white> Profile(s)!", core().fileHandler().profiles().size());
}else{
sender.sendErrorMsg("<red>Encountered errors while (re)loading profiles.");
sender.sendErrorMsg("<red>See the server console for details.");
}
sender.sendPrefixedMsg("<green>Reload complete!");
}
}
@@ -1,7 +1,7 @@
/*
* MIT License
*
* Copyright (c) 2022-2025 Andre_601
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -20,329 +20,456 @@
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.commands;
import ch.andre601.advancedserverlist.api.objects.NullBool;
import ch.andre601.advancedserverlist.api.profiles.ProfileEntry;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.interfaces.commands.CommandType;
import ch.andre601.advancedserverlist.core.interfaces.commands.PluginCommand;
import ch.andre601.advancedserverlist.core.migration.minimotd.MiniMOTDConfigMigrator;
import ch.andre601.advancedserverlist.core.migration.serverlistplus.SLPConfigMigrator;
import ch.andre601.advancedserverlist.core.profiles.ServerListProfile;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.key.CloudKey;
import ch.andre601.advancedserverlist.core.profiles.profile.ProfileSerializer;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.yaml.NodeStyle;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.util.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
public class CommandHandler{
public static final CloudKey<CommandType.Type> COMMAND_TYPE = CloudKey.of("command_type", CommandType.Type.class);
public static final CloudKey<String> OPTION = CloudKey.of("profile_option", String.class);
private final List<PluginCommand> subCommands;
public static void handleSet(AdvancedServerList<?> core, CommandContext<CmdSender> context){
CmdSender sender = context.sender();
String profile = context.get("profile");
String value = getOptionValue(context.getOrDefault("value", null));
String option = context.command().commandMeta().getOrDefault(OPTION, "");
ServerListProfile slp = profile(core, profile);
if(slp == null){
sender.sendErrorMsg("<red>No profile with name</red> %s <red>found!", profile);
public CommandHandler(AdvancedServerList<?> core){
subCommands = List.of(
new Help(core),
new Reload(core),
new ClearCache(core),
new Migrate(core),
new Profiles(core)
);
}
public List<PluginCommand> getSubCommands(){
return subCommands;
}
public void handle(CmdSender sender, String[] args){
if(args.length == 0){
sender.sendErrorMsg("<red>Too few arguments! Use <grey>/asl help</grey> for a list of commands.");
return;
}
int priority = slp.priority();
String condition = slp.condition();
ProfileEntry entry = slp.defaultProfile();
ProfileEntry.Builder builder = slp.defaultProfile().builder();
List<String> oldValues = new ArrayList<>();
List<String> newValues = new ArrayList<>();
switch(CmdProfiles.ProfileOption.fromString(option)){
case PRIORITY -> {
oldValues.add(String.valueOf(slp.priority()));
if(value == null || value.isEmpty()){
priority = 0;
break;
}
try{
priority = Integer.parseInt(value);
newValues.add(String.valueOf(priority));
}catch(NumberFormatException ex){
sender.sendErrorMsg("<red>Invalid value provided. Expected number but got</red> %s", value);
for(PluginCommand subCommand : subCommands){
if(subCommand.name().equalsIgnoreCase(args[0])){
if(!sender.hasPermission(subCommand.permission())){
sender.sendErrorMsg(
"<red>You do not have the permission <grey>%s</grey> to execute this command.",
subCommand.permission()
);
return;
}
}
case CONDITION -> {
oldValues.add(slp.condition());
if(value == null || value.isEmpty()){
condition = "";
break;
}
condition = value;
newValues.add(value);
}
case MOTD -> {
oldValues.addAll(entry.motd());
if(value == null || value.isEmpty()){
builder.motd(Collections.emptyList());
break;
}
String[] lines = value.split("\\\\n|<newline>|<br>");
if(lines.length == 0){
builder.motd(Collections.emptyList());
break;
}
if(lines.length > 1){
if(lines.length > 2)
sender.sendPrefixedMsg("<gold>Found %d Lines for MOTD. Only using first 2 lines...", lines.length);
builder.motd(Arrays.asList(lines));
}else{
builder.motd(Collections.singletonList(lines[0]));
}
newValues.addAll(Arrays.asList(lines));
}
case FAVICON -> {
oldValues.add(entry.favicon());
if(value == null || value.isEmpty()){
builder.favicon("");
break;
}
builder.favicon(value);
newValues.add(value);
}
case PLAYERCOUNT_HIDEPLAYERS -> {
if(!entry.hidePlayersEnabled().isNotSet())
oldValues.add(String.valueOf(entry.hidePlayersEnabled().getOrDefault(false)));
if(value == null || value.isEmpty()){
builder.hidePlayersEnabled(NullBool.NOT_SET);
break;
}
NullBool bool = NullBool.resolve(Boolean.parseBoolean(value));
builder.hidePlayersEnabled(bool);
newValues.add(String.valueOf(bool.getOrDefault(false)));
}
case PLAYERCOUNT_HIDEPLAYERSHOVER -> {
if(!entry.hidePlayersHoverEnabled().isNotSet())
oldValues.add(String.valueOf(entry.hidePlayersHoverEnabled().getOrDefault(false)));
if(value == null || value.isEmpty()){
builder.hidePlayersHoverEnabled(NullBool.NOT_SET);
break;
}
NullBool bool = NullBool.resolve(Boolean.parseBoolean(value));
builder.hidePlayersHoverEnabled(bool);
newValues.add(String.valueOf(bool.getOrDefault(false)));
}
case PLAYERCOUNT_HOVER -> {
oldValues.addAll(entry.players());
if(value == null || value.isEmpty()){
builder.players(Collections.emptyList());
break;
}
String[] lines = value.split("\\\\n|<newline>");
if(lines.length == 0){
builder.players(Collections.emptyList());
break;
}
builder.players(Arrays.asList(lines));
newValues.addAll(Arrays.asList(lines));
}
case PLAYERCOUNT_TEXT -> {
if(!entry.playerCountText().isEmpty())
oldValues.add(entry.playerCountText());
if(value == null || value.isEmpty()){
builder.playerCountText("");
break;
}
builder.playerCountText(value);
newValues.add(value);
}
case PLAYERCOUNT_EXTRAPLAYERS_ENABLED -> {
if(!entry.extraPlayersEnabled().isNotSet())
oldValues.add(String.valueOf(entry.extraPlayersEnabled().getOrDefault(false)));
if(value == null || value.isEmpty()){
builder.extraPlayersEnabled(NullBool.NOT_SET);
break;
}
NullBool bool = NullBool.resolve(Boolean.parseBoolean(value));
builder.extraPlayersEnabled(bool);
newValues.add(String.valueOf(bool.getOrDefault(false)));
}
case PLAYERCOUNT_EXTRAPLAYERS_AMOUNT -> {
if(!entry.extraPlayersCount().isEmpty())
oldValues.add(entry.extraPlayersCount());
if(value == null || value.isEmpty()){
builder.extraPlayersCount("");
break;
}
builder.extraPlayersCount(value);
newValues.add(value);
}
case PLAYERCOUNT_MAXPLAYERS_ENABLED -> {
if(!entry.maxPlayersEnabled().isNotSet())
oldValues.add(String.valueOf(entry.maxPlayersEnabled().getOrDefault(false)));
if(value == null || value.isEmpty()){
builder.maxPlayersEnabled(NullBool.NOT_SET);
break;
}
NullBool bool = NullBool.resolve(Boolean.parseBoolean(value));
builder.maxPlayersEnabled(bool);
newValues.add(String.valueOf(bool.getOrDefault(false)));
}
case PLAYERCOUNT_MAXPLAYERS_AMOUNT -> {
if(!entry.maxPlayersCount().isEmpty())
oldValues.add(entry.extraPlayersCount());
if(value == null || value.isEmpty()){
builder.maxPlayersCount("");
break;
}
builder.maxPlayersCount(value);
newValues.add(value);
}
case PLAYERCOUNT_ONLINEPLAYERS_ENABLED -> {
if(!entry.onlinePlayersEnabled().isNotSet())
oldValues.add(String.valueOf(entry.onlinePlayersEnabled().getOrDefault(false)));
if(value == null || value.isEmpty()){
builder.onlinePlayersEnabled(NullBool.NOT_SET);
break;
}
NullBool bool = NullBool.resolve(Boolean.parseBoolean(value));
builder.onlinePlayersEnabled(bool);
newValues.add(String.valueOf(bool.getOrDefault(false)));
}
case PLAYERCOUNT_ONLINEPLAYERS_AMOUNT -> {
if(!entry.onlinePlayersCount().isEmpty())
oldValues.add(entry.onlinePlayersCount());
if(value == null || value.isEmpty()){
builder.onlinePlayersCount("");
break;
}
builder.onlinePlayersCount(value);
newValues.add(value);
}
case null, default -> {
sender.sendErrorMsg("<red>Unknown option </red>%s<red>!", option);
subCommand.handle(sender, Arrays.copyOfRange(args, 1, args.length));
return;
}
}
if(core.fileHandler().saveProfile(profileName(profile), priority, condition, slp.profiles(), builder.build())){
if(sender.isPlayer()){
sender.sendPrefixedMsg(
"<green>Successfully updated <white>[<grey><hover:show_text:\"%s\">%s</hover></grey>]</white>!",
changeValueHover(oldValues, newValues),
option
sender.sendErrorMsg("<red>Unknown subcommand <grey>%s</grey>. Use <grey>/asl help</grey> for a list of commands.", args[0]);
}
private static class Help extends PluginCommand{
private final AdvancedServerList<?> core;
public Help(AdvancedServerList<?> core){
super("help");
this.core = core;
}
@Override
public void handle(CmdSender sender, String[] args){
sender.sendPrefixedMsg("- Commands. Hover for details.");
for(PluginCommand command : core.getCommandHandler().getSubCommands()){
sender.sendMsg();
sender.sendMsg(
"<click:suggest_command:/asl %s>" +
command.usage() +
"</click>",
command.name()
);
}else{
sender.sendPrefixedMsg("<green>Successfully updated <white>[<grey>%s</grey>]</white>:", option);
if(oldValues.isEmpty()){
sender.sendMsg("<red>-</red> <white><i>Not set</i></white>");
}else{
oldValues.forEach(oldValue -> sender.sendMsg("<red>-</red> %s", oldValue));
}
sender.sendMsg();
if(newValues.isEmpty()){
sender.sendMsg("<green>+</green> <white><i>Reset to default</i></white>");
}else{
newValues.forEach(newValue -> sender.sendMsg("<green>+</green> %s", newValue));
}
sender.sendMsg();
sender.sendMsg("<grey>[<hover:show_text:\"%s\"><white>Permission</white></hover>]</grey>", command.permission());
}
sender.sendPrefixedMsg(
"<green>Use <white><click:suggest_command:/asl reload>/asl reload</click></white> to apply changes."
);
}else{
sender.sendErrorMsg("<red>Encountered an IOException while trying to save file</red> %s<red>.", profile);
sender.sendErrorMsg("<red>Check console for details.");
core.plugin().pluginLogger().warn("Encountered IOException while saving file <white>%s.yml</white>", profile);
}
@Override
public String usage(){
return new CommandStringBuilder("help", description())
.build();
}
@Override
public String description(){
return "Shows all available commands.";
}
}
public static ServerListProfile profile(AdvancedServerList<?> core, String name){
return core.fileHandler().profiles().stream()
.filter(p -> p.file().equalsIgnoreCase(profileName(name)))
.findFirst()
.orElse(null);
}
public static String profileName(String name){
if(name == null || name.isEmpty())
return "";
private static class Reload extends PluginCommand{
String filename = name.toLowerCase(Locale.ROOT);
private final AdvancedServerList<?> core;
return filename.endsWith(".yml") ? filename : filename + ".yml";
}
private static String changeValueHover(List<String> oldValues, List<String> newValues){
return """
<red>-</red> %s<reset>
public Reload(AdvancedServerList<?> core){
super("reload");
<green>+</green> %s""".formatted(
oldValues.isEmpty() ? "<i>Not set</i>" : String.join("<reset>\n<red>-</red> ", oldValues),
newValues.isEmpty() ? "<i>Not set</i>" : String.join("<reset>\n<green>+</green> ", newValues)
);
this.core = core;
}
@Override
public void handle(CmdSender sender, String[] args){
sender.sendPrefixedMsg("Reloading plugin...");
if(core.getFileHandler().reloadConfig()){
sender.sendPrefixedMsg("<green>Reloaded <grey>config.yml</grey>!");
}else{
sender.sendErrorMsg("<red>Error while reloading <grey>config.yml</grey>!");
}
if(core.getFileHandler().reloadProfiles()){
sender.sendPrefixedMsg("<green>Loaded <grey>%d profile(s)</grey>!", core.getFileHandler().getProfiles().size());
}else{
sender.sendErrorMsg("<red>Error while loading profile(s)!");
}
sender.sendPrefixedMsg("<green>Reload complete!");
}
@Override
public String usage(){
return new CommandStringBuilder("reload", description())
.build();
}
@Override
public String description(){
return "Reloads the main config and all profiles.";
}
}
private static String getOptionValue(Object obj){
return switch(obj){
case null -> "";
case Boolean bool -> Boolean.toString(bool);
case Integer integer -> Integer.toString(integer);
case String str -> str;
default -> obj.toString();
};
private static class ClearCache extends PluginCommand{
private final AdvancedServerList<?> core;
public ClearCache(AdvancedServerList<?> core){
super("clearcache");
this.core = core;
}
@Override
public void handle(CmdSender sender, String[] args){
sender.sendPrefixedMsg("Clearing caches...");
core.clearFaviconCache();
sender.sendPrefixedMsg("<green>Successfully cleared Favicon Cache!");
core.clearPlayerCache();
sender.sendPrefixedMsg("<green>Successfully cleared Player Cache!");
sender.sendPrefixedMsg("<green>Cache clearing complete!");
}
@Override
public String usage(){
return new CommandStringBuilder("clearcache", description())
.build();
}
@Override
public String description(){
return "Clears the Favicon and Player Cache.";
}
}
private static class Migrate extends PluginCommand{
private final AdvancedServerList<?> core;
public Migrate(AdvancedServerList<?> core){
super("migrate");
this.core = core;
}
@Override
public void handle(CmdSender sender, String[] args){
if(args.length == 0){
sender.sendErrorMsg("<red>Insufficient arguments! Please provide a plugin to migrate from.");
sender.sendErrorMsg("<red>Available options:");
sender.sendErrorMsg(" - ServerListPlus");
sender.sendErrorMsg(" - MiniMOTD");
return;
}
if(args[0].equalsIgnoreCase("serverlistplus")){
if(!core.getPlugin().isPluginEnabled("ServerListPlus")){
sender.sendErrorMsg("<red>Plugin ServerListPlus is not enabled.");
sender.sendErrorMsg("<red>It needs to be active for the migration to work!");
return;
}
sender.sendPrefixedMsg("Migrating ServerListPlus configuration file...");
int migrated = SLPConfigMigrator.migrate(core, sender);
if(migrated == 0){
sender.sendErrorMsg("<red>Unable to migrate any ServerListPlus configuration. Check console for details!");
}else{
sender.sendPrefixedMsg("<green>Migration complete!");
}
}else
if(args[0].equalsIgnoreCase("minimotd")){
sender.sendPrefixedMsg("Migrating MiniMOTD configuration file...");
if(MiniMOTDConfigMigrator.migrate(core, sender)){
sender.sendPrefixedMsg("<green>Migration complete!");
}else{
sender.sendErrorMsg("<red>Unable to migrate MiniMOTD configuration. Check console for details!");
}
}else{
sender.sendErrorMsg("<red>Unknown plugin <grey>%s</grey>. Available Options:", args[0]);
sender.sendErrorMsg(" - ServerListPlus");
sender.sendErrorMsg(" - MiniMOTD");
}
}
@Override
public String usage(){
return new CommandStringBuilder("migrate", description())
.appendSpace()
.appendArgument(new CommandStringBuilder.ArgumentBuilder("Plugin").description(
"""
The Plugin to migrate from.
Supported options:
- <white>ServerListPlus</white>
- <white>MiniMOTD</white>
"""
).build())
.build();
}
@Override
public String description(){
return "Migrates the configuration from supported Plugins to AdvancedServerList.";
}
}
private static class Profiles extends PluginCommand{
private final AdvancedServerList<?> core;
public Profiles(AdvancedServerList<?> core){
super("profiles");
this.core = core;
}
@Override
public void handle(CmdSender sender, String[] args){
if(args.length == 0){
sender.sendErrorMsg("<red>Invalid command usage.");
sender.sendErrorMsg("<red>Usage:</red> %s", usage());
return;
}
switch(args[0].toLowerCase(Locale.ROOT)){
case "list" -> {
sender.sendPrefixedMsg("Available Profiles:");
core.getFileHandler().getProfiles().stream()
.sorted(Comparator.comparingInt(ServerListProfile::priority).reversed())
.forEach(
profile -> sender.sendPrefixedMsg("- <white><hover:show_text:\"%s\">%s</hover></white>", getHover(profile), profile.file())
);
}
case "add" -> {
if(args.length == 1){
sender.sendErrorMsg("<red>Insufficient arguments.");
sender.sendErrorMsg("<red>Usage:</red> /asl profiles add <name>");
return;
}
String name = getProfileName(args[1]);
if(core.getFileHandler().getProfiles().stream().anyMatch(profile -> profile.file().equalsIgnoreCase(name))){
sender.sendErrorMsg("<red>A profile with file name</red> %s <red>already exists!", name);
return;
}
if(core.getFileHandler().createFile(name)){
sender.sendPrefixedMsg("<green>Successfully created</green> %s<green>!", name);
sender.sendPrefixedMsg("<green>Load the file using</green> /asl reload");
}else{
sender.sendErrorMsg("<red>Error while trying to create</red> %s<red>!", name);
sender.sendErrorMsg("<red>Check console for further details.");
}
}
case "copy" -> {
if(args.length < 3){
sender.sendErrorMsg("<red>Insufficient arguments.");
sender.sendErrorMsg("<red>Usage:</red> /asl profiles copy <profile> <name>");
return;
}
String name = getProfileName(args[1]);
String copy = getProfileName(args[2]);
ServerListProfile targetProfile = core.getFileHandler().getProfiles().stream()
.filter(file -> file.file().equalsIgnoreCase(name))
.findFirst()
.orElse(null);
boolean copyExists = core.getFileHandler().getProfiles().stream()
.anyMatch(profile -> profile.file().equals(copy));
if(targetProfile == null){
sender.sendErrorMsg("<red>There is no file with name</red> %s<red>!", name);
return;
}
if(copyExists){
sender.sendErrorMsg("<red>A profile with file name</red> %s <red>already exists!", copy);
return;
}
Path profilePath = core.getPlugin().getFolderPath().resolve("profiles").resolve(copy);
try{
Files.createFile(profilePath);
}catch(IOException ex){
sender.sendErrorMsg("<red>There was an error while creating the file</red> %s<red>!", copy);
sender.sendErrorMsg("<red>Check console for further details.");
core.getPlugin().getPluginLogger().warn("Encountered IOException while creating file <white>%s</white>", ex, copy);
return;
}
YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
.path(profilePath)
.indent(2)
.nodeStyle(NodeStyle.BLOCK)
.defaultOptions(options -> options.serializers(builder -> builder.register(ProfileEntry.class, ProfileSerializer.INSTANCE)))
.build();
ConfigurationNode node;
try{
node = loader.load();
}catch(IOException ex){
sender.sendErrorMsg("<red>There was an error while loading the file</red> %s<red>!", copy);
sender.sendErrorMsg("<red>Check console for further details.");
core.getPlugin().getPluginLogger().warn("Encountered IOException while loading file <white>%s</white>", ex, copy);
return;
}
if(node == null){
sender.sendErrorMsg("Cannot copy</red> %s <red>to</red> %s<red>. ConfigurationNode was null.", name, copy);
return;
}
try{
node.node("priority").set(targetProfile.priority());
node.node("condition").set(targetProfile.condition());
node.node("profiles").setList(ProfileEntry.class, targetProfile.profiles());
node.set(ProfileEntry.class, targetProfile.defaultProfile());
}catch(SerializationException ex){
sender.sendErrorMsg("<red>There was an Error while copying values from</red> %s <red>to</red> %s<red>.", name, copy);
sender.sendErrorMsg("<red>Check console for details.");
core.getPlugin().getPluginLogger().warn("Encountered SerializationException while set values for <white>%s</white>!", ex, copy);
return;
}
try{
loader.save(node);
sender.sendPrefixedMsg("<green>Successfully created copy of</green> %s <green>as</green> %s<green>!", name, copy);
sender.sendPrefixedMsg("<green>Load the file using</green> /asl reload");
}catch(IOException ex){
sender.sendErrorMsg("<red>There was an Error while trying to save</red> %s<red>!", copy);
sender.sendErrorMsg("<red>Check console for details.");
core.getPlugin().getPluginLogger().warn("Encountered IOException while saving ConfigurationNode for <white>%s</white>!", ex, copy);
}
}
default -> {
sender.sendErrorMsg("<red>Unknown argument</red> %s<red>!", args[0]);
sender.sendErrorMsg("<red>Usage:</red> %s", usage());
}
}
}
@Override
public String usage(){
return new CommandStringBuilder("profiles", "Creates a new profile, copies one from an existing file, or lists all available profiles.")
.appendSpace()
.appendText("<grey>\\<")
// add <name>
.appendLiteralArgument("add", "Adds a new profile using the default values.")
.appendSpace()
.appendArgument(new CommandStringBuilder.ArgumentBuilder("name").description("Name of the new profile").required().build())
// Divider
.appendSpace()
.appendText("|")
.appendSpace()
// copy <profile> <name>
.appendLiteralArgument("copy", "Creates a copy of the provided profile.")
.appendSpace()
.appendArgument(new CommandStringBuilder.ArgumentBuilder("profile").description("The profile to copy.").required().build())
.appendSpace()
.appendArgument(new CommandStringBuilder.ArgumentBuilder("name").description("Name of the copy.").required().build())
// Divider
.appendSpace()
.appendText("|")
.appendSpace()
// list
.appendLiteralArgument("list", "Lists all currently know profiles and whether they are valid or not.")
// Closing bracket
.appendText("></grey>")
.build();
}
@Override
public String description(){
return "Creates a new profile, copies one from an existing file, or lists all available profiles.";
}
private String getHover(ServerListProfile profile){
return String.format(
"""
<grey>Priority: <white>%s</white>
Condition: <white>%s</white>
Valid profile: %s
""",
profile.priority(),
profile.condition() == null || profile.condition().isEmpty() ? "<i>None</i>" : profile.condition(),
profile.isInvalidProfile() ? "<red>No</red>" : "<green>Yes</green>"
);
}
private String getProfileName(String name){
String fileName = name.toLowerCase(Locale.ROOT);
return fileName.endsWith(".yml") ? fileName : fileName + ".yml";
}
}
}
@@ -0,0 +1,110 @@
/*
* MIT License
*
* Copyright (c) 2022-2025 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package ch.andre601.advancedserverlist.core.commands;
import java.util.ArrayList;
import java.util.List;
public class CommandStringBuilder{
private final List<CommandPart> commandParts = new ArrayList<>();
public CommandStringBuilder(String subcommand, String description){
appendLiteralArgument(subcommand, description);
}
public CommandStringBuilder appendArgument(Argument argument){
this.commandParts.add(argument);
return this;
}
public CommandStringBuilder appendText(String text){
this.commandParts.add(new Text(text));
return this;
}
public CommandStringBuilder appendLiteralArgument(String name, String description){
return appendText("<hover:show_text:\"<grey>" + description + "</grey>\"><white>" + name + "</white></hover>");
}
public CommandStringBuilder appendSpace(){
return appendText(" ");
}
public String build(){
StringBuilder builder = new StringBuilder("<aqua>/asl ");
for(CommandPart part : commandParts){
builder.append(part.build());
}
builder.append("</aqua>"); // Closing the first color tag.
return builder.toString();
}
public static class ArgumentBuilder{
private final String name;
private String description = "No description provided.";
private boolean required = false;
public ArgumentBuilder(String name){
this.name = name;
}
public ArgumentBuilder description(String description){
this.description = description;
return this;
}
public ArgumentBuilder required(){
this.required = true;
return this;
}
public Argument build(){
return new Argument(name, description, required);
}
}
private record Text(String text) implements CommandPart{
@Override
public String build(){
return text();
}
}
private record Argument(String name, String description, boolean required) implements CommandPart{
@Override
public String build(){
return "<hover:show_text:\"<grey>" + description() + "</grey>\">" +
"<grey>" + (required() ? "\\<" : "[") + "<white>" + name() + "</white>" + (required() ? ">" : "]") +
"</grey></hover>";
}
}
private interface CommandPart{
String build();
}
}
@@ -1,22 +0,0 @@
package ch.andre601.advancedserverlist.core.commands;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.interfaces.commands.CommandType;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.execution.postprocessor.CommandPostprocessingContext;
import org.incendo.cloud.execution.postprocessor.CommandPostprocessor;
import org.incendo.cloud.services.type.ConsumerService;
public class CommandTypePostprocessor implements CommandPostprocessor<CmdSender>{
@Override
public void accept(@NonNull CommandPostprocessingContext<CmdSender> context){
CmdSender sender = context.commandContext().sender();
CommandType.Type type = context.commandContext().command().commandMeta().get(CommandHandler.COMMAND_TYPE);
if(type == CommandType.Type.ALL || (type == CommandType.Type.PLAYER_ONLY && sender.isPlayer()))
return;
sender.sendErrorMsg("<red>This command can only be executed by players!");
ConsumerService.interrupt();
}
}
@@ -1,172 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022-2025 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package ch.andre601.advancedserverlist.core.commands;
import ch.andre601.advancedserverlist.api.objects.NullBool;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class ProfileOptionsSender{
private final List<OptionStringBuilder> builders = new ArrayList<>();
private final String file;
public ProfileOptionsSender(String file){
this.file = file;
}
public ProfileOptionsSender append(OptionStringBuilder builder){
this.builders.add(builder.file(file));
return this;
}
public void send(CmdSender sender){
sender.sendMsg();
sender.sendPrefixedMsg("Profile Info [<white>%s</white>]", file);
for(OptionStringBuilder builder : builders){
sender.sendMsg(builder.build());
}
sender.sendMsg();
sender.sendMsg("[<grey>Hover for info, click for command</grey>]");
sender.sendMsg();
}
public static class OptionStringBuilder{
public static OptionStringBuilder of(String displayName, String hover){
return new OptionStringBuilder(displayName)
.hover(hover == null || hover.isEmpty() ? "<i>Not set</i>" : hover)
.color(hover == null || hover.isEmpty() ? ColorName.RED : ColorName.GREEN);
}
public static OptionStringBuilder of(String displayName, List<String> hover){
return new OptionStringBuilder(displayName)
.hover(hover == null || hover.isEmpty() ? "<i>Not set</i>" : String.join("\n<reset>", hover))
.color(hover == null || hover.isEmpty() ? ColorName.RED : ColorName.GREEN);
}
public static OptionStringBuilder of(String displayName, NullBool hover){
return new OptionStringBuilder(displayName)
.hover(hover == null || !hover.getOrDefault(false) ? "<red>Disabled</red>" : "<green>Enabled</green>")
.color(hover == null || !hover.getOrDefault(false) ? ColorName.RED : ColorName.GREEN);
}
public static OptionStringBuilder of(String displayName, int hover){
return new OptionStringBuilder(displayName)
.hover(String.valueOf(hover));
}
public static OptionStringBuilder of(String displayName){
return new OptionStringBuilder(displayName).hover(null);
}
private final String displayName;
private String file = null;
private String prefix = "├─";
private ColorName color = ColorName.AQUA;
private String hover = "<i>Not set</i>";
private CmdProfiles.ProfileOption option = null;
private OptionStringBuilder(String displayName){
this.displayName = displayName;
}
public OptionStringBuilder file(String file){
this.file = file;
return this;
}
public OptionStringBuilder prefix(String prefix){
this.prefix = prefix;
return this;
}
public OptionStringBuilder color(ColorName color){
this.color = color;
return this;
}
public OptionStringBuilder hover(String hover){
this.hover = hover;
return this;
}
public OptionStringBuilder option(CmdProfiles.ProfileOption option){
this.option = option;
return this;
}
public String build(){
StringBuilder builder = new StringBuilder("<grey>")
.append(prefix)
.append(" <white>[")
.append('<')
.append(color)
.append('>');
if(hover != null && !hover.isEmpty()){
builder.append("<hover:show_text:\"")
.append(hover)
.append("\">");
}
if(option != null && file != null && !file.isEmpty()){
builder.append("<click:suggest_command:/asl profiles set ")
.append(file)
.append(" ")
.append(option)
.append(" >");
}
builder.append(displayName);
if(option != null && file != null && !file.isEmpty())
builder.append("</click>");
if(hover != null && !hover.isEmpty())
builder.append("</hover>");
builder.append("</")
.append(color)
.append(">]</white></grey>");
return builder.toString();
}
}
public enum ColorName{
RED,
GREEN,
AQUA;
@Override
public String toString(){
return this.name().toLowerCase(Locale.ROOT);
}
}
}
@@ -4,10 +4,7 @@ import ch.andre601.advancedserverlist.api.PlaceholderProvider;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class MaintenancePlaceholder extends PlaceholderProvider{
private final AdvancedServerList<?> plugin;
@@ -18,8 +15,8 @@ public class MaintenancePlaceholder extends PlaceholderProvider{
this.plugin = plugin;
}
@Override
public @Nullable String parsePlaceholder(String placeholder, GenericPlayer player, GenericServer server){
if(!placeholder.equals("maintenanceEnabled") || !plugin.plugin().isPluginEnabled("Maintenance"))
public String parsePlaceholder(String placeholder, GenericPlayer player, GenericServer server){
if(!placeholder.equals("maintenanceEnabled") || !plugin.getPlugin().isPluginEnabled("Maintenance"))
return null;
return String.valueOf(MaintenanceUtil.get().isMaintenanceEnabled());
@@ -1,11 +0,0 @@
package ch.andre601.advancedserverlist.core.compat.miniplaceholders;
import io.github.miniplaceholders.api.MiniPlaceholders;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
public class MiniPlaceholdersUtil{
public static TagResolver tagResolvers(){
return MiniPlaceholders.globalPlaceholders();
}
}
@@ -1,94 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.compat.papi;
import ch.andre601.advancedserverlist.core.objects.ValueCache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.william278.papiproxybridge.api.PlaceholderAPI;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class PAPIUtil{
private static PAPIUtil instance;
private final ValueCache<CompletableFuture<Set<String>>> serversCache = new ValueCache<>(Duration.ofSeconds(10));
private final Cache<CacheKey, CompletableFuture<String>> textCache = CacheBuilder.newBuilder()
.expireAfterWrite(Duration.ofSeconds(10))
.build();
private final PlaceholderAPI papi;
private PAPIUtil(){
this.papi = PlaceholderAPI.createInstance();
}
public static PAPIUtil get(){
if(instance != null)
return instance;
return (instance = new PAPIUtil());
}
// Make sure the version of PAPIProxyBridge we get has the required methods we need...
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isCompatible(){
try{
papi.getClass().getMethod("getServers");
return true;
}catch(NoSuchMethodException ex){
return false;
}
}
public String getServer(){
Set<String> values = serversCache.get(papi::getServers).getNow(Collections.emptySet());
if(values.isEmpty())
return null;
return List.copyOf(values).getFirst();
}
public <P> P getPlayer(Collection<P> players){
if(players.isEmpty())
return null;
return List.copyOf(players).getFirst();
}
public String parse(String text, UUID carrier, UUID player){
try{
return textCache.get(new CacheKey(player, text), () -> papi.formatPlaceholders(text, carrier, player)).getNow(text);
}catch(ExecutionException ex){
return text;
}
}
private record CacheKey(UUID uuid, String text){}
}
@@ -44,28 +44,28 @@ import java.util.List;
public class PingEventHandler{
public static <F, P extends GenericPlayer> ProfileEntry handleEvent(GenericEventWrapper<F, P> event){
event.plugin().pluginLogger().debug(PingEventHandler.class, "Received ping event. Handling it...");
event.getPlugin().getPluginLogger().debug(PingEventHandler.class, "Received ping event. Handling it...");
if(event.isInvalidProtocol()){
event.plugin().pluginLogger().debug(PingEventHandler.class, "Not handling event. Protocol was invalid.");
event.getPlugin().getPluginLogger().debug(PingEventHandler.class, "Not handling event. Protocol was invalid.");
return null;
}
PluginCore<F> plugin = event.plugin();
String host = event.virtualHost();
PluginLogger logger = plugin.pluginLogger();
PluginCore<F> plugin = event.getPlugin();
String host = event.getVirtualHost();
PluginLogger logger = plugin.getPluginLogger();
logger.debug(PingEventHandler.class, "Protocol valid. Continue handling...");
int online = event.onlinePlayers();
int max = event.maxPlayers();
int online = event.getOnlinePlayers();
int max = event.getMaxPlayers();
P player = event.createPlayer(
plugin.core().playerHandler().getCachedPlayer(event.playerIP()),
event.protocolVersion()
plugin.getCore().getPlayerHandler().getCachedPlayer(event.getPlayerIP()),
event.getProtocolVersion()
);
GenericServer server = event.createServer(online, max, host);
GenericServer server = event.createGenericServer(online, max, host);
ServerListProfile profile = ProfileManager.resolveProfile(plugin.core(), player, server);
ServerListProfile profile = ProfileManager.resolveProfile(plugin.getCore(), player, server);
if(profile == null){
logger.debugWarn(PingEventHandler.class, "Server List Profile couldn't be resolved properly. Cancelling event handling...");
@@ -93,12 +93,11 @@ public class PingEventHandler{
String onlinePlayers = ComponentParser.text(entry.onlinePlayersCount())
.modifyText(text -> StringReplacer.replace(text, player, server))
.modifyText(text -> event.parsePAPIPlaceholders(text, player))
.applyMMTags(event.plugin())
.toString();
try{
online = onlinePlayers == null ? online : Integer.parseInt(onlinePlayers);
event.onlinePlayers(online);
event.setOnlinePlayers(online);
}catch(NumberFormatException ex){
logger.warn("Option 'playerCount -> onlinePlayers -> amount' is not a valid Number!");
}
@@ -111,14 +110,13 @@ public class PingEventHandler{
String extraPlayers = ComponentParser.text(entry.extraPlayersCount())
.modifyText(text -> StringReplacer.replace(text, player, server))
.modifyText(text -> event.parsePAPIPlaceholders(text, player))
.applyMMTags(event.plugin())
.toString();
try{
max = online + (extraPlayers == null ? 0 : Integer.parseInt(extraPlayers));
logger.debug(PingEventHandler.class, "'playerCount -> extraPlayers -> amount' is a valid number. Applying it...");
event.maxPlayers(max);
event.setMaxPlayers(max);
}catch(NumberFormatException ex){
logger.warn("Option 'playerCount -> extraPlayers -> amount' is not a valid Number!");
}
@@ -129,20 +127,19 @@ public class PingEventHandler{
String maxPlayers = ComponentParser.text(entry.maxPlayersCount())
.modifyText(text -> StringReplacer.replace(text, player, server))
.modifyText(text -> event.parsePAPIPlaceholders(text, player))
.applyMMTags(event.plugin())
.toString();
try{
max = maxPlayers == null ? 0 : Integer.parseInt(maxPlayers);
logger.debug(PingEventHandler.class, "'playerCount -> maxPlayers -> amount' is a valid number. Applying it...");
event.maxPlayers(max);
event.setMaxPlayers(max);
}catch(NumberFormatException ex){
logger.warn("Option 'playerCount -> maxPlayers -> amount' is not a valid Number!");
}
}
GenericServer finalServer = event.createServer(online, max, host);
GenericServer finalServer = event.createGenericServer(online, max, host);
if(ProfileManager.checkOption(entry.motd()) && ignoreMaintenance(event, "motd")){
logger.debug(PingEventHandler.class, "'motd' option present. Applying ['%s']...", String.join("', '", entry.motd()));
@@ -151,13 +148,12 @@ public class PingEventHandler{
.map(line -> ComponentParser.text(line)
.modifyText(text -> StringReplacer.replace(text, player, finalServer))
.modifyText(text -> event.parsePAPIPlaceholders(text, player))
.modifyText(text -> plugin.core().textCenterUtil().getCenteredText(text))
.applyMMTags(event.plugin())
.modifyText(text -> plugin.getCore().getTextCenterUtil().getCenteredText(text))
.toComponent()
)
.toList();
event.motd(Component.join(JoinConfiguration.separator(Component.newline()), motd));
event.setMotd(Component.join(JoinConfiguration.separator(Component.newline()), motd));
}
boolean hidePlayers = ProfileManager.checkOption(entry.hidePlayersEnabled());
@@ -171,11 +167,10 @@ public class PingEventHandler{
if(ProfileManager.checkOption(entry.playerCountText()) && !hidePlayers && ignoreMaintenance(event, "playerCountText")){
logger.debug(PingEventHandler.class, "'playerCount -> text' option set. Applying '%s'...", entry.playerCountText());
event.playerCount(
event.setPlayerCount(
ComponentParser.text(entry.playerCountText())
.modifyText(text -> StringReplacer.replace(text, player, finalServer))
.modifyText(text -> event.parsePAPIPlaceholders(text, player))
.applyMMTags(event.plugin())
.toString()
);
}
@@ -183,33 +178,30 @@ public class PingEventHandler{
boolean hidePlayersHover = ProfileManager.checkOption(entry.hidePlayersHoverEnabled());
if(hidePlayersHover){
logger.debug(PingEventHandler.class, "'playerCount -> hidePlayersHover' option set. Hiding Player Hover...");
event.playersHidden();
event.setPlayersHidden();
}
if(ProfileManager.checkOption(entry.players()) && !hidePlayers && !hidePlayersHover && ignoreMaintenance(event, "playerCountHover")){
logger.debug(PingEventHandler.class, "'playerCount -> hover' option set. Applying ['%s']...", String.join("', '", entry.players()));
event.players(entry.players(), player, server);
event.setPlayers(entry.players(), player, server);
}
if(ProfileManager.checkOption(entry.favicon()) && ignoreMaintenance(event, "favicon")){
logger.debug(PingEventHandler.class, "'favicon' option set. Resolving '%s'...", entry.favicon());
String faviconString = StringReplacer.modifier(entry.favicon())
.modify(text -> StringReplacer.replace(text, player, server))
.modify(text -> event.parsePAPIPlaceholders(text, player))
.asString();
String faviconString = StringReplacer.replace(entry.favicon(), player, server);
F favicon = plugin.faviconHandler().favicon(faviconString);
F favicon = plugin.getFaviconHandler().getFavicon(faviconString);
if(favicon == null){
logger.debugWarn(PingEventHandler.class, "Favicon was invalid or not yet resolved! Using default favicon of Server/Proxy...");
event.defaultFavicon();
event.setDefaultFavicon();
}else{
logger.debug(PingEventHandler.class, "Applying favicon...");
event.favicon(favicon);
event.setFavicon(favicon);
}
}
@@ -223,6 +215,6 @@ public class PingEventHandler{
if(!event.isMaintenanceModeActive())
return true;
return !event.plugin().core().fileHandler().getBool(true, "disableDuringMaintenance", option);
return !event.getPlugin().getCore().getFileHandler().getBool(true, "disableDuringMaintenance", option);
}
}
@@ -46,9 +46,7 @@ public class ConfigMigrator{
public static ConfigurationTransformation.Versioned create(){
return ConfigurationTransformation.versionedBuilder()
.versionKey("configVersion")
.addVersion(LATEST, nineToTen())
.addVersion(9, eightToNine())
.addVersion(8, sevenToEight())
.addVersion(LATEST, sevenToEight())
.addVersion(7, sixToSeven())
.addVersion(6, fiveToSix())
.addVersion(5, fourToFive())
@@ -58,22 +56,6 @@ public class ConfigMigrator{
.build();
}
public static ConfigurationTransformation nineToTen(){
return ConfigurationTransformation.builder()
.addAction(NodePath.path("faviconStrategy"), TransformAction.rename("faviconResizeStrategy"))
.build();
}
public static ConfigurationTransformation eightToNine(){
return ConfigurationTransformation.builder()
.addAction(NodePath.path(), (path, value) -> {
value.node("faviconStrategy").set("resize");
return null;
})
.build();
}
public static ConfigurationTransformation sevenToEight(){
return ConfigurationTransformation.builder()
.addAction(NodePath.path("disableDuringMaintenance"), (path, value) -> {
@@ -32,7 +32,6 @@ import ch.andre601.advancedserverlist.core.profiles.ServerListProfile;
import ch.andre601.advancedserverlist.core.profiles.profile.ProfileSerializer;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.yaml.NodeStyle;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
@@ -47,7 +46,6 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
public class FileHandler{
@@ -63,27 +61,18 @@ public class FileHandler{
public FileHandler(AdvancedServerList<?> core){
this.plugin = core;
this.logger = core.plugin().pluginLogger();
this.logger = core.getPlugin().getPluginLogger();
this.config = core.plugin().folderPath().resolve("config.yml");
this.profilesFolder = core.plugin().folderPath().resolve("profiles");
this.config = core.getPlugin().getFolderPath().resolve("config.yml");
this.profilesFolder = core.getPlugin().getFolderPath().resolve("profiles");
}
public List<ServerListProfile> profiles(){
public List<ServerListProfile> getProfiles(){
return profiles;
}
public ServerListProfile profile(String file){
for(ServerListProfile profile : profiles){
if(profile.file().equalsIgnoreCase(file))
return profile;
}
return null;
}
public boolean loadConfig(){
File folder = plugin.plugin().folderPath().toFile();
File folder = plugin.getPlugin().getFolderPath().toFile();
if(!folder.exists() && !folder.mkdirs()){
logger.warn("Couldn't create folder for plugin. Is it missing Write permissions?");
return false;
@@ -117,48 +106,6 @@ public class FileHandler{
}
}
public boolean saveProfile(String file, int priority, String condition, List<ProfileEntry> profiles, ProfileEntry defaultProfile){
String filename = file.toLowerCase(Locale.ROOT);
Path filePath = profilesFolder.resolve(filename.endsWith(".yml") ? filename : filename + ".yml");
if(!Files.exists(filePath)){
logger.warn("Cannot find profile with file name '<white>%s</white>'", filename);
return false;
}
YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
.defaultOptions(opts -> opts.serializers(build -> build.register(ProfileEntry.class, ProfileSerializer.INSTANCE)))
.path(filePath)
.nodeStyle(NodeStyle.BLOCK)
.build();
ConfigurationNode node;
try{
node = loader.load();
}catch(IOException ex){
logger.warn("Encountered an IOException while loading file '<white>%s</white>'", ex, filename);
return false;
}
if(node == null)
return false;
try{
node.node("priority").set(priority);
node.node("condition").set(condition);
node.node("profiles").setList(ProfileEntry.class, profiles);
node.set(ProfileEntry.class, defaultProfile);
loader.save(node);
return true;
}catch(SerializationException ex){
logger.warn("Encountered a SerializationException while saving file '<white>%s</white>'", ex, filename);
return false;
}catch(IOException ex){
logger.warn("Encountered an IOException while saving file '<white>%s</white>'", ex, filename);
return false;
}
}
public boolean createFile(String name){
if(Files.notExists(profilesFolder) && profilesFolder.toFile().mkdirs()){
logger.debugSuccess(FileHandler.class, "Successfully created profiles folder.");
@@ -272,7 +219,7 @@ public class FileHandler{
private boolean makeBackup(){
logger.info("[-] Making backup of old config.yml...");
File backups = plugin.plugin().folderPath().resolve("backups").toFile();
File backups = plugin.getPlugin().getFolderPath().resolve("backups").toFile();
if(!backups.exists() && !backups.mkdirs()){
logger.failure("Cannot create backups folder for migration!");
return false;
@@ -25,12 +25,12 @@
package ch.andre601.advancedserverlist.core.interfaces.commands;
import net.kyori.adventure.audience.Audience;
public interface CmdSender{
String prefix = "<grey>[<aqua>ASL</aqua>] ";
String errorPrefix = "<grey>[<red>ASL</red>] ";
String prefix = "<grey>[<gradient:aqua:white>AdvancedServerList</gradient>] ";
String errorPrefix = "<grey>[<gradient:dark_red:red>AdvancedServerList</gradient>] ";
boolean hasPermission(String permission);
void sendMsg(String msg, Object... args);
@@ -45,8 +45,4 @@ public interface CmdSender{
default void sendErrorMsg(String msg, Object... args){
sendMsg(errorPrefix + msg, args);
}
Audience audience();
boolean isPlayer();
}
@@ -1,7 +1,7 @@
/*
* MIT License
*
* Copyright (c) 2022-2025 Andre_601
* Copyright (c) 2022-2023 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -20,22 +20,32 @@
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.interfaces.commands;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Locale;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface CommandType{
Type value();
public abstract class PluginCommand{
enum Type{
ALL,
PLAYER_ONLY
private final String name;
public PluginCommand(String name){
this.name = name;
}
public String name(){
return name;
}
public String permission(){
return "advancedserverlist.command." + name().toLowerCase(Locale.ROOT);
}
public abstract void handle(CmdSender sender, String[] args);
public abstract String usage();
public abstract String description();
}
@@ -26,19 +26,16 @@
package ch.andre601.advancedserverlist.core.interfaces.core;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.dialogs.IDialog;
import ch.andre601.advancedserverlist.core.interfaces.dialogs.IDialogList;
import ch.andre601.advancedserverlist.core.interfaces.PluginLogger;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
import ch.andre601.advancedserverlist.core.profiles.ServerListProfile;
import ch.andre601.advancedserverlist.core.profiles.handlers.FaviconHandler;
import org.incendo.cloud.CommandManager;
import java.awt.image.BufferedImage;
import java.nio.file.Path;
public interface PluginCore<F>{
void loadCommands();
void loadEvents();
void loadMetrics();
@@ -51,34 +48,26 @@ public interface PluginCore<F>{
void startScheduler();
AdvancedServerList<F> core();
AdvancedServerList<F> getCore();
Path folderPath();
Path getFolderPath();
PluginLogger pluginLogger();
PluginLogger getPluginLogger();
FaviconHandler<F> faviconHandler();
FaviconHandler<F> getFaviconHandler();
CommandManager<CmdSender> commandManager();
String getPlatformInfo();
String platformInfo();
String loader();
String getLoader();
boolean isPluginEnabled(String plugin);
boolean isDialogsSupported();
F createFavicon(BufferedImage image) throws Exception;
IDialogList.IDialogListBuilder createDialogListBuilder(String title);
IDialog.IDialogBuilder createDialogBuilder(String title, ServerListProfile profile, int profilesPosition);
default boolean isDebugEnabled(){
if(core() == null)
if(getCore() == null)
return false;
return core().fileHandler().getBoolean("debug");
return getCore().getFileHandler().getBoolean("debug");
}
}
@@ -1,24 +0,0 @@
package ch.andre601.advancedserverlist.core.interfaces.dialogs;
import ch.andre601.advancedserverlist.api.objects.NullBool;
import ch.andre601.advancedserverlist.core.commands.CmdProfiles;
import java.util.List;
public interface IDialog<T>{
T createDialog();
interface IDialogBuilder{
IDialogBuilder appendBody(String body);
IDialogBuilder appendTextInput(CmdProfiles.ProfileOption option, String title, String initValue, int maxLength);
IDialogBuilder appendTextbox(CmdProfiles.ProfileOption option, String title, List<String> initValue, int maxLength, int rows);
IDialogBuilder appendNumberRange(CmdProfiles.ProfileOption option, String title, int initValue);
IDialogBuilder appendBoolean(CmdProfiles.ProfileOption option, String title, NullBool initValue);
IDialog<?> build();
}
}
@@ -1,16 +0,0 @@
package ch.andre601.advancedserverlist.core.interfaces.dialogs;
import ch.andre601.advancedserverlist.core.interfaces.commands.CmdSender;
public interface IDialogList{
void show(CmdSender sender);
interface IDialogListBuilder{
IDialogListBuilder appendBody(String body);
IDialogListBuilder appendDialog(IDialog<?> dialog);
IDialogList build();
}
}
@@ -40,23 +40,23 @@ public interface GenericEventWrapper<F, P extends GenericPlayer>{
GenericServerListEvent callEvent(ProfileEntry entry);
void maxPlayers(int maxPlayers);
void setMaxPlayers(int maxPlayers);
void onlinePlayers(int onlinePlayers);
void setOnlinePlayers(int onlinePlayers);
void motd(Component component);
void setMotd(Component component);
void hidePlayers();
void playerCount(String name);
void setPlayerCount(String name);
void players(List<String> players, P player, GenericServer server);
void setPlayers(List<String> players, P player, GenericServer server);
void playersHidden();
void setPlayersHidden();
void favicon(F favicon);
void setFavicon(F favicon);
void defaultFavicon();
void setDefaultFavicon();
void updateEvent();
@@ -64,23 +64,23 @@ public interface GenericEventWrapper<F, P extends GenericPlayer>{
boolean isMaintenanceModeActive();
int protocolVersion();
int getProtocolVersion();
int onlinePlayers();
int getOnlinePlayers();
int maxPlayers();
int getMaxPlayers();
String playerIP();
String getPlayerIP();
String parsePAPIPlaceholders(String text, P player);
String virtualHost();
String getVirtualHost();
PluginCore<F> plugin();
PluginCore<F> getPlugin();
P createPlayer(CachedPlayer player, int protocol);
GenericServer createServer(int playersOnline, int playersMax, String host);
GenericServer createGenericServer(int playersOnline, int playersMax, String host);
default String resolveHost(InetSocketAddress address){
return address == null ? null : address.getHostString();
@@ -49,14 +49,12 @@ import java.util.List;
public class MiniMOTDConfigMigrator{
public static boolean migrate(AdvancedServerList<?> core, CmdSender sender){
sender.sendMsg("Loading Configurate-Hocon library. This may take a while...");
core.plugin().downloadLibrary("org.spongepowered", "configurate-hocon", "4.1.2");
sender.sendMsg("<green>Library loaded!");
core.getPlugin().downloadLibrary("org.spongepowered", "configurate-hocon", "4.1.2");
PluginLogger logger = core.plugin().pluginLogger();
String folder = core.plugin().loader().equals("velocity") ? "minimotd-velocity" : "MiniMOTD";
PluginLogger logger = core.getPlugin().getPluginLogger();
String folder = core.getPlugin().getLoader().equals("velocity") ? "minimotd-velocity" : "MiniMOTD";
Path mainConf = core.plugin().folderPath().getParent().resolve(folder).resolve("main.conf");
Path mainConf = core.getPlugin().getFolderPath().getParent().resolve(folder).resolve("main.conf");
if(!Files.exists(mainConf)){
logger.warn("[<white>Migrator - MiniMOTD</white>] Cannot find a main.conf file in <white>/plugins/%s/</white> folder.", folder);
sender.sendErrorMsg(" -> <red>No main.conf file found.");
@@ -100,7 +98,7 @@ public class MiniMOTDConfigMigrator{
if(config.motdEnabled()){
if(config.motds().size() == 1){
applyMotd(profileBuilder, config.motds().getFirst());
applyMotd(profileBuilder, config.motds().get(0));
}else
if(config.motds().size() > 1){
for(int i = 0; i < config.motds().size(); i++){
@@ -111,7 +109,7 @@ public class MiniMOTDConfigMigrator{
if(config.iconEnabled()){
if(config.motds().size() == 1){
MiniMOTDConfig.Motd motd = config.motds().getFirst();
MiniMOTDConfig.Motd motd = config.motds().get(0);
if(!motd.icon().isEmpty()){
profileBuilder.favicon(motd.icon());
}
@@ -154,7 +152,7 @@ public class MiniMOTDConfigMigrator{
return false;
}
Path profile = core.plugin().folderPath().resolve("profiles").resolve("minimotd_migrated.yml");
Path profile = core.getPlugin().getFolderPath().resolve("profiles").resolve("minimotd_migrated.yml");
if(Files.exists(profile)){
logger.warn("[<white>Migrator - MiniMOTD</white>] Cannot create new file minimotd_migrated.yml. One with the same name is already present.");
sender.sendErrorMsg(" -> <red>File</red> minimotd_migrated.yml <red>already present.");
@@ -99,7 +99,7 @@ public class SLPConfigMigrator{
public static int migrate(AdvancedServerList<?> core, CmdSender sender){
ServerListPlusCore slpCore = ServerListPlusCore.getInstance();
PluginLogger logger = core.plugin().pluginLogger();
PluginLogger logger = core.getPlugin().getPluginLogger();
if(slpCore == null){
logger.warn("[<white>Migrator - ServerListPlus</white>] Cannot migrate ServerListPlus config. ServerListPlus is not active!");
return 0;
@@ -128,7 +128,7 @@ public class SLPConfigMigrator{
}
private static int parseConf(AdvancedServerList<?> core, CmdSender sender, PersonalizedStatusConf.StatusConf conf, Type type){
PluginLogger logger = core.plugin().pluginLogger();
PluginLogger logger = core.getPlugin().getPluginLogger();
if(conf == null){
logger.info("[<white>Migrator - ServerListPlus</white>] No StatusConf found for type <white>%s</white>. Skipping...", type.getName());
sender.sendPrefixedMsg(" -> Not found! Skipping...");
@@ -176,7 +176,7 @@ public class SLPConfigMigrator{
return 0;
}
Path profile = core.plugin().folderPath().resolve("profiles").resolve(type.getFile());
Path profile = core.getPlugin().getFolderPath().resolve("profiles").resolve(type.getFile());
if(Files.exists(profile)){
logger.warn("[<white>Migrator - ServerListPlus</white>] Cannot create new file <white>%s</white>. One with the same name is already present", type.getFile());
sender.sendErrorMsg(" -> <red>File</red> %s <red>already present.", type.getFile());
@@ -223,7 +223,7 @@ public class SLPConfigMigrator{
if(type == Type.PERSONALIZED){
node.node("condition")
.set("${player name} != \"" + core.fileHandler().getString("Anonymous", "unknownPlayer", "name") + "\"");
.set("${player name} != \"" + core.getFileHandler().getString("Anonymous", "unknownPlayer", "name") + "\"");
}else
if(type == Type.BANNED){
node.node("condition")
@@ -0,0 +1,85 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.objects;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public abstract class CacheUtil{
protected final Duration duration;
public CacheUtil(Duration duration){
this.duration = duration;
}
public static class SingletonCache<T> extends CacheUtil{
private CacheValue<T> cacheValue = null;
public SingletonCache(Duration duration){
super(duration);
}
public T get(Supplier<T> supplier){
if(cacheValue == null || cacheValue.expired())
cacheValue = new CacheValue<>(System.currentTimeMillis(), duration, supplier.get());
return cacheValue.value();
}
}
public static class IdentifiableCache<K, V> extends CacheUtil{
private final Map<K, CacheValue<V>> cacheValues = new HashMap<>();
public IdentifiableCache(Duration duration){
super(duration);
}
public V get(K key, Supplier<V> supplier){
CacheValue<V> value = cacheValues.get(key);
if(value == null || value.expired()){
value = new CacheValue<>(System.currentTimeMillis(), duration, supplier.get());
cacheValues.put(key, value);
}
return cacheValues.get(key).value();
}
public void clear(){
cacheValues.clear();
}
}
public record CacheValue<T>(long timestamp, Duration duration, T value){
public boolean expired(){
return (timestamp < 0L) || System.currentTimeMillis() - timestamp >= duration.toMillis();
}
}
}
@@ -0,0 +1,57 @@
package ch.andre601.advancedserverlist.core.objects;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
public class PluginMessageUtil{
private static PluginMessageUtil instance;
private final List<String> knownServers = new CopyOnWriteArrayList<>();
private final Map<String, ParsedText> text = new ConcurrentHashMap<>();
private PluginMessageUtil(){}
public static PluginMessageUtil get(){
if(instance != null)
return instance;
return (instance = new PluginMessageUtil());
}
public void addServer(String name){
this.knownServers.add(name);
}
public List<String> getKnownServers(){
return knownServers;
}
public void clear(){
knownServers.clear();
}
public String getOrExecute(String key, String value, BiFunction<String, String, String> function){
if(text.get(key) != null){
return getOrDefault(key, value);
}
return function.apply(key, value);
}
public void queue(String key, String value, boolean parsed){
text.put(key, new ParsedText(value, parsed));
}
public String getOrDefault(String key, String def){
if(text.get(key) == null)
return def;
return text.get(key).parsed() ? text.remove(key).text() : text.get(key).text();
}
private record ParsedText(String text, boolean parsed){}
}
@@ -25,11 +25,8 @@
package ch.andre601.advancedserverlist.core.parsing;
import ch.andre601.advancedserverlist.core.compat.miniplaceholders.MiniPlaceholdersUtil;
import ch.andre601.advancedserverlist.core.interfaces.core.PluginCore;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import java.util.function.Function;
@@ -64,17 +61,6 @@ public class ComponentParser{
return this;
}
public ComponentParser applyMMTags(PluginCore<?> plugin){
if(!plugin.isPluginEnabled("MiniPlaceholders"))
return this;
TagResolver resolver = MiniPlaceholdersUtil.tagResolvers();
Component component = mm.deserialize(this.text, resolver);
this.text = mm.serialize(component);
return this;
}
public Component toComponent(){
return mm.deserialize(text);
}
@@ -77,8 +77,8 @@ public class TextCenterUtil{
private Map<String, FontInfo> loadFontWidths(AdvancedServerList<?> plugin){
try(InputStream stream = plugin.getClass().getResourceAsStream("/char-widths.json.gz")){
if(stream == null){
plugin.plugin().pluginLogger().warn("Internal char-widths.json.gz couldn't be retrieved.");
plugin.plugin().pluginLogger().warn("The <center> Placeholder won't work.");
plugin.getPlugin().getPluginLogger().warn("Internal char-widths.json.gz couldn't be retrieved.");
plugin.getPlugin().getPluginLogger().warn("The <center> Placeholder won't work.");
return new HashMap<>();
}
@@ -87,9 +87,9 @@ public class TextCenterUtil{
return new Gson().fromJson(reader, new TypeToken<Map<String, FontInfo>>(){}.getType());
}catch(IOException ex){
plugin.plugin().pluginLogger().warn("Encountered IOException while loading font widths.");
plugin.plugin().pluginLogger().warn("The <center> Placeholder won't work.");
plugin.plugin().pluginLogger().warn("Cause: <white>%s</white>", ex.getMessage());
plugin.getPlugin().getPluginLogger().warn("Encountered IOException while loading font widths.");
plugin.getPlugin().getPluginLogger().warn("The <center> Placeholder won't work.");
plugin.getPlugin().getPluginLogger().warn("Cause: <white>%s</white>", ex.getMessage());
return new HashMap<>();
}
}
@@ -1,81 +0,0 @@
package ch.andre601.advancedserverlist.core.placeholders;
import ch.andre601.advancedserverlist.api.PlaceholderProvider;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.function.Function;
public class DatePlaceholder extends PlaceholderProvider{
public DatePlaceholder(){
super("date");
}
@Override
public @Nullable String parsePlaceholder(@NonNull String placeholder, @NonNull GenericPlayer player, @NonNull GenericServer server){
String[] args = placeholder.split("\\s+");
return switch(args[0]){
case "current" -> {
String format = "dd/MM/yyyy";
if(args.length > 1){
format = format(Arrays.copyOfRange(args, 1, args.length));
}
try{
SimpleDateFormat dateFormat = new SimpleDateFormat(format);
yield dateFormat.format(new Date());
}catch(IllegalArgumentException ex){
yield "ERR: Invalid Date pattern " + format;
}
}
case "isBefore" -> compareDates(args, integer -> integer < 0);
case "isExactly" -> compareDates(args, integer -> integer == 0);
case "isAfter" -> compareDates(args, integer -> integer > 0);
default -> null;
};
}
private String format(String... args){
if(args.length == 0)
return "dd/MM/yyyy";
String format = String.join(" ", args);
if(format.trim().isEmpty()){
return "dd/MM/yyyy";
}else{
return format.trim();
}
}
private String compareDates(String[] args, Function<Integer, Boolean> function){
if(args.length == 1)
return "ERR: No Date provided!";
String targetDateStr = args[1];
String format = "dd/MM/yyyy";
if(args.length > 2){
format = format(Arrays.copyOfRange(args, 2, args.length));
}
SimpleDateFormat dateFormat;
try{
dateFormat = new SimpleDateFormat(format);
Date targetDate = dateFormat.parse(targetDateStr);
int diff = new Date().compareTo(targetDate);
return String.valueOf(function.apply(diff));
}catch(IllegalArgumentException ex){
return "ERR: Invalid Date format provided!";
}catch(ParseException ex){
return "ERR: Received invalid Date " + targetDateStr;
}
}
}
@@ -46,18 +46,9 @@ public record ServerListProfile(int priority, String condition, ProfileEntry def
return true;
ParseWarnCollector collector = new ParseWarnCollector(condition);
PluginLogger logger = core.plugin().pluginLogger();
PluginLogger logger = core.getPlugin().getPluginLogger();
boolean result;
try{
result = core.parser().compileToBoolean(condition, collector, player, server);
}catch(Exception ex){
logger.warn("Encountered an Exception while parsing condition '%s'. Cause: %s", condition, ex.getMessage());
if(core.fileHandler().getBool(false, "debug"))
logger.warn("Stacktrace:", ex);
return false;
}
boolean result = core.getParser().evaluate(condition, player, server, collector);
if(collector.hasWarnings()){
logger.warn("Encountered <white>%d</white> Error(s) while parsing condition for file '<white>%s</white>':", collector.getWarnings().size(), file);
@@ -79,7 +70,7 @@ public record ServerListProfile(int priority, String condition, ProfileEntry def
}
if(profiles.size() == 1)
return profiles.getFirst(); // No need to run a random for 1 profile.
return profiles.get(0); // No need to run a random for 1 profile.
synchronized(random){
return profiles.get(random.nextInt(profiles.size()));
@@ -0,0 +1,82 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.profiles.conditions;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.reader.PlaceholderTokenReader;
import ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.reader.PlaceholderValueReader;
import ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.tokens.CustomExpressionTokenizer;
import ch.andre601.expressionparser.ExpressionParserEngine;
import ch.andre601.expressionparser.ParseWarnCollector;
import ch.andre601.expressionparser.operator.Operator;
import ch.andre601.expressionparser.parsers.ExpressionTemplateParser;
import ch.andre601.expressionparser.parsers.ValueReader;
import ch.andre601.expressionparser.templates.ExpressionTemplate;
import ch.andre601.expressionparser.tokens.Token;
import ch.andre601.expressionparser.tokens.readers.TokenReader;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.List;
public class ProfileConditionParser implements ExpressionParserEngine{
private final CustomExpressionTokenizer tokenizer;
private final ExpressionTemplateParser parser;
private ProfileConditionParser(List<TokenReader> tokenReaders, ImmutableMap<Token, Operator> operators, ImmutableList<ValueReader> valueReaders){
this.tokenizer = new CustomExpressionTokenizer(tokenReaders);
this.parser = new ExpressionTemplateParser(operators, valueReaders);
}
public static ProfileConditionParser create(){
return new ProfileConditionParserBuilder().addDefaults()
.addTokenReader(new PlaceholderTokenReader(-20))
.addValueReader(new PlaceholderValueReader())
.build();
}
public boolean evaluate(String text, GenericPlayer player, GenericServer server, ParseWarnCollector collector){
ExpressionTemplate result = parser.parse(tokenizer.parse(text, player, server, collector), collector);
if(result == null)
return false;
return result.returnBooleanExpression().evaluate();
}
@Override
public ExpressionTemplate compile(String s, ParseWarnCollector parseWarnCollector){
return null;
}
private static class ProfileConditionParserBuilder extends ExpressionParserEngine.Builder<ProfileConditionParser>{
@Override
public ProfileConditionParser build(){
return new ProfileConditionParser(this.getTokenReaders(), ImmutableMap.copyOf(this.getOperators()), ImmutableList.copyOf(this.getValueReaders()));
}
}
}
@@ -0,0 +1,48 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.reader;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.expressionparser.ParseWarnCollector;
import ch.andre601.expressionparser.tokens.Token;
import ch.andre601.expressionparser.tokens.readers.TokenReader;
import java.text.ParsePosition;
public abstract class CustomTokenReader extends TokenReader{
public CustomTokenReader(int priority){
super(priority);
}
@Override
public Token read(String text, ParsePosition position, ParseWarnCollector collector){
return null;
}
public abstract Token read(String text, ParsePosition position, GenericPlayer player, GenericServer server, ParseWarnCollector collector);
}
@@ -23,46 +23,27 @@
*
*/
package ch.andre601.advancedserverlist.core.profiles.conditions.reader;
package ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.reader;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.advancedserverlist.core.profiles.conditions.tokens.PlaceholderToken;
import ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.tokens.PlaceholderToken;
import ch.andre601.advancedserverlist.core.profiles.replacer.StringReplacer;
import ch.andre601.expressionparser.ParseWarnCollector;
import ch.andre601.expressionparser.tokens.Token;
import ch.andre601.expressionparser.tokens.readers.TokenReader;
import java.text.ParsePosition;
public class PlaceholderTokenReader extends TokenReader{
public class PlaceholderTokenReader extends CustomTokenReader{
public PlaceholderTokenReader(){
super(-20);
public PlaceholderTokenReader(int priority){
super(priority);
}
@Override
public Token read(String text, ParsePosition position, ParseWarnCollector collector, Object... args){
GenericPlayer player = null;
GenericServer server = null;
for(Object arg : args){
if(arg instanceof GenericPlayer pl){
player = pl;
}else
if(arg instanceof GenericServer srv){
server = srv;
}
}
if(player == null || server == null){
collector.appendWarning(position.getIndex(), "Received null GenericPlayer or GenericServer instance.");
return null;
}
public Token read(String text, ParsePosition position, GenericPlayer player, GenericServer server, ParseWarnCollector collector){
if(position.getIndex() + 1 < text.length() && text.charAt(position.getIndex()) == '$' && text.charAt(position.getIndex() + 1) == '{'){
position.setIndex(position.getIndex() + 2);
return new PlaceholderToken(StringReplacer.parsePlaceholder(text, position, player, server, collector));
}
return null;
@@ -23,9 +23,9 @@
*
*/
package ch.andre601.advancedserverlist.core.profiles.conditions.reader;
package ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.reader;
import ch.andre601.advancedserverlist.core.profiles.conditions.tokens.PlaceholderToken;
import ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.tokens.PlaceholderToken;
import ch.andre601.expressionparser.ParseWarnCollector;
import ch.andre601.expressionparser.parsers.ExpressionTemplateParser;
import ch.andre601.expressionparser.parsers.ValueReader;
@@ -37,8 +37,8 @@ import java.util.List;
public class PlaceholderValueReader extends ValueReader{
@Override
public ExpressionTemplate read(ExpressionTemplateParser parser, List<Token> tokens, ParseWarnCollector collector){
if(tokens.getFirst() instanceof PlaceholderToken){
PlaceholderToken token = (PlaceholderToken)tokens.removeFirst();
if(tokens.get(0) instanceof PlaceholderToken){
PlaceholderToken token = (PlaceholderToken)tokens.remove(0);
return token.getValue();
}
return null;
@@ -0,0 +1,92 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.tokens;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.reader.CustomTokenReader;
import ch.andre601.expressionparser.ParseWarnCollector;
import ch.andre601.expressionparser.tokens.Token;
import ch.andre601.expressionparser.tokens.readers.TokenReader;
import com.google.common.collect.Ordering;
import java.text.ParsePosition;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
public class CustomExpressionTokenizer{
private static final Ordering<TokenReader> TOKEN_READER_ORDERING = Ordering.from(Comparator.comparingInt(TokenReader::getPriority)).reverse();
private final List<TokenReader> tokenReaders;
public CustomExpressionTokenizer(Iterable<TokenReader> tokenReaders){
this.tokenReaders = TOKEN_READER_ORDERING.immutableSortedCopy(tokenReaders);
}
public List<Token> parse(String text, GenericPlayer player, GenericServer server, ParseWarnCollector collector){
ParsePosition position = new ParsePosition(0);
List<Token> tokens = new LinkedList<>();
next_token:
while(true){
while(position.getIndex() < text.length() && Character.isWhitespace(text.charAt(position.getIndex())))
position.setIndex(position.getIndex() + 1);
if(position.getIndex() >= text.length())
break;
for(TokenReader tokenReader : tokenReaders){
Token token;
if(tokenReader instanceof CustomTokenReader customTokenReader){
if(null != (token = customTokenReader.read(text, position, player, server, collector))){
tokens.add(token);
continue next_token;
}
}else{
if(null != (token = tokenReader.read(text, position, collector))){
tokens.add(token);
continue next_token;
}
}
}
if(position.getIndex() >= text.length()){
collector.appendWarningFormatted(
"Current index position (%d) is larger or equal to Text length (%d) for condition '%s'",
position.getIndex(), text.length(), text
);
}else{
collector.appendWarningFormatted(position.getIndex(), "Illegal Token '%c'.", text.charAt(position.getIndex()));
}
break;
}
return tokens;
}
}
@@ -23,7 +23,7 @@
*
*/
package ch.andre601.advancedserverlist.core.profiles.conditions.tokens;
package ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.tokens;
import ch.andre601.expressionparser.expressions.ToBooleanExpression;
import ch.andre601.expressionparser.expressions.ToDoubleExpression;
@@ -1,9 +1,33 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Andre_601
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package ch.andre601.advancedserverlist.core.profiles.handlers;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.interfaces.PluginLogger;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import ch.andre601.advancedserverlist.core.objects.CacheUtil;
import javax.imageio.ImageIO;
import java.awt.*;
@@ -17,8 +41,9 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.time.Duration;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
@@ -30,340 +55,175 @@ public class FaviconHandler<F>{
private final AdvancedServerList<F> core;
private final PluginLogger logger;
private final ThreadPoolExecutor faviconThreadPool;
private final Cache<String, CompletableFuture<F>> faviconCache;
private final CacheUtil.IdentifiableCache<String, CompletableFuture<F>> faviconCache;
private final Random random = new Random();
private final Map<String, F> localFavicons = new HashMap<>();
private final HttpClient client = HttpClient.newHttpClient();
private final Map<String, FaviconPair<F>> localFavicons = new HashMap<>();
private final Random random = new Random();
public FaviconHandler(AdvancedServerList<F> core){
this.core = core;
this.logger = core.plugin().pluginLogger();
this.faviconThreadPool = createThreadPool();
this.faviconCache = CacheBuilder.newBuilder()
.expireAfterWrite(core.fileHandler().getLong(1, 1, "faviconCacheTime"), TimeUnit.MINUTES)
.build();
this.logger = core.getPlugin().getPluginLogger();
this.faviconThreadPool = createFaviconThreadPool();
this.faviconCache = new CacheUtil.IdentifiableCache<>(Duration.ofMinutes(
core.getFileHandler().getLong(1, 1, "faviconCacheTime")
));
loadLocalFavicons();
}
public F favicon(String input){
if(localFavicons.containsKey(input.toLowerCase(Locale.ROOT))){
logger.debug(FaviconHandler.class, "Returning local Favicon '%s'.", input);
return localFavicons.get(input.toLowerCase(Locale.ROOT)).favicon();
}
if(input.equalsIgnoreCase("random")){
logger.debug(FaviconHandler.class, "Returning random local Favicon.");
FaviconPair<F> pair = randomFavicon();
if(pair == null){
logger.warn("Cannot return random Favicon. Make sure PNGs are available in the Favicons folder!");
return null;
public F getFavicon(String input){
if(!localFavicons.isEmpty()){
logger.debug(FaviconHandler.class, "Current Local Favicons:");
for(String key : localFavicons.keySet()){
logger.debug(FaviconHandler.class, " - %s", key);
}
return pair.favicon();
}
CompletableFuture<F> favicon = faviconCache.getIfPresent(input);
if(favicon != null)
return favicon.getNow(null);
logger.debug(FaviconHandler.class, "Processing '%s'", input);
if(input.contains(";")){
String[] parts = input.split(";");
logger.debug(FaviconHandler.class, "Handling multi-part Favicon: ['%s']", String.join("', '", parts));
favicon = CompletableFuture.supplyAsync(() -> {
List<BufferedImage> images = new ArrayList<>();
Arrays.stream(parts).forEach(part -> {
logger.debug(FaviconHandler.class, "Creating BufferedImage for part '%s'...", part);
images.add(create(part));
});
if(images.isEmpty()){
logger.debugWarn(FaviconHandler.class, "Unable to create Multi-part Favicon. List was empty.");
return null;
}
return composeFavicon(images);
}, faviconThreadPool);
}else{
logger.debug(FaviconHandler.class, "Handling Favicon input '%s'...", input);
favicon = CompletableFuture.supplyAsync(() -> {
logger.debug(FaviconHandler.class, "Creating BufferedImage for '%s'...", input);
BufferedImage image = create(input);
if(image == null){
logger.warn("Cannot create Favicon for '%s'. BufferedImage was null!", input);
return null;
}
return createFavicon(image);
}, faviconThreadPool);
if(localFavicons.containsKey(input.toLowerCase(Locale.ROOT))){
logger.debug(FaviconHandler.class, "Input matches local favicon Image. Returning it...");
return localFavicons.get(input.toLowerCase(Locale.ROOT));
}
faviconCache.put(input, favicon);
return favicon.getNow(null);
logger.debug(FaviconHandler.class, "Getting Favicon image from cache...");
return faviconCache.get(input, () -> getFuture(input)).getNow(null);
}
public void cleanCache(){
localFavicons.clear();
faviconCache.invalidateAll();
faviconCache.clear();
loadLocalFavicons();
}
private F composeFavicon(List<BufferedImage> images){
logger.debug(FaviconHandler.class, "Composing Favicon out of %d part(s)...", images.size());
BufferedImage image = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
String strategy = core.fileHandler().getString("resize", "faviconStrategy");
int layer = 0;
for(BufferedImage img : images){
layer++;
logger.debug(FaviconHandler.class, "Applying layer %d...", layer);
if(img == null){
logger.debugWarn(FaviconHandler.class, "Layer %d was null! Skipping...", layer);
continue;
}
switch(strategy.toLowerCase(Locale.ROOT)){
case "center" -> {
int height = img.getHeight();
int width = img.getWidth();
int posHeight = 0;
int posWidth = 0;
if(height != 64){
posHeight = (64 - height) / 2;
}
// 64 - 128 = -64
if(width != 64){
posWidth = (64 - width) / 2;
}
graphics.drawImage(img, posWidth, posHeight, img.getWidth(), img.getHeight(), null);
}
case "none" -> graphics.drawImage(img, 0, 0, img.getWidth(), img.getHeight(), null);
default -> graphics.drawImage(img, 0, 0, 64, 64, null);
}
logger.debug(FaviconHandler.class, "Applied layer %d!", layer);
}
graphics.dispose();
logger.debug(FaviconHandler.class, "Image Composing complete! Creating Favicon instance...");
return createFavicon(image);
}
private BufferedImage create(String input){
private CompletableFuture<F> getFuture(String input){
if(input.toLowerCase(Locale.ROOT).startsWith("https://")){
logger.debug(FaviconHandler.class, "Handling URL '%s'...", input);
return fromURL(input);
logger.debug(FaviconHandler.class, "Resolving URL '%s'...", input);
return CompletableFuture.supplyAsync(() -> fromURL(core, input), this.faviconThreadPool);
}else
if(input.equalsIgnoreCase("random")){
logger.debug(FaviconHandler.class, "Returning random local Favicon...");
FaviconPair<F> pair = randomFavicon();
return pair == null ? null : pair.image();
return CompletableFuture.completedFuture(getRandomized());
}else
if(input.toLowerCase(Locale.ROOT).endsWith(".png")){
logger.debug(FaviconHandler.class, "Returning local Favicon '%s'...", input);
FaviconPair<F> pair = localFavicons.get(input.toLowerCase(Locale.ROOT));
return pair == null ? null : pair.image();
logger.debug(FaviconHandler.class, "Resolving image file '%s'...", input);
return CompletableFuture.completedFuture(localFavicons.get(input.toLowerCase(Locale.ROOT)));
}else{
logger.debug(FaviconHandler.class, "Handling Player Name/UUID '%s'...", input);
return fromURL("https://mc-heads.net/avatar/" + input + "/64");
logger.debug(FaviconHandler.class, "Resolving '%s' as 'https://mc-heads.net/avatar/%s/64'...", input, input);
return CompletableFuture.supplyAsync(() -> fromURL(core, "https://mc-heads.net/avatar/" + input + "/64"), this.faviconThreadPool);
}
}
private FaviconPair<F> randomFavicon(){
private F getRandomized(){
logger.debug(FaviconHandler.class, "Obtaining random Favicon...");
if(localFavicons.isEmpty())
return null;
List<FaviconPair<F>> favicons = new ArrayList<>(localFavicons.values());
if(favicons.isEmpty())
return null;
List<F> faviconList = List.copyOf(localFavicons.values());
if(favicons.size() == 1)
return favicons.getFirst();
// Don't use Random for just one entry.
if(faviconList.size() == 1)
return faviconList.get(0);
synchronized(random){
return favicons.get(random.nextInt(favicons.size()));
return faviconList.get(random.nextInt(faviconList.size()));
}
}
private void loadLocalFavicons(){
this.localFavicons.clear();
logger.info("[-] Loading local images as Favicons...");
logger.info("[-] Loading local image files as Favicons...");
Path folder = core.plugin().folderPath().resolve("favicons");
Path folder = core.getPlugin().getFolderPath().resolve("favicons");
if(!Files.exists(folder)){
try{
Files.createDirectories(folder);
logger.debugSuccess(FaviconHandler.class, "Created favicons folder");
logger.debugSuccess(FaviconHandler.class, "Created favicons folder.");
}catch(IOException ex){
logger.warn("Encountered IOException while creating favicons folder.", ex);
logger.failure("Cannot create favicons folder. Encountered an IOException.", ex);
return;
}
}
try(Stream<Path> stream = Files.list(folder)){
stream.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().endsWith(".png"))
.forEach(this::loadFile);
try(Stream<Path> pathStream = Files.list(folder)){
pathStream.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().endsWith(".png"))
.forEach(this::loadFile);
logger.success("Loaded <white>%s</white> local Favicon(s).", localFavicons.size());
logger.success("Loaded <white>%d</white> local Favicons.", localFavicons.size());
}catch(IOException ex){
if(core.fileHandler().getBool(false, "debug")){
logger.warn("Encountered IOException while loading local favicons.", ex);
}else{
logger.warn("Encountered IOException while loading local favicons. Reason: %s", ex.getMessage());
}
logger.failure("Cannot load files from favicons folder.", ex);
}
}
private void loadFile(Path path){
try(InputStream stream = Files.newInputStream(path)){
BufferedImage image = createBufferedImage(stream);
if(image == null)
F favicon = createFavicon(stream);
if(favicon == null){
logger.failure("Cannot create Favicon from file '<white>%s</white>'. Received Favicon was null.", path.getFileName().toString());
return;
}
F favicon = core.plugin().createFavicon(image);
if(favicon == null)
return;
localFavicons.put(path.getFileName().toString().toLowerCase(Locale.ROOT), new FaviconPair<>(image, favicon));
localFavicons.put(path.getFileName().toString().toLowerCase(Locale.ROOT), favicon);
logger.debugSuccess(FaviconHandler.class, "Loaded file '<white>%s</white>' as Favicon.", path.getFileName().toString());
}catch(IOException ex){
if(core.fileHandler().getBool(false, "debug")){
logger.warn("Encountered IOException while creating a Favicon from '%s'.", ex, path.getFileName().toString());
}else{
logger.warn("Encountered IOException while creating a Favicon from '%s'. Reason: %s", path.getFileName().toString(), ex.getMessage());
}
}catch(Exception ex){
if(core.fileHandler().getBool(false, "debug")){
logger.warn("Encountered Exception while creating a Favicon from '%s'.", ex, path.getFileName().toString());
}else{
logger.warn("Encountered Exception while creating a Favicon from '%s'. Reason: %s", path.getFileName().toString(), ex.getMessage());
}
logger.failure("Cannot create Favicon from file '<white>%s</white>'. Encountered IOException.", ex);
}
}
private BufferedImage fromURL(String url){
private F fromURL(AdvancedServerList<F> core, String url){
try{
logger.debug(FaviconHandler.class, "Creating request for URL '%s'...", url);
logger.debug(FaviconHandler.class, "Creating Request for URL '%s'...", url);
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url))
.header("User-Agent", "AdvancedServerList-" + core.plugin().loader() + "/" + core.version())
.build();
.uri(new URI(url))
.header("User-Agent", "AdvancedServerList-" + core.getPlugin().getLoader() + "/" + core.getVersion())
.build();
try(InputStream stream = client.send(request, HttpResponse.BodyHandlers.ofInputStream()).body()){
return createBufferedImage(stream);
}catch(IOException ex){
if(core.fileHandler().getBool(false, "debug")){
logger.warn("Encountered IOException while performing a web request for '%s'.", ex, url);
}else{
logger.warn("Encountered IOException while performing a web request for '%'. Reason: %s", url, ex.getMessage());
}
return null;
}catch(InterruptedException ex){
if(core.fileHandler().getBool(false, "debug")){
logger.warn("Encountered an InterruptedException while performing a web request for '%s'.", ex, url);
}else{
logger.warn("Encountered an InterruptedException while performing a web request for '%s'. Reason: %s", url, ex.getMessage());
}
return createFavicon(stream);
}catch(InterruptedException | IOException ex){
logger.warn("Cannot create Favicon from URL '<white>%s</white>'. Encountered an Exception.", ex, url);
return null;
}
}catch(URISyntaxException ex){
if(core.fileHandler().getBool(false, "debug")){
logger.warn("Encountered an URISyntaxException while performing a web request for '%s'.", ex, url);
}else{
logger.warn("Encountered an URISyntaxException while performing a web request for '%s'. Reason: %s", url, ex.getMessage());
}
logger.warn("Cannot create Favicon from URL '<white>%s</white>'. Encountered a URISyntaxException.", ex, url);
return null;
}
}
private BufferedImage createBufferedImage(InputStream stream){
logger.debug(FaviconHandler.class, "Creating BufferedImage from InputStream...");
private F createFavicon(InputStream stream){
try{
logger.debug(FaviconHandler.class, "Creating BufferedImage from InputStream...");
BufferedImage original = ImageIO.read(stream);
if(original == null){
logger.warn("Cannot create Favicon. Received a null BufferedImage!");
logger.warn("Cannot create Favicon. Received null BufferedImage.");
return null;
}
logger.debug(FaviconHandler.class, "Check image size for possible resizing...");
if(original.getHeight() == 64 && original.getWidth() == 64){
logger.debug(FaviconHandler.class, "Image is 64x64. No resizing needed!");
return original;
// Don't waste resources resizing images already having right size.
if(original.getWidth() == 64 && original.getHeight() == 64){
logger.debug(FaviconHandler.class, "BufferedImage is 64x64 pixels. No resizing needed.");
return core.getPlugin().createFavicon(original);
}
logger.debug(FaviconHandler.class, "Applying Favicon resize strategy...");
logger.debug(FaviconHandler.class, "Resizing image to 64x64 pixels...");
BufferedImage image = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
Graphics2D graphics2D = image.createGraphics();
String resizeMode = core.fileHandler().getString("resize", "faviconStrategy");
switch(resizeMode.toLowerCase(Locale.ROOT)){
case "center" -> {
int height = original.getHeight();
int width = original.getWidth();
int posHeight = 0;
int posWidth = 0;
if(height != 64){
posHeight = (64 - height) / 2;
}
if(width != 64){
posWidth = (64 - width) / 2;
}
graphics.drawImage(original, posWidth, posHeight, original.getWidth(), original.getHeight(), null);
}
case "none" -> graphics.drawImage(original, 0, 0, original.getWidth(), original.getHeight(), null);
default -> graphics.drawImage(original, 0, 0, 64, 64, null);
}
graphics2D.drawImage(original, 0, 0, 64, 64, null);
graphics2D.dispose();
graphics.dispose();
logger.debug(FaviconHandler.class, "Image resized. Returning it...");
return image;
}catch(IOException ex){
logger.warn("Encountered an IOException while processing Favicon!", ex);
return null;
}
}
private F createFavicon(BufferedImage image){
try{
return core.plugin().createFavicon(image);
logger.debug(FaviconHandler.class, "Image resized! Returning Favicon...");
return core.getPlugin().createFavicon(image);
}catch(Exception ex){
if(core.fileHandler().getBool(false, "debug")){
logger.warn("Encountered an Exception while trying to create a Favicon instance.", ex);
}else{
logger.warn("Encountered an Exception while trying to create a Favicon instance. Reason: %s", ex.getMessage());
}
logger.warn("Unable to create Favicon. Received Exception.", ex);
return null;
}
}
private ThreadPoolExecutor createThreadPool(){
private ThreadPoolExecutor createFaviconThreadPool(){
return new ThreadPoolExecutor(3, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1024), r -> {
Thread t = new Thread(r, "AdvancedServerList-FaviconThread");
t.setDaemon(true);
return t;
});
}
private record FaviconPair<F>(BufferedImage image, F favicon){}
}
@@ -61,8 +61,8 @@ public class PlayerHandler{
public PlayerHandler(AdvancedServerList<?> core){
this.core = core;
this.logger = core.plugin().pluginLogger();
this.cache = core.plugin().folderPath().resolve("playercache.json");
this.logger = core.getPlugin().getPluginLogger();
this.cache = core.getPlugin().getFolderPath().resolve("playercache.json");
}
public void load(){
@@ -71,7 +71,7 @@ public class PlayerHandler{
return;
}
if(core.fileHandler().getBoolean("disableCache")){
if(core.getFileHandler().getBoolean("disableCache")){
logger.info("'disableCache' is set to true. Skipping playercache.json loading...");
return;
}
@@ -103,7 +103,7 @@ public class PlayerHandler{
return;
}
if(core.fileHandler().getBoolean("disableCache")){
if(core.getFileHandler().getBoolean("disableCache")){
logger.debug(PlayerHandler.class, "[-] 'disable_cache' is set to true. Not saving cached players.");
return;
}
@@ -117,14 +117,14 @@ public class PlayerHandler{
}
public void addPlayer(String ip, String name, UUID uuid){
if(contains(ip) || core.fileHandler().getBoolean("disableCache"))
if(contains(ip) || core.getFileHandler().getBoolean("disableCache"))
return;
cachedPlayers.add(new CachedPlayer(ip, name, uuid));
}
public CachedPlayer getCachedPlayer(String key){
if(!contains(key) || core.fileHandler().getBoolean("disableCache"))
if(!contains(key) || core.getFileHandler().getBoolean("disableCache"))
return getDefaultPlayer();
for(CachedPlayer player : cachedPlayers){
@@ -164,8 +164,8 @@ public class PlayerHandler{
return (defaultPlayer = new CachedPlayer(
"0.0.0.0",
core.fileHandler().getString("Anonymous", "unknownPlayer", "name"),
convertToUUID(core.fileHandler().getString(defaultUUID.toString(), "unknownPlayer", "uuid"))
core.getFileHandler().getString("Anonymous", "unknownPlayer", "name"),
convertToUUID(core.getFileHandler().getString(defaultUUID.toString(), "unknownPlayer", "uuid"))
));
}
@@ -78,7 +78,7 @@ public class ProfileManager{
}
public static ServerListProfile resolveProfile(AdvancedServerList<?> core, GenericPlayer player, GenericServer server){
for(ServerListProfile profile : core.fileHandler().profiles()){
for(ServerListProfile profile : core.getFileHandler().getProfiles()){
if(profile.isInvalidProfile())
continue;
@@ -120,13 +120,20 @@ public class ProfileManager{
}
public static boolean checkOption(Object obj){
return switch(obj){
case List<?> list -> !list.isEmpty(); // Check if list isn't empty
case String str -> !str.isEmpty(); // Check if String is not empty
case NullBool nb -> nb.getOrDefault(false); // Return NullBool's value
case null, default -> false;
};
if(obj == null)
return false;
if(obj instanceof List<?> list){
return !list.isEmpty(); // Check if list isn't empty
}else
if(obj instanceof String str){
return !str.isEmpty(); // Check if String is not empty
}else
if(obj instanceof NullBool nb){
return nb.getOrDefault(false); // Return NullBool's value
}
return false;
}
private static <T> T resolve(ProfileEntry defProfile, ProfileEntry profile, Function<ProfileEntry, T> function){
@@ -30,18 +30,13 @@ import ch.andre601.advancedserverlist.api.PlaceholderProvider;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.advancedserverlist.core.AdvancedServerList;
import ch.andre601.advancedserverlist.core.profiles.conditions.tokens.PlaceholderToken;
import ch.andre601.advancedserverlist.core.profiles.conditions.placeholders.tokens.PlaceholderToken;
import ch.andre601.expressionparser.ParseWarnCollector;
import java.text.ParsePosition;
import java.util.function.Function;
public class StringReplacer{
public static StringModifier modifier(String text){
return new StringModifier(text);
}
public static String replace(String input, GenericPlayer player, GenericServer server){
if(input == null)
return null;
@@ -68,7 +63,7 @@ public class StringReplacer{
}
private static String parsePlaceholder0(String input, ParsePosition position, GenericPlayer player, GenericServer server, ParseWarnCollector collector){
AdvancedServerListAPI api = AdvancedServerList.api();
AdvancedServerListAPI api = AdvancedServerList.getApi();
int index = position.getIndex();
StringBuilder identifier = new StringBuilder();
@@ -153,22 +148,4 @@ public class StringReplacer{
return replacement;
}
public static class StringModifier{
private String value;
public StringModifier(String value){
this.value = value;
}
public StringModifier modify(Function<String, String> function){
this.value = function.apply(value);
return this;
}
public String asString(){
return value;
}
}
}
-13
View File
@@ -66,19 +66,6 @@ printBanner: true
#
faviconCacheTime: 1
#
# Sets how AdvancedServerList should handle the size and positioning of images for favicons.
#
# Available options:
#
# - resize: Resizes the image to 64x64 pixels if it isn't these dimensions already.
# - center: Doesn't resize the image, but instead tries to center it as good as possible in a 64x64 area.
# - none: Doesn't resize the image and leaves it positioned at x 0 and y 0 (top-left) of the 64x64 area.
#
# Setting any not listed option or leaving this blank/removing it sets the option to resize.
#
faviconResizeStrategy: 'resize'
#
# Options to disable while the Maintenance Plugin is present and Maintenance Mode is active.
#
+37 -43
View File
@@ -2,7 +2,7 @@
# Set the priority of this profile.
# If there are multiple profiles (files) will they be sorted by priority, starting with highest.
#
# Read more: https://asl.andre601.ch/profiles/options/priority/
# Read more: https://asl.andre601.ch/profiles/#priority
#
priority: 0
@@ -10,11 +10,11 @@ priority: 0
# Set condition(s) that should return true to display this profile.
# This allows you to create profiles to only show for specific scenarios (i.e. player is banned).
#
# 'AND'/'&&' and 'OR'/'||' can be used to chain together multiple conditions.
# Multiple conditions can be chaing with the keywords 'and', 'or', or their counterparts '&&' and '||'.
#
# Removing the condition option or setting it to an empty String will be treated as a true condition being returned.
# No condition or an empty condition will be treated as true by default.
#
# Read more: https://asl.andre601.ch/profiles/options/condition
# Read more: https://asl.andre601.ch/profiles/#condition
#
condition: ''
@@ -30,7 +30,7 @@ condition: ''
#
# When not present or empty (profiles: []), no profiles will be used and global options from the file will be used instead.
#
# Read more: https://asl.andre601.ch/profiles/options/profiles
# Read more: https://asl.andre601.ch/profiles/#profiles
#
profiles:
- motd: ['<aqua>Line A', '<gold>Line B']
@@ -40,17 +40,14 @@ profiles:
#
# Set the "Message of the day" to display.
# This option supports RGB colors for 1.16+ Servers and clients. More advanced MiniMessage features such as <gradient>
# can be used for further styling the text.
#
# <center> can be put at the start of a line to have AdvancedServerList automatically center the text as good as
# possible.
# This option supports RGB colors for 1.16+ Servers and clients, including gradients using MiniMessage's <gradient>
# option.
#
# Only the first two lines will be considered and any additional one is ignored.
#
# When not present or empty (motd: []), no MOTD will be set.
#
# Read more: https://asl.andre601.ch/profiles/options/motd
# Read more: https://asl.andre601.ch/profiles/#motd
#
motd:
- '<grey>First Line'
@@ -58,36 +55,33 @@ motd:
#
# Set the Favicon (image on the left of a server entry) to display.
# This option supports three types of input:
# This option supports three types of inputs:
#
# 1. Filename, including the .png extension, matching a filename in the favicons folder of the plugin.
# - 'random' can be used to have AdvancedServerList select a random image from the favicons folder every time
# the server is pinged.
# 1. Name, including .png extension, pointing to an image located in the plugin's favicons folder.
# 2. "random" to display a random PNG from the favicons folder.
# 3. URL pointing to an image.
# 4. Player name, UUID or a placeholder that returns either.
# - https://mc-heads.net will be used to retrieve an image of the provided name/UUID.
# 4. Player name, UUID or placeholders returning one of these.
# https://mc-heads.net will be used to retrieve an image of the provided name/UUID.
#
# Any image below or above 64x64 pixels will be resized according to the faviconResizeMode option in the config.yml.
# Note on favicons not showing:
# AdvancedServerList has to create a Favicon object for the Proxy/Server to use which takes time to do so.
# To not delay the Proxy/Server from being displayed (increase the loading time on the player end) is this creation
# done asynchronously for URL/Player name/UUID. This has the downside that the Proxy/Server may be done handling the
# ping before the Favicon is created, causing none to be displayed.
# Note that this won't happen with local favicons, as those are created during the plugin's startup.
#
# Any images above or below 64x64 pixels will be automatically resized to these dimensions.
#
# If not present or empty (favicon: ''), defaults to not modifying the favicon.
#
# Read more: https://asl.andre601.ch/profiles/options/favicon
#
# "The favicon isn't showing!"
# This is a side effect of using either URLs or player names/UUIDs for the favicon.
# These types require AdvancedServerList to create a new favicon object from scratch. This action takes a significant
# amount of time, which can cause the server to show with a huge ping.
# To avoid this is this action performed asynchronously. This means that the plugin doesn't wait for the favicon to
# be created before continuing, which can result in no favicon being available when it is done handling the ping.
# This should only happen for the first ping of a player and subsequent pings should display the favicon, if the
# pings are done within the configured cache time (See 'faviconCacheTime' in the config.yml).
# Read more: https://asl.andre601.ch/profiles/#favicon
#
favicon: ''
#
# Contains options for modifying the player count (Text displaying <online>/<max>).
#
# Read more: https://asl.andre601.ch/profiles/options/playercount
# Read more: https://asl.andre601.ch/profiles/#playercount
#
playerCount:
#
@@ -97,7 +91,7 @@ playerCount:
#
# If not present, defaults to false.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/hideplayers
# Read more: https://asl.andre601.ch/profiles/#hideplayers
#
hidePlayers: false
#
@@ -106,17 +100,17 @@ playerCount:
#
# If not present, defaults to false.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/hideplayershover
# Read more: https://asl.andre601.ch/profiles/#hideplayershover
#
hidePlayersHover: false
#
# Set the lines shown when the player hovers over the player count.
# Unlike the MOTD option does this one only support basic color and formatting codes. Any RGB colors will be
# down-sampled to the nearest matching color.
# downsampled to the nearest matching color.
#
# If not present or empty (hover: []), defaults to not modifying the hover.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/hover
# Read more: https://asl.andre601.ch/profiles/#hover
#
hover:
- '<aqua>Line 1'
@@ -130,7 +124,7 @@ playerCount:
#
# If not present or empty (text: ''), defaults to not modifying the text.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/text
# Read more: https://asl.andre601.ch/profiles/#text
#
text: ''
#
@@ -139,7 +133,7 @@ playerCount:
#
# Using this feature will modify the ${server playersMax} placeholder, except for usage in the condition option.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/extraplayers
# Read more: https://asl.andre601.ch/profiles/#extraplayers
#
extraPlayers:
#
@@ -149,7 +143,7 @@ playerCount:
#
# If not present, defaults to false.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/extraplayers#enabled
# Read more: https://asl.andre601.ch/profiles/#extraplayers-enabled
#
enabled: false
#
@@ -161,7 +155,7 @@ playerCount:
#
# If not present, defaults to 0.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/extraplayers#amount
# Read more: https://asl.andre601.ch/profiles/#extraplayers-amount
#
amount: 0
#
@@ -169,7 +163,7 @@ playerCount:
#
# Using this feature will modify the ${server playersMax} placeholder, except for usage in the condition option.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/maxplayers
# Read more: https://asl.andre601.ch/profiles/#maxplayers
#
maxPlayers:
#
@@ -178,7 +172,7 @@ playerCount:
#
# If not present, defaults to false.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/maxplayers#enabled
# Read more: https://asl.andre601.ch/profiles/#maxplayers-enabled
#
enabled: false
#
@@ -187,13 +181,13 @@ playerCount:
#
# If not present, defaults to 0.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/maxplayers#amount
# Read more: https://asl.andre601.ch/profiles/#maxplayers-amount
#
amount: 0
#
# The onlinePlayers feature allows to set a number to use as the online player count to display.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/onlineplayers
# Read more: https://asl.andre601.ch/profiles/#onlineplayers
#
onlinePlayers:
#
@@ -202,7 +196,7 @@ playerCount:
#
# If not present, defaults to false.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/onlineplayers#enabled
# Read more: https://asl.andre601.ch/profiles/#onlineplayers-enabled
#
enabled: false
#
@@ -211,6 +205,6 @@ playerCount:
#
# If not present, defaults to current online player count.
#
# Read more: https://asl.andre601.ch/profiles/options/playercount/onlineplayers#amount
# Read more: https://asl.andre601.ch/profiles/#onlineplayers-amount
#
amount: 0
@@ -25,35 +25,27 @@
package ch.andre601.advancedserverlist.core;
import ch.andre601.advancedserverlist.api.objects.GenericPlayer;
import ch.andre601.advancedserverlist.api.objects.GenericServer;
import ch.andre601.expressionparser.DefaultExpressionParserEngine;
import ch.andre601.advancedserverlist.core.profiles.conditions.ProfileConditionParser;
import ch.andre601.expressionparser.ParseWarnCollector;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ExpressionParserTests{
private final DefaultExpressionParserEngine parser = DefaultExpressionParserEngine.createDefault();
private final ProfileConditionParser parser = ProfileConditionParser.create();
private final DummyPluginLogger logger = new DummyPluginLogger();
private final Map<String, Boolean> testValuesMap = getTestMap();
private final TestPlayer player = new TestPlayer();
private final TestServer server = new TestServer();
@Test
public void testBasicExpressionResults(){
for(Map.Entry<String, Boolean> values : testValuesMap.entrySet()){
logger.info("TEST[expression=\"%s\", expected=%b]", values.getKey(), values.getValue());
ParseWarnCollector collector = new ParseWarnCollector(values.getKey());
boolean result = parser.compileToBoolean(values.getKey(), collector, player, server);
boolean result = parser.evaluate(values.getKey(), null, null, collector);
logger.info("Result (Output, Expected?): %b, %b", result, (result == values.getValue()));
assertEquals(result, values.getValue());
@@ -74,38 +66,4 @@ public class ExpressionParserTests{
"false or 100 < 10", false
);
}
private static class TestPlayer implements GenericPlayer{
@Override
public @NonNull String getName(){
return "Lorem";
}
@Override
public int getProtocol(){
return 1;
}
@Override
public @NonNull UUID getUUID(){
return UUID.randomUUID();
}
}
private static class TestServer implements GenericServer{
@Override
public int getPlayersOnline(){
return 10;
}
@Override
public int getPlayersMax(){
return 20;
}
@Override
public @Nullable String getHost(){
return "play.example.com";
}
}
}
-15
View File
@@ -1,15 +0,0 @@
def on_pre_page_macros(env):
footer = """
{% if page.meta and page.meta.api %}
{% include 'api_docs/constructor_summaries.md' %}
{% include 'api_docs/nested_class_summaries.md' %}
{% include 'api_docs/enum_summaries.md' %}
{% include 'api_docs/method_summaries.md' %}
{% include 'api_docs/constructor_details.md' %}
{% include 'api_docs/enum_details.md' %}
{% include 'api_docs/method_details.md' %}
{% endif %}
"""
env.markdown += footer
-23
View File
@@ -1,23 +0,0 @@
import re
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.files import Files
from mkdocs.structure.pages import Page
from re import Match
def on_page_markdown(
markdown: str, *, page: Page, config: MkDocsConfig, files: Files
):
def replace(match: Match):
platform = match.group(1)
if platform == "paper": return "[:fontawesome-solid-paper-plane:](https://papermc.io){ .md-button .md-button--icon title=\"PaperMC\" target=\"_blank\" rel=\"noopener\" }"
elif platform == "bungee": return "[:simple-spigotmc:](https://spigotmc.org){ .md-button .md-button--icon title=\"BungeeCord\" target=\"_blank\" rel=\"noopener\" }"
elif platform == "velocity": return "[:simple-velocity:](https://velocitypowered.com){ .md-button .md-button--icon title=\"Velocity\" target=\"_blank\" rel=\"noopener\" }"
elif platform == "proxies": return "[:simple-spigotmc:](https://spigotmc.org){ .md-button .md-button--icon title=\"BungeeCord\" target=\"_blank\" rel=\"noopener\" } [:simple-velocity:](https://velocitypowered.com){ .md-button .md-button--icon title=\"Velocity\" target=\"_blank\" rel=\"noopener\" }"
elif platform == "all": return "[:fontawesome-solid-paper-plane:](https://papermc.io){ .md-button .md-button--icon title=\"PaperMC\" target=\"_blank\" rel=\"noopener\" } [:simple-spigotmc:](https://spigotmc.org){ .md-button .md-button--icon title=\"BungeeCord\" target=\"_blank\" rel=\"noopener\" } [:simple-velocity:](https://velocitypowered.com){ .md-button .md-button--icon title=\"Velocity\" target=\"_blank\" rel=\"noopener\" }"
else: return platform
return re.sub(
r"<!-- icon:(paper|bungee|velocity|proxies|all) -->",
replace, markdown, flags = re.I | re.M
)
+35
View File
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% import "api-objects/info-template.html" as info %}
{% block content %}
{% include "partials/content.html" %}
{% if page.meta %}
<!--
Object summaries
-->
{% if page.meta.constructors %}
{{ info.summary('Constructor Summary', page.meta.constructors, details_type='constructor', defaults=page.meta.defaults) }}
{% endif %}
{% if page.meta.classes %}
{{ info.summary('Class Summary', page.meta.classes, details_type='class', defaults=page.meta.defaults) }}
{% endif %}
{% if page.meta.enums %}
{{ info.summary('Enum Constant Summary', page.meta.enums, details_type='enum', defaults=page.meta.defaults) }}
{% endif %}
{% if page.meta.methods or page.meta.inherits %}
{{ info.summary('Method Summary', page.meta.methods, page.meta.inherits, details_type='method', defaults=page.meta.defaults) }}
{% endif %}
<!--
Detailed object info
-->
{% if page.meta.constructors %}
{{ info.info('Constructor Detail', page.meta.constructors, 'constructor', defaults=page.meta.defaults) }}
{% endif %}
{% if page.meta.enums %}
{{ info.info('Enum Constant Detail', page.meta.enums, 'enum', defaults=page.meta.defaults) }}
{% endif %}
{% if page.meta.methods %}
{{ info.info('Method Detail', page.meta.methods, 'method', defaults=page.meta.defaults)}}
{% endif %}
{% endif %}
{% endblock %}
@@ -0,0 +1,228 @@
{% macro summary(title, summaries, inherits, details_type = 'unknown', defaults = []) -%}
<h2 id="{{ title | lower() | replace(' ', '-') | e}}">
{{ title }}
</h2>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
{% if summaries %}
<table>
<thead>
<tr>
{% if details_type and details_type == "method" %}
<th>Modifier and Type</th>
{% endif %}
<th>{{ details_type | capitalize() }}</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for summary in summaries %}
<tr>
{% set modifiers = namespace(name = "") %}
{% for attribute in summary.attributes %}
{% set modifiers.name = modifiers.name ~ attribute %}
{% if loop.revindex > 0 %}{% set modifiers.name = modifiers.name ~ " " %}{% endif %}
{% endfor %}
{% if modifiers.name %}{% set modifiers.name = modifiers.name ~ " " %}{% endif %}
{% if summary.type and summary.type.name %}
{% set modifiers.name = modifiers.name ~ summary.type.name %}
{% elif defaults and defaults.type and defaults.type.name %}
{% set modifiers.name = modifiers.name ~ defaults.type.name %}
{% else %}
{% set modifiers.name = modifiers.name ~ "void" %}
{% endif %}
{% set desc = "" %}
{% if summary.deprecated %}
{% set desc = "<api__deprecated></api__deprecated><br>" ~ summary.deprecated.split("<br>")[0] %}
{% elif summary.description %}
{% set desc = summary.description.split("<br>")[0] %}
{% endif %}
{% set final_name = namespace(name = "") %}
{% if details_type and details_type == "enum" %}
{% set final_name.name = summary.name | upper() %}
{% else %}
{% set final_name.name = summary.name ~ "(" %}
{% endif %}
{% for parameter in summary.parameters %}
{% set final_name.name = final_name.name ~ parameter.type %}
{% if loop.revindex > 1 %}{% set final_name.name = final_name.name ~ ", " %}{% endif %}
{% endfor %}
{% if details_type != "enum" %}
{% set final_name.name = final_name.name ~ ")" %}
{% endif %}
{% if details_type and details_type == "method" %}
{% if modifiers.name %}
<td><code>{{ modifiers.name }}</code></td>
{% else %}
<td></td>
{% endif %}
{% endif %}
<td>
{% if summary.link %}
<a href="{{ summary.link }}"><code>{{ final_name.name | e}}</code></a>
{% else %}
<a href="#{{ final_name.name | lower() | replace(' ', '-') | e }}"><code>{{ final_name.name | e}}</code></a>
{% endif %}
</td>
<td>{{ desc }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if inherits %}
{% for classpath, inherit in inherits.items() %}
<table>
<thead>
<tr>
<th>Methods inherited from <a href="{{ inherit.link }}"><code>{{ classpath }}</code></a></th>
</tr>
</thead>
<tbody>
<tr>
<td>
{% for inh in inherit.list %}
<a href="{{ inherit.link }}#{{ inh | lower() | replace(' ', '-') | e }}">
<code>{{- inh | e -}}</code>
</a>
{%- if loop.revindex > 1 %}, {% endif -%}
{% endfor %}
</td>
</tr>
</tbody>
</table>
{% endfor %}
{% endif %}
</div>
</div>
{%- endmacro %}
{% macro info(title, details, details_type = 'unknown', defaults = []) -%}
<h2 id="{{ title | lower() | replace(' ', '-') | e}}">
{{ title }}
</h2>
{% for detail in details %}
<div class="api-detail">
{% set final_name = namespace(name = "") %}
{% if details_type == "enum" %}
{% set final_name.name = detail.name | upper() %}
{% else %}
{% set final_name.name = detail.name ~ "(" %}
{% endif %}
{% for parameter in detail.parameters %}
{% set final_name.name = final_name.name ~ parameter.type %}
{% if loop.revindex > 1 %}{% set final_name.name = final_name.name ~ ", " %}{% endif %}
{% endfor %}
{% if details_type != "enum" %}
{% set final_name.name = final_name.name ~ ")" %}
{% endif %}
<h3 id="{{ final_name.name | lower() | replace(' ', '-') | e }}">
{% if detail.deprecated %}
<api__deprecated></api__deprecated>
{% endif %}
{% for attribute in detail.attributes %}
<api__{{- attribute -}}></api__{{- attribute -}}>
{% endfor %}
{% if enum %}
<api__static></api__static> <api__final></api__final>
{% endif %}
{% if detail.type and details_type == "method" %}
{% if detail.type.link %}
<a href="{{ detail.type.link }}" class="api-type__{{- detail.type.type | default('primitive') -}}">{{ detail.type.name | default('void') | e }}</a>
{% else %}
<span class="api-type__{{- detail.type.type | default('primitive') -}}">{{ detail.type.name | default('void') | e }}</span>
{% endif %}
{% elif defaults and defaults.type and details_type == "method" %}
{% if defaults.type.link %}
<a href="{{ defaults.type.link }}" class="api-type__{{- defaults.type.type | default('primitive') -}}">{{ defaults.type.name | default('void') | e }}</a>
{% else %}
<span class="api-type__{{- defaults.type.type | default('primitive') -}}">{{ defaults.type.name | default('void') }}</span>
{% endif %}
{% elif details_type == "method" %}
<span class="api-type__primitive">void</span>
{% endif %}
<code>{{ final_name.name | e }}</code><a href="#{{ final_name.name | lower() | replace(' ', '-') | e }}" class="headerlink" title="Permanent link"></a>
</h3>
{% if detail.deprecated %}
<div class="admonition deprecated">
<div class="admonition-title">Deprecation Warning</div>
<p>
{{ detail.deprecated }}
</p>
</div>
{% endif %}
{% if detail.description %}
<p class="api-detail__description">
{{ detail.description }}
</p>
{% endif %}
{% if detail.parameters %}
<h4 id="{{ detail.name | lower() | replace(' ', '-') | e }}-parameters">
Parameters:<a href="#{{ detail.name | lower() | replace(' ', '-') | e }}-parameters" class="headerlink" title="Permanent link"></a>
</h4>
<ul>
{% for parameter in detail.parameters %}
<li>
{% for attribute in parameter.attribute %}
<api__{{- attribute -}}></api__{{- attribute -}}>
{% endfor %}
<code>{{ parameter.type | e }}: {{ parameter.name }}</code>{% if parameter.description %} - {{ parameter.description }}{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% if detail.returns %}
<h4 id="{{ detail.name | lower() | replace(' ', '-') | e }}-returns">
Returns:<a href="#{{ detail.name | lower() | replace(' ', '-') | e }}-returns" class="headerlink" title="Permanent link"></a>
</h4>
<p class="api-detail__description">
{{ detail.returns }}
</p>
{% endif %}
{% if detail.throws %}
<h4 id="{{ detail.name | lower() | replace(' ', '-') | e }}-throws">
Throws:<a href="#{{ detail.name | lower() | replace(' ', '-') | e }}-throws" class="headerlink" title="Permanent link"></a>
</h4>
<ul>
{% for throw in detail.throws %}
<li>
<code>{{ throw.name }}</code>{% if throw.description %} - {{ throw.description }}{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% if seealso %}
<h4 id="{{ detail.name | lower() | replace(' ', '-') | e }}-see-also">
See also:<a href="#{{ detail.name | lower() | replace(' ', '-') | e }}-see-also" class="headerlink" title="Permanent link"></a>
</h4>
<ul>
{% for see in detail.seealso %}
<li>
<a href="{{ see.link }}"><code>{{ see.name }}</code></a>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
{%- endmacro %}
@@ -1,61 +0,0 @@
{% if page.meta.constructors %}
## Constructor Details
{% for constructor in page.meta.constructors %}
<div class="api-detail" markdown>
{% set name = namespace(text = constructor.name ~ "(") %}
{% for parameter in constructor.parameters %}
{% set name.text = name.text ~ parameter.type %}
{% if loop.revindex > 1 %}{% set name.text = name.text ~ ", " %}{% endif %}
{% endfor %}
{% set name.text = name.text ~ ")" %}
{% if constructor.deprecated %}
{% set name.text = "<api__deprecated></api__deprecated> " ~ name.text %}
{% endif %}
{% for attribute in constructor.attributes %}
{% set name.text = "<api__{{- attribute -}}></api__{{- attribute -}}> " ~ name.text %}
{% endfor %}
### {{ name.text | e }} { #{{ constructor.name | lower() | replace(' ', '-') | e }} }
{% if constructor.deprecated %}
/// deprecated | Deprecation Warning
{{ constructor.deprecated }}
///
{% endif %}
{% if constructor.description %}
<p class="api-detail__description">
{{ constructor.description }}
</p>
{% endif %}
{% if constructor.parameters %}
#### Parameters: { #{{ constructor.name | lower() | replace(' ', '-') | e }}-parameters }
{% for parameter in constructor.parameters %}
- {% for attribute in parameter.attributes %}<api__{{- attribute -}}></api__{{- attribute -}}>{% endfor %} `{{ parameter.type | e}}: {{ parameter.name }}`{% if parameter.description %} - {{ parameter.description }}{% endif %}
{% endfor %}
{% endif %}
{% if constructor.seealso %}
#### See also: { #{{ constructor.name | lower() | replace(' ', '-') | e }}-see-also }
<div class="api-detail__description" markdown>
{% for see in constructor.seealso %}
- [`{{ see.name }}`]({{ see.link }})
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
@@ -1,14 +0,0 @@
{% if page.meta.constructors %}
## Constructor Summary
| Constructor | Description |
|-------------|-------------|{% for constructor in page.meta.constructors %}
| [`{{ constructor.name }}({% for parameter in constructor.parameters -%}
{{ parameter.type }}{% if loop.revindex > 1 %}, {% endif %}
{%- endfor %})`](#{{ constructor.name | lower() }}) | {% if constructor.deprecated -%}
<api__deprecated></api__deprecated> {{ constructor.deprecated.split("\n")[0] }}
{%- elif constructor.description -%}
{{ constructor.description.split("\n")[0] }}
{%- endif %} |
{%- endfor %}
{% endif %}
-32
View File
@@ -1,32 +0,0 @@
{% if page.meta.enums %}
## Enum Constant Details
{% for enum in page.meta.enums %}
<div class="api-detail" markdown>
{% set modifiers = namespace(text = "<api__static></api__static> <api__final></api__final> ") %}
{% if enum.deprecated %}
{% set modifiers.text = "<api__deprecated></api__deprecated> " ~ modifiers.text %}
{% endif %}
### {{ modifiers.text }} `{{ enum.name | upper() }}` { #{{ enum.name | lower() | replace(' ', '-') | e }} }
{% if enum.deprecated %}
/// deprecated | Deprecation Warning
{{ enum.deprecated }}
///
{% endif %}
{% if enum.description %}
<div class="api-detail__description" markdown>
{{ enum.description }}
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
@@ -1,8 +0,0 @@
{% if page.meta.enums %}
## Enum Constant Summary
| Enum | Description |
|------|-------------|{% for enum in page.meta.enums %}
| [`{{ enum.name | upper() }}`](#{{ enum.name | lower() }}) | {{ enum.description.split("\n")[0] }} |
{%- endfor %}
{% endif %}
-3
View File
@@ -1,3 +0,0 @@
{% macro render_name(method) -%}
{{ method.name }}({% for parameter in method.parameters %}{{ parameter.type }}{% if loop.revindex > 1 %}, {% endif %}{% endfor %})
{%- endmacro %}
-109
View File
@@ -1,109 +0,0 @@
{% if page.meta.methods %}
## Method Details
{% for method in page.meta.methods %}
<div class="api-detail" markdown>
{% set modifiers = namespace(text = "") %}
{% if method.deprecated %}
{% set modifiers.text = "<api__deprecated></api__deprecated> " %}
{% endif %}
{% for attribute in method.attributes %}
{% set modifiers.text = modifiers.text ~ "<api__" ~ attribute ~ "></api__" ~ attribute ~ ">" %}
{% if loop.revindex > 0 %}{% set modifiers.text = modifiers.text ~ " " %}{% endif %}
{% endfor %}
{% if modifiers.text %}{% set modifiers.text = modifiers.text ~ " " %}{% endif %}
{% set method_type = "" %}
{% if method.type %}
{% if method.type.link %}
{% set method_type = '[' ~ method.type.name|default('void')|e ~ '](' ~ method.type.link ~ '){ .api-type__' ~ method.type.type|default('primitive') ~ '} ' %}
{% else %}
{% set method_type = '<span class="api-type__' ~ method.type.type|default('primitive') ~ '">' ~ method.type.name|default('void')|e ~ '</span> ' %}
{% endif %}
{% elif page.meta.defaults and page.meta.defaults.type %}
{% if page.meta.defaults.type.link %}
{% set method_type = '[' ~ page.meta.defaults.type.name|default('void')|e ~ '](' ~ page.meta.defaults.type.link ~ '){ .api-type__' ~ page.meta.defaults.type.type|default('primitive') ~ '} ' %}
{% else %}
{% set method_type = '<span class="api-type__' ~ page.meta.defaults.type.type|default('primitive') ~ '">' ~ page.meta.defaults.type.name|default('void')|e ~ '</span> ' %}
{% endif %}
{% else %}
{% set method_type = '<span class="api-type__primitive">void</span> ' %}
{% endif %}
{% set name = namespace(text = method.name ~ "(") %}
{% for parameter in method.parameters %}
{% set name.text = name.text ~ parameter.type %}
{% if loop.revindex > 1 %}{% set name.text = name.text ~ ", " %}{% endif %}
{% endfor %}
{% set name.text = name.text ~ ")" %}
### {{ modifiers.text }} {{ method_type }} `{{ name.text }}` { #{{ method.name | lower() | replace(' ', '-') | e }} }
{% if method.deprecated %}
/// deprecated | Deprecation Warning
{{ method.deprecated }}
///
{% endif %}
{% if method.description %}
<div class="api-detail__description" markdown>
{{ method.description }}
</div>
{% endif %}
{% if method.parameters %}
#### Parameters: { #{{ method.name | lower() | replace(' ', '-') | e }}-parameters }
<div class="api-detail__description" markdown>
{% for parameter in method.parameters %}
- {% for attribute in parameter.attributes %}<api__{{- attribute -}}></api__{{- attribute -}}>{% endfor %} `{{ parameter.type | e}}: {{ parameter.name }}`{% if parameter.description %} - {{ parameter.description }}{% endif %}
{% endfor %}
</div>
{% endif %}
{% if method.returns %}
#### Returns: { #{{ method.name | lower() | replace(' ', '-') | e }}-returns }
<div class="api-detail__description" markdown>
{{ method.returns }}
</div>
{% endif %}
{% if method.throws %}
#### Throws: { #{{ method.name | lower() | replace(' ', '-') | e }}-throws }
<div class="api-detail__description" markdown>
{% for throw in method.throws %}
- `{{ throw.name }}`{% if throw.description %} - {{ throw.description }}{% endif %}
{% endfor %}
</div>
{% endif %}
{% if method.seealso %}
#### See also: { #{{ method.name | lower() | replace(' ', '-') | e }}-see-also }
<div class="api-detail__description" markdown>
{% for see in method.seealso %}
- [`{{ see.name }}`]({{ see.link }})
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
@@ -1,31 +0,0 @@
{% if page.meta.methods or page.meta.inherits %}
## Method Summary
{% if page.meta.methods %}
| Modifier and Type | Method | Description |
|-------------------|--------|-------------|{% for method in page.meta.methods %}
| `{% if method.attributes -%}
{% for attribute in method.attributes -%}
{{ attribute }}{% if loop.revindex > 0 %} {% endif %}
{%- endfor %} {%- endif %}{% if method.type and method.type.name -%}
{{ method.type.name }}
{%- elif page.meta.defaults and page.meta.defaults.type and page.meta.defaults.type.name -%}
{{ page.meta.defaults.type.name }}
{%- else -%}void{%- endif %}` | [`{{ method.name }}({% for parameter in method.parameters -%}
{{ parameter.type }}{% if loop.revindex > 1 %}, {% endif %}
{%- endfor %})`](#{{ method.name | lower() }}) | {% if method.deprecated -%}
<api__deprecated></api__deprecated> {{ method.deprecated.split("\n")[0] }}
{%- elif method.description -%}
{{ method.description.split("\n")[0] }}
{%- endif %} |
{%- endfor %}
{% endif %}
{% if page.meta.inherits %}
{% for classpath, inherit in page.meta.inherits.items() %}
| Methods inherited from [`{{ classpath }}`]({{ inherit.link }}) |
|----------------------------------------------------------------|
| {% for entry in inherit.list -%}[`{{ entry.name | e }}`]({{ inherit.link }}#{{ entry.link | lower() | replace(' ', '-') | e }}){% if loop.revindex > 1 %}, {% endif %}{% endfor %} |
{% endfor %}
{% endif %}
{% endif %}
@@ -1,11 +0,0 @@
{% if page.meta.classes %}
## Nested Class Summary
| Modifier and Type | Class | Description |
|-------------------|-------|-------------|{% for class in page.meta.classes %}
| `{% if class.attributes -%}
{% for attribute in class.attributes -%}
{{ attribute }}{% if loop.revindex > 1 %} {% endif %}
{%- endfor %} {%- endif %}{{ class.type }}` | [`{{ class.name }}`]({{ class.link }}) | {{ class.description.split("\n")[0] }} |
{%- endfor %}
{% endif %}
+3 -1
View File
@@ -1,5 +1,7 @@
{% extends "base.html" %}
{% block announce %}
<span class="rainbow"><span class="balloon twemoji">{% include ".icons/material/balloon.svg" %}</span> <b>100 THOUSAND downloads</b> on <a class="modrinth" href="https://modrinth.com/plugin/advancedserverlist" target="_blank" rel="nofollow"><span class="twemoji">{% include ".icons/simple/modrinth.svg" %}</span> <b>Modrinth!</b></a> <span class="balloon2 twemoji">{% include ".icons/material/balloon.svg" %}</span><br><b>THANK YOU!</b></span>
<span class="twemoji">{% include ".icons/material/party-popper.svg" %}</span>
Celebrating <a href="https://andre601.ch/blog/2024/12-13-history-of-asl/" target="_blank" rel="nofollow"><b>25,000 downloads!</b></a> Thank you!
<span class="twemoji">{% include ".icons/material/party-popper.svg" %}</span>
{% endblock %}
+1 -1
View File
@@ -1,6 +1,6 @@
[discord]: https://discord.gg/MyPTjpsgGH
[placeholderapi]: https://modrinth.com/plugin/PlaceholderAPI
[placeholderapi]: https://hangar.papermc.io/HelpChat/PlaceholderAPI
[viaversion]: https://hangar.papermc.io/ViaVersion/ViaVersion
[papiproxybridge]: https://hangar.papermc.io/William278/PAPIProxyBridge
[maintenance]: https://hangar.papermc.io/kennytv/Maintenance
@@ -16,7 +16,8 @@ The BanPlugins Addon is available in downloads of AdvancedServerList on [:simple
**Supported Platforms:**
<!-- icon:paper --> <!-- icon:bungee -->
- :fontawesome-solid-paper-plane: Paper
- :simple-spigotmc: BungeeCord
| Placeholder | Description |
|---------------------------------|------------------------------------------------------------------------------------------|
@@ -56,7 +57,9 @@ motd:
**Supported Platforms:**
<!-- icon:all -->
- :fontawesome-solid-paper-plane: Paper
- :simple-spigotmc: BungeeCord
- :fontawesome-solid-paper-plane: Velocity
| Placeholder | Description |
|---------------------------------|------------------------------------------------------------------------------------------|
@@ -92,7 +95,9 @@ motd:
**Supported Platforms:**
<!-- icon:all -->
- :fontawesome-solid-paper-plane: Paper
- :simple-spigotmc: BungeeCord
- :fontawesome-solid-paper-plane: Velocity
| Placeholder | Description |
|------------------------------|------------------------------------------------------------------------------------------|
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff. Show more