Anti-Spam roots have been created!
This commit is contained in:
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
group = 'io.github.thetrouper'
|
||||
version = '0.0.1'
|
||||
version = '0.0.2'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
@@ -7,9 +7,11 @@ package io.github.thetrouper.sentinel;
|
||||
import io.github.thetrouper.sentinel.commands.InfoCommand;
|
||||
import io.github.thetrouper.sentinel.commands.ReopCommand;
|
||||
import io.github.thetrouper.sentinel.data.Config;
|
||||
import io.github.thetrouper.sentinel.events.ChatEvent;
|
||||
import io.github.thetrouper.sentinel.events.CmdBlockEvents;
|
||||
import io.github.thetrouper.sentinel.events.CommandEvent;
|
||||
import io.github.thetrouper.sentinel.events.NBTEvents;
|
||||
import io.github.thetrouper.sentinel.server.functions.AntiSpam;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -56,6 +58,24 @@ public final class Sentinel extends JavaPlugin {
|
||||
// Plugin startup logic
|
||||
loadConfiguration();
|
||||
log.info("Sentinel has loaded! (" + getDescription().getVersion() + ")");
|
||||
// Enable Functions
|
||||
AntiSpam.enableAntiSpam();
|
||||
|
||||
prefix = Config.Plugin.getPrefix();
|
||||
|
||||
// Commands -> BE SURE TO REGISTER ANY NEW COMMANDS IN PLUGIN.YML (src/main/java/resources/plugin.yml)!
|
||||
getCommand("sentinel").setExecutor(new InfoCommand());
|
||||
getCommand("sentinel").setTabCompleter(new InfoCommand());
|
||||
getCommand("reop").setExecutor(new ReopCommand());
|
||||
|
||||
// Events
|
||||
manager.registerEvents(new CommandEvent(),this);
|
||||
manager.registerEvents(new CmdBlockEvents(), this);
|
||||
manager.registerEvents(new NBTEvents(), this);
|
||||
manager.registerEvents(new ChatEvent(),this);
|
||||
|
||||
// Scheduled timers
|
||||
Bukkit.getScheduler().runTaskTimer(this, AntiSpam::decayHeat,0, 20);
|
||||
log.info("\n" +
|
||||
" ____ __ ___ \n" +
|
||||
"/\\ _`\\ /\\ \\__ __ /\\_ \\ \n" +
|
||||
@@ -64,18 +84,7 @@ public final class Sentinel extends JavaPlugin {
|
||||
" /\\ \\L\\ \\/\\ __//\\ \\/\\ \\ \\ \\_\\ \\ \\/\\ \\/\\ \\/\\ __/ \\_\\ \\_ \n" +
|
||||
" \\ `\\____\\ \\____\\ \\_\\ \\_\\ \\__\\\\ \\_\\ \\_\\ \\_\\ \\____\\/\\____\\\n" +
|
||||
" \\/_____/\\/____/\\/_/\\/_/\\/__/ \\/_/\\/_/\\/_/\\/____/\\/____/\n" +
|
||||
" ]======------ Advanced Anti-Grief ------======[");
|
||||
prefix = Config.Plugin.getPrefix();
|
||||
|
||||
// Commands -> BE SURE TO REGISTER ANY NEW COMMANDS IN PLUGIN.YML (src/main/java/resources/plugin.yml)!
|
||||
getCommand("sentinel").setExecutor(new InfoCommand());
|
||||
getCommand("sentinel").setTabCompleter(new InfoCommand.Tabs());
|
||||
getCommand("reop").setExecutor(new ReopCommand());
|
||||
|
||||
// Events
|
||||
manager.registerEvents(new CommandEvent(),this);
|
||||
manager.registerEvents(new CmdBlockEvents(), this);
|
||||
manager.registerEvents(new NBTEvents(), this);
|
||||
" ]====---- Advanced Anti-Grief & Chat Filter ----====[");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +127,7 @@ public final class Sentinel extends JavaPlugin {
|
||||
dangerousCommands = config.getStringList("config.plugin.dangerous");
|
||||
|
||||
// Load log protected commands
|
||||
logDangerousCommands = config.getBoolean("config.plugin.log-protected");
|
||||
logDangerousCommands = config.getBoolean("config.plugin.log-dangerous");
|
||||
|
||||
// Load logged commands
|
||||
loggedCommands = config.getStringList("config.plugin.logged");
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
|
||||
package io.github.thetrouper.sentinel.commands;
|
||||
|
||||
import io.github.thetrouper.sentinel.discord.WebhookSender;
|
||||
import io.github.thetrouper.sentinel.exceptions.CmdExHandler;
|
||||
import io.github.thetrouper.sentinel.server.functions.AntiSpam;
|
||||
import io.github.thetrouper.sentinel.server.util.TextUtils;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.command.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -17,33 +16,45 @@ import java.util.List;
|
||||
/**
|
||||
* Example command
|
||||
*/
|
||||
public class InfoCommand implements CommandExecutor {
|
||||
|
||||
public class InfoCommand implements TabExecutor {
|
||||
public static boolean debugmode;
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
try {
|
||||
sender.sendMessage(TextUtils.prefix("This server is running Sentinel by TheTrouper. Hot reloading is not advised."));
|
||||
if (args.length == 0) {
|
||||
sender.sendMessage(TextUtils.prefix("&cYou must specify an item to give."));
|
||||
return true;
|
||||
}
|
||||
switch (args[0]) {
|
||||
case "debugmode" -> {
|
||||
debugmode = !debugmode;
|
||||
sender.sendMessage(TextUtils.prefix("Debug mode set to §b" + debugmode));
|
||||
}
|
||||
case "webhooktest" -> {
|
||||
sender.sendMessage(TextUtils.prefix("Testing the webhook..."));
|
||||
WebhookSender.sendEmbedWarning(sender.getName(), "/sentinel webhooktest",true,true,false);
|
||||
WebhookSender.sendHelloWorldEmbed();
|
||||
}
|
||||
case "checkheat" -> {
|
||||
sender.sendMessage(TextUtils.prefix("Your heat is §e" + AntiSpam.heatMap.get(sender).toString()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
CmdExHandler handler = new CmdExHandler(ex,command);
|
||||
sender.sendMessage(handler.getErrorMessage());
|
||||
ex.printStackTrace();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example command's tab completer
|
||||
*/
|
||||
public static class Tabs implements TabCompleter {
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
List<String> list = new ArrayList<>();
|
||||
switch (args.length) {
|
||||
case 1 -> list.add("reload");
|
||||
}
|
||||
list.removeIf(s -> !s.toLowerCase().contains(args[args.length - 1].toLowerCase()));
|
||||
return list;
|
||||
}
|
||||
return new TabComplBuilder(sender,command,alias,args)
|
||||
.add(1,new String[]{
|
||||
"debugmode",
|
||||
"webhooktest",
|
||||
"checkheat"
|
||||
}).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.github.thetrouper.sentinel.commands;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class TabComplBuilder {
|
||||
|
||||
private Map<Integer,List<String>> entries = new HashMap<>();
|
||||
private final CommandSender sender;
|
||||
private final Command command;
|
||||
private final String alias;
|
||||
private final String[] args;
|
||||
|
||||
public TabComplBuilder(CommandSender sender, Command command, String alias, String[] args) {
|
||||
this.sender = sender;
|
||||
this.command = command;
|
||||
this.alias = alias;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to the tab completion
|
||||
* @param atIndex should be a number above or equal to 1
|
||||
* @param entry string array
|
||||
* @param condition condition
|
||||
*/
|
||||
public TabComplBuilder add(int atIndex, String[] entry, boolean condition) {
|
||||
if (condition) add(atIndex,entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to the tab completion
|
||||
* @param atIndex should be a number above or equal to 1
|
||||
* @param entry string array
|
||||
*/
|
||||
public TabComplBuilder add(int atIndex, String[] entry) {
|
||||
atIndex = Math.max(1,atIndex);
|
||||
entries.put(atIndex,Arrays.stream(entry).toList());
|
||||
return this;
|
||||
}
|
||||
|
||||
public TabComplBuilder add(int atIndex, List<String> entry, boolean condition) {
|
||||
if (condition) add(atIndex,entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TabComplBuilder add(int atIndex, List<String> entry) {
|
||||
entries.put(atIndex,entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> build() {
|
||||
List<String> list = new ArrayList<>(entries.get(args.length) != null ? entries.get(args.length) : new ArrayList<>());
|
||||
list.removeIf(s -> !s.toLowerCase().contains(args[args.length - 1].toLowerCase()));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.github.thetrouper.sentinel.data;
|
||||
|
||||
public class Emojis {
|
||||
public static String success = "<:success:1125240412081238066>";
|
||||
public static String failure = "<:failure:1125241087909429369>";
|
||||
public static String rightArrow = "<:rightArrow:1125241843597189160>";
|
||||
public static String nuke = "<:nuke:1125244368807280702>";
|
||||
public static String member = "<:member:1125244044407218176>";
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package io.github.thetrouper.sentinel.discord;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.awt.Color;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Array;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Class used to execute Discord Webhooks with low effort
|
||||
*/
|
||||
public class DiscordWebhook {
|
||||
|
||||
private final String url;
|
||||
private String content;
|
||||
private String username;
|
||||
private String avatarUrl;
|
||||
private boolean tts;
|
||||
private List<EmbedObject> embeds = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs a new DiscordWebhook instance
|
||||
*
|
||||
* @param url The webhook URL obtained in Discord
|
||||
*/
|
||||
public DiscordWebhook(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public void setTts(boolean tts) {
|
||||
this.tts = tts;
|
||||
}
|
||||
|
||||
public void addEmbed(EmbedObject embed) {
|
||||
this.embeds.add(embed);
|
||||
}
|
||||
|
||||
public void execute() throws IOException {
|
||||
if (this.content == null && this.embeds.isEmpty()) {
|
||||
throw new IllegalArgumentException("Set content or add at least one EmbedObject");
|
||||
}
|
||||
|
||||
JSONObject json = new JSONObject();
|
||||
|
||||
json.put("content", this.content);
|
||||
json.put("username", this.username);
|
||||
json.put("avatar_url", this.avatarUrl);
|
||||
json.put("tts", this.tts);
|
||||
|
||||
if (!this.embeds.isEmpty()) {
|
||||
List<JSONObject> embedObjects = new ArrayList<>();
|
||||
|
||||
for (EmbedObject embed : this.embeds) {
|
||||
JSONObject jsonEmbed = new JSONObject();
|
||||
|
||||
jsonEmbed.put("title", embed.getTitle());
|
||||
jsonEmbed.put("description", embed.getDescription());
|
||||
jsonEmbed.put("url", embed.getUrl());
|
||||
|
||||
if (embed.getColor() != null) {
|
||||
Color color = embed.getColor();
|
||||
int rgb = color.getRed();
|
||||
rgb = (rgb << 8) + color.getGreen();
|
||||
rgb = (rgb << 8) + color.getBlue();
|
||||
|
||||
jsonEmbed.put("color", rgb);
|
||||
}
|
||||
|
||||
EmbedObject.Footer footer = embed.getFooter();
|
||||
EmbedObject.Image image = embed.getImage();
|
||||
EmbedObject.Thumbnail thumbnail = embed.getThumbnail();
|
||||
EmbedObject.Author author = embed.getAuthor();
|
||||
List<EmbedObject.Field> fields = embed.getFields();
|
||||
|
||||
if (footer != null) {
|
||||
JSONObject jsonFooter = new JSONObject();
|
||||
|
||||
jsonFooter.put("text", footer.getText());
|
||||
jsonFooter.put("icon_url", footer.getIconUrl());
|
||||
jsonEmbed.put("footer", jsonFooter);
|
||||
}
|
||||
|
||||
if (image != null) {
|
||||
JSONObject jsonImage = new JSONObject();
|
||||
|
||||
jsonImage.put("url", image.getUrl());
|
||||
jsonEmbed.put("image", jsonImage);
|
||||
}
|
||||
|
||||
if (thumbnail != null) {
|
||||
JSONObject jsonThumbnail = new JSONObject();
|
||||
|
||||
jsonThumbnail.put("url", thumbnail.getUrl());
|
||||
jsonEmbed.put("thumbnail", jsonThumbnail);
|
||||
}
|
||||
|
||||
if (author != null) {
|
||||
JSONObject jsonAuthor = new JSONObject();
|
||||
|
||||
jsonAuthor.put("name", author.getName());
|
||||
jsonAuthor.put("url", author.getUrl());
|
||||
jsonAuthor.put("icon_url", author.getIconUrl());
|
||||
jsonEmbed.put("author", jsonAuthor);
|
||||
}
|
||||
|
||||
List<JSONObject> jsonFields = new ArrayList<>();
|
||||
for (EmbedObject.Field field : fields) {
|
||||
JSONObject jsonField = new JSONObject();
|
||||
|
||||
jsonField.put("name", field.getName());
|
||||
jsonField.put("value", field.getValue());
|
||||
jsonField.put("inline", field.isInline());
|
||||
|
||||
jsonFields.add(jsonField);
|
||||
}
|
||||
|
||||
jsonEmbed.put("fields", jsonFields.toArray());
|
||||
embedObjects.add(jsonEmbed);
|
||||
}
|
||||
|
||||
json.put("embeds", embedObjects.toArray());
|
||||
}
|
||||
|
||||
URL url = new URL(this.url);
|
||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||
connection.addRequestProperty("Content-Type", "application/json");
|
||||
connection.addRequestProperty("User-Agent", "Java-DiscordWebhook-BY-Gelox_");
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod("POST");
|
||||
|
||||
OutputStream stream = connection.getOutputStream();
|
||||
stream.write(json.toString().getBytes());
|
||||
stream.flush();
|
||||
stream.close();
|
||||
|
||||
connection.getInputStream().close(); //I'm not sure why but it doesn't work without getting the InputStream
|
||||
connection.disconnect();
|
||||
}
|
||||
|
||||
public static class EmbedObject {
|
||||
private String title;
|
||||
private String description;
|
||||
private String url;
|
||||
private Color color;
|
||||
|
||||
private Footer footer;
|
||||
private Thumbnail thumbnail;
|
||||
private Image image;
|
||||
private Author author;
|
||||
private List<Field> fields = new ArrayList<>();
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public Footer getFooter() {
|
||||
return footer;
|
||||
}
|
||||
|
||||
public Thumbnail getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public Author getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public List<Field> getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
public EmbedObject setTitle(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbedObject setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbedObject setUrl(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbedObject setColor(Color color) {
|
||||
this.color = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbedObject setFooter(String text, String icon) {
|
||||
this.footer = new Footer(text, icon);
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbedObject setThumbnail(String url) {
|
||||
this.thumbnail = new Thumbnail(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbedObject setImage(String url) {
|
||||
this.image = new Image(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbedObject setAuthor(String name, String url, String icon) {
|
||||
this.author = new Author(name, url, icon);
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbedObject addField(String name, String value, boolean inline) {
|
||||
this.fields.add(new Field(name, value, inline));
|
||||
return this;
|
||||
}
|
||||
|
||||
private class Footer {
|
||||
private String text;
|
||||
private String iconUrl;
|
||||
|
||||
private Footer(String text, String iconUrl) {
|
||||
this.text = text;
|
||||
this.iconUrl = iconUrl;
|
||||
}
|
||||
|
||||
private String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
private String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
}
|
||||
|
||||
private class Thumbnail {
|
||||
private String url;
|
||||
|
||||
private Thumbnail(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
private String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
private class Image {
|
||||
private String url;
|
||||
|
||||
private Image(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
private String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
private class Author {
|
||||
private String name;
|
||||
private String url;
|
||||
private String iconUrl;
|
||||
|
||||
private Author(String name, String url, String iconUrl) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.iconUrl = iconUrl;
|
||||
}
|
||||
|
||||
private String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
private String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
}
|
||||
|
||||
private class Field {
|
||||
private String name;
|
||||
private String value;
|
||||
private boolean inline;
|
||||
|
||||
private Field(String name, String value, boolean inline) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.inline = inline;
|
||||
}
|
||||
|
||||
private String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean isInline() {
|
||||
return inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class JSONObject {
|
||||
|
||||
private final HashMap<String, Object> map = new HashMap<>();
|
||||
|
||||
void put(String key, Object value) {
|
||||
if (value != null) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
Set<Map.Entry<String, Object>> entrySet = map.entrySet();
|
||||
builder.append("{");
|
||||
|
||||
int i = 0;
|
||||
for (Map.Entry<String, Object> entry : entrySet) {
|
||||
Object val = entry.getValue();
|
||||
builder.append(quote(entry.getKey())).append(":");
|
||||
|
||||
if (val instanceof String) {
|
||||
builder.append(quote(String.valueOf(val)));
|
||||
} else if (val instanceof Integer) {
|
||||
builder.append(Integer.valueOf(String.valueOf(val)));
|
||||
} else if (val instanceof Boolean) {
|
||||
builder.append(val);
|
||||
} else if (val instanceof JSONObject) {
|
||||
builder.append(val.toString());
|
||||
} else if (val.getClass().isArray()) {
|
||||
builder.append("[");
|
||||
int len = Array.getLength(val);
|
||||
for (int j = 0; j < len; j++) {
|
||||
builder.append(Array.get(val, j).toString()).append(j != len - 1 ? "," : "");
|
||||
}
|
||||
builder.append("]");
|
||||
}
|
||||
|
||||
builder.append(++i == entrySet.size() ? "}" : ",");
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String quote(String string) {
|
||||
return "\"" + string + "\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package io.github.thetrouper.sentinel.discord;
|
||||
|
||||
public class WebHook {
|
||||
// To be implemented once I have learned how to use JDA
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package io.github.thetrouper.sentinel.discord;
|
||||
|
||||
import io.github.thetrouper.sentinel.Sentinel;
|
||||
import io.github.thetrouper.sentinel.commands.InfoCommand;
|
||||
import io.github.thetrouper.sentinel.data.Emojis;
|
||||
import io.github.thetrouper.sentinel.discord.DiscordWebhook;
|
||||
import io.github.thetrouper.sentinel.server.util.ServerUtils;
|
||||
import io.github.thetrouper.sentinel.server.util.TextUtils;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.IOException;
|
||||
|
||||
public class WebhookSender {
|
||||
|
||||
public static void sendHelloWorldEmbed() {
|
||||
String webhookUrl = Sentinel.webhook;
|
||||
|
||||
// Create a new DiscordWebhook instance
|
||||
DiscordWebhook webhook = new DiscordWebhook(webhookUrl);
|
||||
|
||||
// Create an EmbedObject and set its properties
|
||||
DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject()
|
||||
.setDescription("Hello, World!")
|
||||
.setColor(Color.GREEN);
|
||||
|
||||
// Add the EmbedObject to the webhook
|
||||
webhook.addEmbed(embed);
|
||||
|
||||
try {
|
||||
// Execute the webhook
|
||||
webhook.execute();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static String successOrFail(boolean bool) {
|
||||
if (bool) {
|
||||
return Emojis.success;
|
||||
} else {
|
||||
return Emojis.failure;
|
||||
}
|
||||
}
|
||||
public static void sendEmbedWarning(String player, String command, boolean denied, boolean removedOp, boolean banned) {
|
||||
ServerUtils.sendDebugMessage("Creating Command Webhook...");
|
||||
final String description =
|
||||
Emojis.rightArrow + " **Player:** " + player + " " + Emojis.member + "\\n" +
|
||||
Emojis.rightArrow + " **Command:** " + command + " " + Emojis.nuke + "\\n" +
|
||||
Emojis.rightArrow + " **Denied:** " + successOrFail(denied) + "\\n" +
|
||||
Emojis.rightArrow + " **Removed OP:** " + successOrFail(removedOp) + "\\n" +
|
||||
Emojis.rightArrow + " **Banned:** " + successOrFail(banned) + "\\n";
|
||||
|
||||
DiscordWebhook webhook = new DiscordWebhook(Sentinel.webhook);
|
||||
webhook.setAvatarUrl("https://r2.e-z.host/d440b58a-ba90-4839-8df6-8bba298cf817/3lwit5nt.png");
|
||||
webhook.setUsername("Sentinel Anti-Nuke | Logs");
|
||||
DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject()
|
||||
.setAuthor("Anti-Nuke has been triggered","","")
|
||||
.setTitle("The use of a dangerous command has been detected!")
|
||||
.setDescription(description)
|
||||
.setColor(Color.RED);
|
||||
webhook.addEmbed(embed);
|
||||
try {
|
||||
ServerUtils.sendDebugMessage("Executing webhook...");
|
||||
webhook.execute();
|
||||
} catch (IOException e) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Epic webhook failure!!!"));
|
||||
Sentinel.log.info(e.toString());
|
||||
}
|
||||
}
|
||||
public static void sendEmbedWarning(String player, Block b, boolean denied, boolean removedOp, boolean banned) {
|
||||
ServerUtils.sendDebugMessage("Creating Block Webhook...");
|
||||
final String description =
|
||||
Emojis.rightArrow + " **Player:** " + player + " " + Emojis.member + "\\n" +
|
||||
Emojis.rightArrow + " **Block:** " + b.getType() + " " + Emojis.nuke + "\\n" +
|
||||
Emojis.rightArrow + " **Denied:** " + successOrFail(denied) + "\\n" +
|
||||
Emojis.rightArrow + " **Removed OP:** " + successOrFail(removedOp) + "\\n" +
|
||||
Emojis.rightArrow + " **Banned:** " + successOrFail(banned) + "\\n";
|
||||
|
||||
DiscordWebhook webhook = new DiscordWebhook(Sentinel.webhook);
|
||||
webhook.setAvatarUrl("https://r2.e-z.host/d440b58a-ba90-4839-8df6-8bba298cf817/3lwit5nt.png");
|
||||
webhook.setUsername("Sentinel Anti-Nuke | Logs");
|
||||
DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject()
|
||||
.setAuthor("Anti-Nuke has been triggered","","")
|
||||
.setTitle("The use of a dangerous block has been detected!")
|
||||
.setDescription(description)
|
||||
.setColor(Color.RED);
|
||||
webhook.addEmbed(embed);
|
||||
try {
|
||||
ServerUtils.sendDebugMessage("Executing webhook...");
|
||||
webhook.execute();
|
||||
} catch (IOException e) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Epic webhook failure!!!"));
|
||||
Sentinel.log.info(e.toString());
|
||||
}
|
||||
|
||||
}
|
||||
public static void sendEmbedWarning(String player, ItemStack item, boolean denied, boolean removedOp, boolean banned) {
|
||||
ServerUtils.sendDebugMessage("Creating Webhook...");
|
||||
final String description =
|
||||
Emojis.rightArrow + " **Player:** " + player + " " + Emojis.member + "\\n" +
|
||||
Emojis.rightArrow + " **Item:** " + item.getType() + " " + Emojis.nuke + "\\n" +
|
||||
Emojis.rightArrow + " **Denied:** " + successOrFail(denied) + "\\n" +
|
||||
Emojis.rightArrow + " **Removed OP:** " + successOrFail(removedOp) + "\\n" +
|
||||
Emojis.rightArrow + " **Banned:** " + successOrFail(banned) + "\\n";
|
||||
|
||||
DiscordWebhook webhook = new DiscordWebhook(Sentinel.webhook);
|
||||
webhook.setAvatarUrl("https://r2.e-z.host/d440b58a-ba90-4839-8df6-8bba298cf817/3lwit5nt.png");
|
||||
webhook.setUsername("Sentinel Anti-Nuke | Logs");
|
||||
DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject()
|
||||
.setAuthor("Anti-Nuke has been triggered","","")
|
||||
.setTitle("The use of a dangerous item has been detected!")
|
||||
.setDescription(description)
|
||||
.setColor(Color.BLUE);
|
||||
webhook.addEmbed(embed);
|
||||
try {
|
||||
ServerUtils.sendDebugMessage("Executing webhook...");
|
||||
webhook.execute();
|
||||
} catch (IOException e) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Epic webhook failure!!!"));
|
||||
Sentinel.log.info(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.github.thetrouper.sentinel.events;
|
||||
|
||||
import io.github.thetrouper.sentinel.server.functions.AntiSpam;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
|
||||
public class ChatEvent implements Listener {
|
||||
@EventHandler
|
||||
public static void onChat(AsyncPlayerChatEvent e) {
|
||||
AntiSpam.handleAntiSpam(e);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public class CmdBlockEvents implements Listener {
|
||||
Player p = e.getPlayer();
|
||||
if (!Sentinel.isTrusted(p)) {
|
||||
e.setCancelled(true);
|
||||
Sentinel.log.info("Use of a command block has been denied for " + p.getName() + " at X:" + b.getX() + " Y:" + b.getY() + " Z:" + b.getZ());
|
||||
DeniedActions.handleDeniedAction(p,b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,16 +30,16 @@ public class CmdBlockEvents implements Listener {
|
||||
private void onCMDBlockPlace(BlockPlaceEvent e) {
|
||||
if (!Sentinel.preventCmdBlocks) return;
|
||||
Block b = e.getBlockPlaced();
|
||||
if (b.getType() == Material.COMMAND_BLOCK || b.getType() == Material.REPEATING_COMMAND_BLOCK || b.getType() == Material.CHAIN_COMMAND_BLOCK) {
|
||||
if (b.getType() == Material.COMMAND_BLOCK || b.getType() == Material.CHAIN_COMMAND_BLOCK || b.getType() == Material.REPEATING_COMMAND_BLOCK ) {
|
||||
Player p = e.getPlayer();
|
||||
if (!Sentinel.isTrusted(p)) {
|
||||
e.setCancelled(true);
|
||||
Sentinel.log.info("Placing a command block has been denied for " + p.getName() + " at X:" + b.getX() + " Y:" + b.getY() + " Z:" + b.getZ());
|
||||
DeniedActions.handleDeniedAction(p,b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@EventHandler
|
||||
private void onCMDBlockMinecartPlace(PlayerInteractEntityEvent e) {
|
||||
private void onCMDBlockMinecartUse(PlayerInteractEntityEvent e) {
|
||||
if (!Sentinel.preventCmdBlocks) return;
|
||||
if (e.getRightClicked().getType() == EntityType.MINECART_COMMAND) {
|
||||
Player p = e.getPlayer();
|
||||
|
||||
@@ -2,6 +2,8 @@ package io.github.thetrouper.sentinel.events;
|
||||
|
||||
import io.github.thetrouper.sentinel.Sentinel;
|
||||
import io.github.thetrouper.sentinel.server.util.DeniedActions;
|
||||
import io.github.thetrouper.sentinel.server.util.ServerUtils;
|
||||
import io.github.thetrouper.sentinel.server.util.TextUtils;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
@@ -13,16 +15,22 @@ public class CommandEvent implements Listener {
|
||||
private void onCommand(PlayerCommandPreprocessEvent e) {
|
||||
Player p = e.getPlayer();
|
||||
String command = e.getMessage().substring(1).split(" ")[0];
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Checking command"));
|
||||
if (Sentinel.isDangerousCommand(command)) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix( "Command is dangerous"));
|
||||
if (!Sentinel.isTrusted(p)) {
|
||||
e.setCancelled(true);
|
||||
DeniedActions.handleDeniedAction(p,command);
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Command is canceled"));
|
||||
DeniedActions.handleDeniedAction(p,e.getMessage());
|
||||
}
|
||||
}
|
||||
if (Sentinel.blockSpecificCommands) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Checking command for specific"));
|
||||
if (command.contains(":")) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Checking is specific"));
|
||||
if (!Sentinel.isTrusted(p)) {
|
||||
e.setCancelled(true);
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Command is canceled"));
|
||||
DeniedActions.handleDeniedAction(p,command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.github.thetrouper.sentinel.server.functions;
|
||||
|
||||
import io.github.thetrouper.sentinel.server.util.GPTUtils;
|
||||
import io.github.thetrouper.sentinel.server.util.TextUtils;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static io.github.thetrouper.sentinel.server.util.GPTUtils.calculateSimilarity;
|
||||
|
||||
public class AntiSpam {
|
||||
public static Map<Player, Integer> heatMap;
|
||||
public static Map<Player, String> lastMessageMap;
|
||||
|
||||
public static void enableAntiSpam() {
|
||||
heatMap = new HashMap<>();
|
||||
lastMessageMap = new HashMap<>();
|
||||
}
|
||||
public static void handleAntiSpam(AsyncPlayerChatEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
String message = event.getMessage();
|
||||
if (!heatMap.containsKey(player)) heatMap.put(player, 0);
|
||||
if (heatMap.get(player) > 10) {
|
||||
event.setCancelled(true);
|
||||
player.sendMessage(TextUtils.prefix("Rate limit exceeded! Please wait before sending another message."));
|
||||
return;
|
||||
}
|
||||
if (lastMessageMap.containsKey(player)) {
|
||||
String lastMessage = lastMessageMap.get(player);
|
||||
double similarity = calculateSimilarity(message, lastMessage);
|
||||
if (similarity > 0.5) heatMap.put(player, heatMap.get(player) + 3);
|
||||
if (similarity > 0.9) heatMap.put(player, heatMap.get(player) + 6);
|
||||
}
|
||||
lastMessageMap.put(player, message);
|
||||
}
|
||||
public static void decayHeat() {
|
||||
for (Player player : heatMap.keySet()) {
|
||||
int heat = heatMap.get(player);
|
||||
if (heat > 0) {
|
||||
heatMap.put(player, heat - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.github.thetrouper.sentinel.server.util;
|
||||
|
||||
import io.github.thetrouper.sentinel.Sentinel;
|
||||
import io.github.thetrouper.sentinel.discord.WebhookSender;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
@@ -13,27 +14,42 @@ import org.bukkit.inventory.ItemStack;
|
||||
|
||||
public class DeniedActions {
|
||||
private static String logMessage;
|
||||
private static boolean banned;
|
||||
private static boolean opRemoved;
|
||||
private static boolean denied;
|
||||
|
||||
public static void handleDeniedAction(Player p, String command) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Handling denied command..."));
|
||||
if (!Sentinel.logDangerousCommands) return;
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("LDC is enabled"));
|
||||
logMessage = "]==-- Sentinel --==[\n" +
|
||||
"A Dangerous command has been attempted!\n" +
|
||||
"Player: " + p.getName() + "\n" +
|
||||
"Command: " + command + "\n";
|
||||
if (Sentinel.deop) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Deoping player"));
|
||||
p.setOp(false);
|
||||
logMessage = logMessage + "Operator Removed: ✔\n";
|
||||
opRemoved = true;
|
||||
} else {
|
||||
logMessage = logMessage + "Operator Removed: ✘\n";
|
||||
opRemoved = false;
|
||||
}
|
||||
if (Sentinel.ban) {
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Banning player"));
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ban " + p.getName() + " ]=- Sentinel Anti-Grief -=[ You have been banned for attempting a dangerous command. Contact an administrator if you believe this to be a mistake.");
|
||||
logMessage = logMessage + "Banned: ✔\n";
|
||||
banned = true;
|
||||
} else {
|
||||
logMessage = logMessage + "Banned: ✘\n";
|
||||
banned = false;
|
||||
}
|
||||
ServerUtils.sendDebugMessage(TextUtils.prefix("Sending log"));
|
||||
logMessage = logMessage + "Denied: ✔";
|
||||
denied = true;
|
||||
Sentinel.log.info(logMessage);
|
||||
notifyTrusted(p, command);
|
||||
WebhookSender.sendEmbedWarning(p.getName(),command,denied,opRemoved,banned);
|
||||
}
|
||||
public static void handleDeniedAction(Player p, Block block) {
|
||||
if (!Sentinel.logCmdBlocks) return;
|
||||
@@ -44,18 +60,24 @@ public class DeniedActions {
|
||||
if (Sentinel.deop) {
|
||||
p.setOp(false);
|
||||
logMessage = logMessage + "Operator Removed: ✔\n";
|
||||
opRemoved = true;
|
||||
} else {
|
||||
logMessage = logMessage + "Operator Removed: ✘\n";
|
||||
opRemoved = false;
|
||||
}
|
||||
if (Sentinel.ban) {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ban " + p.getName() + " ]=- Sentinel Anti-Grief -=[ You have been banned for attempting to use dangerous blocks. Contact an administrator if you believe this to be a mistake.");
|
||||
logMessage = logMessage + "Banned: ✔\n";
|
||||
banned = true;
|
||||
} else {
|
||||
logMessage = logMessage + "Banned: ✘\n";
|
||||
banned = false;
|
||||
}
|
||||
logMessage = logMessage + "Denied: ✔";
|
||||
denied = true;
|
||||
Sentinel.log.info(logMessage);
|
||||
notifyTrusted(p, block);
|
||||
WebhookSender.sendEmbedWarning(p.getName(),block,denied,opRemoved,banned);
|
||||
}
|
||||
public static void handleDeniedAction(Player p, ItemStack i) {
|
||||
if (!Sentinel.logNBT) return;
|
||||
@@ -66,18 +88,24 @@ public class DeniedActions {
|
||||
if (Sentinel.deop) {
|
||||
p.setOp(false);
|
||||
logMessage = logMessage + "Operator Removed: ✔\n";
|
||||
opRemoved = true;
|
||||
} else {
|
||||
logMessage = logMessage + "Operator Removed: ✘\n";
|
||||
opRemoved = false;
|
||||
}
|
||||
if (Sentinel.ban) {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ban " + p.getName() + " ]=- Sentinel Anti-Grief -=[ You have been banned for attempting a dangerous command. Contact an administrator if you believe this to be a mistake.");
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ban " + p.getName() + " ]=- Sentinel Anti-Grief -=[ You have been banned for attempting to use an NBT item. Contact an administrator if you believe this to be a mistake.");
|
||||
logMessage = logMessage + "Banned: ✔\n";
|
||||
banned = true;
|
||||
} else {
|
||||
logMessage = logMessage + "Banned: ✘\n";
|
||||
banned = false;
|
||||
}
|
||||
logMessage = logMessage + "Denied: ✔";
|
||||
denied = true;
|
||||
Sentinel.log.info(logMessage);
|
||||
notifyTrusted(p, i);
|
||||
WebhookSender.sendEmbedWarning(p.getName(),i,denied,opRemoved,banned);
|
||||
}
|
||||
private static void notifyTrusted(Player p, String command) {
|
||||
TextComponent message = new TextComponent(TextUtils.prefix(p.getName() + " has attempted a dangerous command!"));
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.github.thetrouper.sentinel.server.util;
|
||||
|
||||
public class GPTUtils {
|
||||
// I'd be surprised if anyone knew how tf this shi works, I just asked GPT to write it.
|
||||
public static double calculateSimilarity(String str1, String str2) {
|
||||
int len1 = str1.length();
|
||||
int len2 = str2.length();
|
||||
|
||||
int[][] dp = new int[len1 + 1][len2 + 1];
|
||||
|
||||
for (int i = 0; i <= len1; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
|
||||
for (int j = 0; j <= len2; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= len1; i++) {
|
||||
for (int j = 1; j <= len2; j++) {
|
||||
if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int maxLen = Math.max(len1, len2);
|
||||
int distance = dp[len1][len2];
|
||||
|
||||
double similarity = ((double) (maxLen - distance) / maxLen) * 100;
|
||||
return similarity;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
package io.github.thetrouper.sentinel.server.util;
|
||||
|
||||
import io.github.thetrouper.sentinel.Sentinel;
|
||||
import io.github.thetrouper.sentinel.commands.InfoCommand;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@@ -17,9 +18,16 @@ import java.util.Set;
|
||||
* Server utils
|
||||
*/
|
||||
public abstract class ServerUtils {
|
||||
|
||||
|
||||
|
||||
public static void sendDebugMessage(String message) {
|
||||
if (InfoCommand.debugmode) {
|
||||
Sentinel.log.info(message);
|
||||
for (Player trustedPlayer : Bukkit.getOnlinePlayers()) {
|
||||
if (Sentinel.isTrusted(trustedPlayer)) {
|
||||
trustedPlayer.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* List of names of online players
|
||||
* @return list of names
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
#
|
||||
#
|
||||
# Configure the plugin here, the default config may not be adequate to your needs.
|
||||
#
|
||||
#
|
||||
config:
|
||||
# Sentinel 0.0.2
|
||||
# ____ __ ___
|
||||
#/\ _`\ /\ \__ __ /\_ \
|
||||
#\ \,\L\_\ __ ___\ \ ,_\/\_\ ___ __\//\ \
|
||||
# \/_\__ \ /'__`\/' _ `\ \ \/\/\ \ /' _ `\ /'__`\\ \ \
|
||||
# /\ \L\ \/\ __//\ \/\ \ \ \_\ \ \/\ \/\ \/\ __/ \_\ \_
|
||||
# \ `\____\ \____\ \_\ \_\ \__\\ \_\ \_\ \_\ \____\/\____\
|
||||
# \/_____/\/____/\/_/\/_/\/__/ \/_/\/_/\/_/\/____/\/____/
|
||||
# ]======------ Configuration & Setup Guide ------=====[
|
||||
# Sentinel is inspired by WickBot.com
|
||||
# Be sure to check out their amazing discord bot!
|
||||
config :
|
||||
plugin:
|
||||
# -------------------------------
|
||||
# Important Setup (Do this first)
|
||||
# -------------------------------
|
||||
prefix: "§d§lSentinel §8» §7" # Prefix of the plugin. Line below is the discord webhook for logs to be sent to
|
||||
webhook: "https://discord.com/api/webhooks/1124908469842096211/https://discord.com/api/webhooks/1124908469842096211/7NGOeFvtmxQ4n0_hSvbqhZUjnzRHIicLpHKETYU92n9JaLUPPsueBSn7w4wUfAnhjlLF"
|
||||
webhook: "https://discord.com/api/webhooks/1124908469842096211/7NGOeFvtmxQ4n0_hSvbqhZUjnzRHIicLpHKETYU92n9JaLUPPsueBSn7w4wUfAnhjlLF"
|
||||
trusted: # List the UUIDs of players who are trusted, will bypass the plugin and be immune to logs and are able to re-op themeselves
|
||||
- "049460f7-21cb-42f5-8059-d42752bf406f" # obvWolf
|
||||
block-specific: true # Defaulted true | Weather or not to block ALL plugin specific commands from non-trusted members (EX: minecraft:ban) these will not be logged.
|
||||
@@ -28,3 +37,13 @@ config:
|
||||
deop: true # Defaulted true | This will remove an untrusted player's operator permissions whenever they attempt dangerous actions
|
||||
ban: false # Defaulted false | This will ban a player when they attempt dangerous actions
|
||||
reop-command: false # Defaulted false | This enables the command allowing trusted players to op themselves if they get deoped.
|
||||
# -------------------------------
|
||||
# Chat Filter Setup & AntiSpam
|
||||
# -------------------------------
|
||||
chat:
|
||||
# AntiSpam Heat system
|
||||
anti-spam: true # Default true | Enables the anti-spam
|
||||
default-gain: 1 # Heat gained as base for every message
|
||||
medium-gain: 3 # Heat gained when your message is 50% similar
|
||||
high-gain: 6 # Heat gained for
|
||||
max-heat: 15 # Highest value of heat a player can reach
|
||||
Reference in New Issue
Block a user