Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fb60e15a3 |
No files matched your search
+2
-2
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'com.gradleup.shadow' version "9.6.1"
|
||||
id 'com.gradleup.shadow' version "8.3.5"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -29,7 +29,7 @@ subprojects {
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
|
||||
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
|
||||
compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ public class ChatMessages extends Appliance {
|
||||
public Component addReportActions(Component message, String username) {
|
||||
return message
|
||||
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um diesen Spieler zu reporten").color(NamedTextColor.GOLD)))
|
||||
.clickEvent(ClickEvent.suggestCommand(String.format("/report %s ", username)));
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, String.format("/report %s ", username)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ public class Report extends Appliance {
|
||||
.append(
|
||||
Component
|
||||
.text(createdReport.data().url(), NamedTextColor.GRAY)
|
||||
.clickEvent(ClickEvent.openUrl(createdReport.data().url()))
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, createdReport.data().url()))
|
||||
)
|
||||
.appendNewline()
|
||||
.appendNewline()
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ public class AntiFormattedBook extends Appliance {
|
||||
|
||||
if (component instanceof TextComponent tc && tc.content().indexOf(SECTION) >= 0) return true;
|
||||
|
||||
if (component instanceof NBTComponent<?> nbt) {
|
||||
if (component instanceof NBTComponent<?, ?> nbt) {
|
||||
if (nbt.separator() != null && this.hasFormattingDeep(nbt.separator())) return true;
|
||||
}
|
||||
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.loginLog;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
record LoginAttempt(
|
||||
@NotNull Instant at,
|
||||
@NotNull UUID uuid,
|
||||
@NotNull String name,
|
||||
@NotNull String ip,
|
||||
boolean bedrock,
|
||||
@NotNull String stage,
|
||||
@NotNull String result,
|
||||
@Nullable String reason
|
||||
) {
|
||||
static final String STAGE_PRE_LOGIN = "PRE_LOGIN";
|
||||
static final String STAGE_LOGIN = "LOGIN";
|
||||
static final String RESULT_ABORTED = "ABORTED";
|
||||
|
||||
static String ipOf(@Nullable InetAddress address) {
|
||||
return address == null ? "unknown" : address.getHostAddress();
|
||||
}
|
||||
|
||||
LoginAttempt withOutcome(@NotNull String stage, @NotNull String result, @Nullable String reason) {
|
||||
return new LoginAttempt(this.at, this.uuid, this.name, this.ip, this.bedrock, stage, result, reason);
|
||||
}
|
||||
}
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.loginLog;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.database.Database;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.geysermc.floodgate.api.FloodgateApi;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Records every login attempt (allowed or not) with its IP address in the table login_attempts
|
||||
* (schema: resources/migrations/loginLog). Basis for detecting ban evasion.
|
||||
* Never blocks or delays a login: writes run on the database thread, failures are only logged.
|
||||
*/
|
||||
public class LoginLog extends Appliance {
|
||||
private static final int MAX_REASON_LENGTH = 255;
|
||||
private static final Duration PENDING_TIMEOUT = Duration.ofSeconds(60);
|
||||
private static final long TICKS_PER_MINUTE = 20L * 60;
|
||||
|
||||
private final Map<UUID, LoginAttempt> pendingAttempts = new ConcurrentHashMap<>();
|
||||
private final List<BukkitTask> tasks = new ArrayList<>();
|
||||
private Database database;
|
||||
private int retentionMonths;
|
||||
|
||||
public LoginLog() {
|
||||
super("loginLog");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
this.database = Main.instance().getDatabase();
|
||||
if(!this.database.isAvailable()) {
|
||||
Main.logger().warning("LoginLog: no database configured, login attempts are not recorded");
|
||||
return;
|
||||
}
|
||||
this.retentionMonths = this.localConfig().getInt("retentionMonths", 16);
|
||||
|
||||
this.tasks.add(Bukkit.getScheduler().runTaskTimerAsynchronously(
|
||||
Main.instance(), this::flushStaleAttempts, TICKS_PER_MINUTE, TICKS_PER_MINUTE
|
||||
));
|
||||
this.tasks.add(Bukkit.getScheduler().runTaskTimerAsynchronously(
|
||||
Main.instance(), this::deleteExpired, TICKS_PER_MINUTE, TICKS_PER_MINUTE * 60 * 24
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
this.tasks.forEach(BukkitTask::cancel);
|
||||
}
|
||||
|
||||
void onPreLogin(UUID uuid, String name, @Nullable InetAddress address, String result, @Nullable Component kickMessage) {
|
||||
if(!this.database.isAvailable()) return;
|
||||
LoginAttempt attempt = new LoginAttempt(
|
||||
Instant.now(), uuid, name, LoginAttempt.ipOf(address), this.isBedrock(uuid),
|
||||
LoginAttempt.STAGE_PRE_LOGIN, result, plainText(kickMessage)
|
||||
);
|
||||
|
||||
// allowed attempts are written after the login event, so they carry the final result
|
||||
if(kickMessage == null) this.pendingAttempts.put(uuid, attempt);
|
||||
else this.write(attempt);
|
||||
}
|
||||
|
||||
void onLogin(UUID uuid, String name, @Nullable InetAddress address, String result, @Nullable Component kickMessage) {
|
||||
if(!this.database.isAvailable()) return;
|
||||
LoginAttempt pending = this.pendingAttempts.remove(uuid);
|
||||
LoginAttempt attempt = pending != null
|
||||
? pending
|
||||
: new LoginAttempt(Instant.now(), uuid, name, LoginAttempt.ipOf(address), this.isBedrock(uuid), "", "", null);
|
||||
|
||||
this.write(attempt.withOutcome(LoginAttempt.STAGE_LOGIN, result, plainText(kickMessage)));
|
||||
}
|
||||
|
||||
private void flushStaleAttempts() {
|
||||
Instant threshold = Instant.now().minus(PENDING_TIMEOUT);
|
||||
this.pendingAttempts.values().removeIf(attempt -> {
|
||||
if(attempt.at().isAfter(threshold)) return false;
|
||||
this.write(attempt.withOutcome(LoginAttempt.STAGE_PRE_LOGIN, LoginAttempt.RESULT_ABORTED, null));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void write(LoginAttempt attempt) {
|
||||
String sql = "INSERT INTO login_attempts (at, uuid, name, ip, bedrock, stage, result, reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
this.database.execute("recording login attempt of " + attempt.name(), connection -> {
|
||||
try(PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setObject(1, LocalDateTime.ofInstant(attempt.at(), ZoneOffset.UTC));
|
||||
statement.setString(2, attempt.uuid().toString());
|
||||
statement.setString(3, attempt.name());
|
||||
statement.setString(4, attempt.ip());
|
||||
statement.setBoolean(5, attempt.bedrock());
|
||||
statement.setString(6, attempt.stage());
|
||||
statement.setString(7, attempt.result());
|
||||
statement.setString(8, attempt.reason());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void deleteExpired() {
|
||||
String sql = "DELETE FROM login_attempts WHERE at < UTC_TIMESTAMP() - INTERVAL ? MONTH";
|
||||
this.database.execute("deleting expired login attempts", connection -> {
|
||||
try(PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setInt(1, this.retentionMonths);
|
||||
int deleted = statement.executeUpdate();
|
||||
if(deleted > 0) Main.logger().info(String.format("LoginLog: deleted %d expired login attempts", deleted));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isBedrock(UUID uuid) {
|
||||
try {
|
||||
return FloodgateApi.getInstance().isFloodgatePlayer(uuid);
|
||||
} catch(Throwable e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable String plainText(@Nullable Component component) {
|
||||
if(component == null) return null;
|
||||
String text = PlainTextComponentSerializer.plainText().serialize(component)
|
||||
.strip()
|
||||
.replaceAll("\\s*\\n\\s*", " | ");
|
||||
return text.length() > MAX_REASON_LENGTH ? text.substring(0, MAX_REASON_LENGTH) : text;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(new LoginLogListener());
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.loginLog;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
|
||||
import org.bukkit.event.player.PlayerLoginEvent;
|
||||
|
||||
/**
|
||||
* Listens on MONITOR priority, so the final result of all other checks (whitelist, bans, maintenance, ...) is recorded.
|
||||
*/
|
||||
class LoginLogListener extends ApplianceListener<LoginLog> {
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPreLogin(AsyncPlayerPreLoginEvent event) {
|
||||
this.getAppliance().onPreLogin(
|
||||
event.getUniqueId(),
|
||||
event.getName(),
|
||||
event.getAddress(),
|
||||
event.getLoginResult().name(),
|
||||
event.getLoginResult() == AsyncPlayerPreLoginEvent.Result.ALLOWED ? null : event.kickMessage()
|
||||
);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onLogin(PlayerLoginEvent event) {
|
||||
this.getAppliance().onLogin(
|
||||
event.getPlayer().getUniqueId(),
|
||||
event.getPlayer().getName(),
|
||||
event.getAddress(),
|
||||
event.getResult().name(),
|
||||
event.getResult() == PlayerLoginEvent.Result.ALLOWED ? null : event.kickMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
-- Every login attempt (allowed or not) with the player's IP, basis for detecting ban evasion.
|
||||
CREATE TABLE login_attempts (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
at DATETIME(3) NOT NULL COMMENT 'UTC',
|
||||
uuid CHAR(36) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
ip VARCHAR(45) NOT NULL,
|
||||
bedrock BOOLEAN NOT NULL,
|
||||
stage VARCHAR(16) NOT NULL COMMENT 'PRE_LOGIN or LOGIN',
|
||||
result VARCHAR(32) NOT NULL COMMENT 'Bukkit login result, or ABORTED',
|
||||
reason VARCHAR(255) NULL COMMENT 'kick message as plain text',
|
||||
KEY idx_at (at),
|
||||
KEY idx_uuid (uuid),
|
||||
KEY idx_ip (ip)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
dependencies {
|
||||
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
|
||||
compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||
|
||||
@@ -4,7 +4,6 @@ import eu.mhsl.craftattack.spawn.core.api.client.RepositoryLoader;
|
||||
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.config.Configuration;
|
||||
import eu.mhsl.craftattack.spawn.core.database.Database;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
@@ -24,7 +23,6 @@ public final class Main extends JavaPlugin {
|
||||
|
||||
private List<Appliance> appliances;
|
||||
private RepositoryLoader repositoryLoader;
|
||||
private Database database;
|
||||
private Reflections reflections;
|
||||
|
||||
@Override
|
||||
@@ -44,8 +42,6 @@ public final class Main extends JavaPlugin {
|
||||
Configuration.readConfig();
|
||||
List<String> disabledAppliances = Configuration.pluginConfig.getStringList("disabledAppliances");
|
||||
|
||||
this.database = new Database(Configuration.cfg.getConfigurationSection("database"));
|
||||
|
||||
Main.logger().info("Initializing reflections...");
|
||||
this.reflections = new Reflections(projectPackage);
|
||||
|
||||
@@ -98,8 +94,6 @@ public final class Main extends JavaPlugin {
|
||||
HandlerList.unregisterAll(this);
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
Main.logger().info("Disabled " + this.appliances.size() + " appliances!");
|
||||
|
||||
if(this.database != null) this.database.close();
|
||||
}
|
||||
|
||||
public Appliance restartAppliance(Class<? extends Appliance> applianceClass) {
|
||||
@@ -144,10 +138,6 @@ public final class Main extends JavaPlugin {
|
||||
return this.appliances;
|
||||
}
|
||||
|
||||
public Database getDatabase() {
|
||||
return this.database;
|
||||
}
|
||||
|
||||
public RepositoryLoader getRepositoryLoader() {
|
||||
return this.repositoryLoader;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.api.client.Repository;
|
||||
import eu.mhsl.craftattack.spawn.core.api.server.HttpServer;
|
||||
import eu.mhsl.craftattack.spawn.core.config.Configuration;
|
||||
import eu.mhsl.craftattack.spawn.core.database.Migrations;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
@@ -108,7 +107,6 @@ public abstract class Appliance {
|
||||
}
|
||||
|
||||
public void enableSequence(JavaPlugin plugin) {
|
||||
Migrations.apply(Main.instance().getDatabase(), this.migrationsName());
|
||||
this.onEnable();
|
||||
this.initialize(plugin);
|
||||
}
|
||||
@@ -118,14 +116,6 @@ public abstract class Appliance {
|
||||
this.destruct(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the folder containing this appliance's SQL migrations: {@code migrations/<name>/} in the resources.
|
||||
*/
|
||||
private String migrationsName() {
|
||||
String name = this.getClass().getSimpleName();
|
||||
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
|
||||
}
|
||||
|
||||
protected <T extends Appliance> T queryAppliance(Class<T> clazz) {
|
||||
return Main.instance().getAppliance(clazz);
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.core.database;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Shared MariaDB access for all appliances. Configured in the "database" section of the config.
|
||||
* All statements run on a single background thread in submission order, so callers (e.g. login events)
|
||||
* are never blocked. Without configuration the database is unavailable and all calls are ignored.
|
||||
*/
|
||||
public class Database {
|
||||
@FunctionalInterface
|
||||
public interface SqlAction {
|
||||
void run(Connection connection) throws SQLException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SqlQuery<T> {
|
||||
T run(Connection connection) throws SQLException;
|
||||
}
|
||||
|
||||
private final @Nullable String jdbcUrl;
|
||||
private final String user;
|
||||
private final String password;
|
||||
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||
1, 1, 0, TimeUnit.MILLISECONDS,
|
||||
new ArrayBlockingQueue<>(1000),
|
||||
runnable -> new Thread(runnable, "Spawn-Database"),
|
||||
(runnable, executor) -> Main.logger().warning("Database queue full, dropping statement")
|
||||
);
|
||||
private @Nullable Connection connection;
|
||||
|
||||
public Database(@Nullable ConfigurationSection config) {
|
||||
String host = config == null ? "" : config.getString("host", "");
|
||||
if(host.isBlank()) {
|
||||
this.jdbcUrl = null;
|
||||
this.user = "";
|
||||
this.password = "";
|
||||
Main.logger().info("No database configured, database features are disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
this.jdbcUrl = String.format(
|
||||
"jdbc:mysql://%s:%d/%s?connectTimeout=3000&socketTimeout=10000&allowMultiQueries=true",
|
||||
host,
|
||||
config.getInt("port", 3306),
|
||||
config.getString("name", "craftattack")
|
||||
);
|
||||
this.user = config.getString("user", "");
|
||||
this.password = config.getString("password", "");
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return this.jdbcUrl != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the action in the background without waiting for it. Errors are logged.
|
||||
*/
|
||||
public void execute(String description, SqlAction action) {
|
||||
if(!this.isAvailable()) return;
|
||||
this.executor.execute(() -> {
|
||||
try {
|
||||
action.run(this.getConnection());
|
||||
} catch(SQLException e) {
|
||||
this.handleError(description, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the query in the background. The future fails if the database is unavailable or the query fails.
|
||||
*/
|
||||
public <T> CompletableFuture<T> query(SqlQuery<T> query) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
if(!this.isAvailable()) {
|
||||
future.completeExceptionally(new IllegalStateException("No database configured"));
|
||||
return future;
|
||||
}
|
||||
this.executor.execute(() -> {
|
||||
try {
|
||||
future.complete(query.run(this.getConnection()));
|
||||
} catch(SQLException e) {
|
||||
this.closeConnection();
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.executor.shutdown();
|
||||
try {
|
||||
if(!this.executor.awaitTermination(5, TimeUnit.SECONDS))
|
||||
Main.logger().warning("Database: pending statements were not finished before shutdown");
|
||||
} catch(InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
this.closeConnection();
|
||||
}
|
||||
|
||||
private Connection getConnection() throws SQLException {
|
||||
if(this.connection == null || !this.connection.isValid(2))
|
||||
this.connection = DriverManager.getConnection(this.jdbcUrl, this.user, this.password);
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
private void closeConnection() {
|
||||
if(this.connection == null) return;
|
||||
try {
|
||||
this.connection.close();
|
||||
} catch(SQLException ignored) {
|
||||
}
|
||||
this.connection = null;
|
||||
}
|
||||
|
||||
private void handleError(String description, SQLException e) {
|
||||
Main.logger().log(Level.WARNING, String.format("Database: %s failed: %s", description, e.getMessage()));
|
||||
this.closeConnection();
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.core.database;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Applies SQL migration files shipped in the plugin jar.
|
||||
* Files live in {@code migrations/<appliance>/NNN_description.sql} and run in ascending order of NNN.
|
||||
* Applied versions are tracked per appliance in the table {@code schema_migrations}.
|
||||
*/
|
||||
public class Migrations {
|
||||
private static final String DIRECTORY = "migrations/";
|
||||
private static final Pattern FILE_PATTERN = Pattern.compile("^([^/]+)/(\\d+)_[^/]+\\.sql$");
|
||||
private static Map<String, List<Migration>> migrationsByAppliance;
|
||||
|
||||
private record Migration(int version, String fileName, String path) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues all pending migrations of the appliance on the database thread,
|
||||
* so they run before any statement the appliance submits afterwards.
|
||||
*/
|
||||
public static void apply(Database database, String appliance) {
|
||||
List<Migration> migrations = findMigrations().getOrDefault(appliance, List.of());
|
||||
if(migrations.isEmpty()) return;
|
||||
|
||||
if(!database.isAvailable()) {
|
||||
Main.logger().warning(String.format("Appliance '%s' has migrations, but no database is configured", appliance));
|
||||
return;
|
||||
}
|
||||
|
||||
database.execute("migrations of " + appliance, connection -> {
|
||||
createMigrationTable(connection);
|
||||
Set<Integer> applied = appliedVersions(connection, appliance);
|
||||
for(Migration migration : migrations) {
|
||||
if(applied.contains(migration.version())) continue;
|
||||
Main.logger().info(String.format("Applying migration %s/%s", appliance, migration.fileName()));
|
||||
try(Statement statement = connection.createStatement()) {
|
||||
statement.execute(readResource(migration.path()));
|
||||
}
|
||||
markApplied(connection, appliance, migration);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static synchronized Map<String, List<Migration>> findMigrations() {
|
||||
if(migrationsByAppliance != null) return migrationsByAppliance;
|
||||
Map<String, List<Migration>> migrations = new HashMap<>();
|
||||
try(JarFile jar = new JarFile(pluginJar())) {
|
||||
jar.stream()
|
||||
.map(JarEntry::getName)
|
||||
.filter(name -> name.startsWith(DIRECTORY))
|
||||
.forEach(path -> {
|
||||
Matcher matcher = FILE_PATTERN.matcher(path.substring(DIRECTORY.length()));
|
||||
if(!matcher.matches()) return;
|
||||
String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
migrations.computeIfAbsent(matcher.group(1), appliance -> new ArrayList<>())
|
||||
.add(new Migration(Integer.parseInt(matcher.group(2)), fileName, path));
|
||||
});
|
||||
} catch(IOException e) {
|
||||
throw new IllegalStateException("Could not read migrations from plugin jar", e);
|
||||
}
|
||||
migrations.values().forEach(list -> list.sort(Comparator.comparingInt(Migration::version)));
|
||||
migrationsByAppliance = migrations;
|
||||
return migrations;
|
||||
}
|
||||
|
||||
private static void createMigrationTable(Connection connection) throws SQLException {
|
||||
try(Statement statement = connection.createStatement()) {
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
appliance VARCHAR(64) NOT NULL,
|
||||
version INT NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
applied_at DATETIME NOT NULL DEFAULT UTC_TIMESTAMP(),
|
||||
PRIMARY KEY (appliance, version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<Integer> appliedVersions(Connection connection, String appliance) throws SQLException {
|
||||
Set<Integer> versions = new HashSet<>();
|
||||
try(PreparedStatement statement = connection.prepareStatement("SELECT version FROM schema_migrations WHERE appliance = ?")) {
|
||||
statement.setString(1, appliance);
|
||||
try(ResultSet result = statement.executeQuery()) {
|
||||
while(result.next()) versions.add(result.getInt(1));
|
||||
}
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
|
||||
private static void markApplied(Connection connection, String appliance, Migration migration) throws SQLException {
|
||||
try(PreparedStatement statement = connection.prepareStatement("INSERT INTO schema_migrations (appliance, version, file_name) VALUES (?, ?, ?)")) {
|
||||
statement.setString(1, appliance);
|
||||
statement.setInt(2, migration.version());
|
||||
statement.setString(3, migration.fileName());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static String readResource(String path) throws SQLException {
|
||||
try(InputStream stream = Main.class.getClassLoader().getResourceAsStream(path)) {
|
||||
if(stream == null) throw new SQLException("Migration file not found: " + path);
|
||||
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch(IOException e) {
|
||||
throw new SQLException("Could not read migration file " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static File pluginJar() {
|
||||
try {
|
||||
return new File(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI());
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Could not locate plugin jar", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class IteratorUtil {
|
||||
}
|
||||
|
||||
public static void times(int times, Runnable callback) {
|
||||
IntStream.range(0, times).forEach(_ -> callback.run());
|
||||
IntStream.range(0, times).forEach(value -> callback.run());
|
||||
}
|
||||
|
||||
public static <T> void iterateListInGlobal(int sectionStart, List<T> list, BiConsumer<Integer, T> callback) {
|
||||
|
||||
@@ -7,16 +7,6 @@ api:
|
||||
secret: secret
|
||||
baseurl: https://mhsl.eu/craftattack/api/
|
||||
|
||||
database:
|
||||
host:
|
||||
port: 3306
|
||||
name: craftattack
|
||||
user:
|
||||
password:
|
||||
|
||||
loginLog:
|
||||
retentionMonths: 16
|
||||
|
||||
worldMuseum:
|
||||
uuid:
|
||||
connect-server-name: worldmuseum
|
||||
|
||||
@@ -2,7 +2,7 @@ dependencies {
|
||||
implementation project(':core')
|
||||
implementation project(':common')
|
||||
|
||||
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
|
||||
compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.frostWalkerBoat;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.Levelled;
|
||||
import org.bukkit.entity.Boat;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.vehicle.VehicleMoveEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
class BoatMoveListener extends ApplianceListener<FrostWalkerBoat> {
|
||||
@EventHandler
|
||||
public void onBoatMove(VehicleMoveEvent event) {
|
||||
if(!(event.getVehicle() instanceof Boat boat)) return;
|
||||
|
||||
// if(!event.getVehicle().getPersistentDataContainer().has(
|
||||
// this.getAppliance().getNamespacedKey(),
|
||||
// PersistentDataType.BOOLEAN)
|
||||
// ) return;
|
||||
//
|
||||
// if(Boolean.FALSE.equals(event.getVehicle().getPersistentDataContainer().get(
|
||||
// this.getAppliance().getNamespacedKey(),
|
||||
// PersistentDataType.BOOLEAN
|
||||
// ))) return;
|
||||
|
||||
Block blockAtBoat = event.getTo().getBlock();
|
||||
|
||||
if(blockAtBoat.getType() == Material.WATER) {
|
||||
if (!(blockAtBoat.getBlockData() instanceof Levelled waterData)) return;
|
||||
if (waterData.getLevel() != 0) return;
|
||||
if (!blockAtBoat.getRelative(BlockFace.UP).isEmpty()) return;
|
||||
|
||||
Location teleportLocation = event.getTo().clone();
|
||||
teleportLocation.setY(blockAtBoat.getY()+1.1);
|
||||
boat.teleport(teleportLocation);
|
||||
}
|
||||
|
||||
Vector velocity = event.getTo().toVector().subtract(event.getFrom().toVector());
|
||||
velocity.setY(0);
|
||||
Block iceTargetBlock;
|
||||
if(velocity.isZero()) {
|
||||
iceTargetBlock = event.getTo().getBlock().getRelative(BlockFace.DOWN);
|
||||
} else {
|
||||
Location iceTargetLocation = blockAtBoat.getLocation().clone().add(velocity.clone().normalize().multiply(5));
|
||||
iceTargetBlock = iceTargetLocation.getBlock().getRelative(BlockFace.DOWN);
|
||||
}
|
||||
this.getAppliance().freezeWater(iceTargetBlock);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.frostWalkerBoat;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.Levelled;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FrostWalkerBoat extends Appliance {
|
||||
public NamespacedKey getNamespacedKey() {
|
||||
return new NamespacedKey(this.getClass().getSimpleName().toLowerCase(), "is_frostwalker_enchanted");
|
||||
}
|
||||
|
||||
public void freezeWater(Block targetBlock) {
|
||||
int radius = 4;
|
||||
double circleRadiusSquared = Math.pow(radius + 0.5, 2);
|
||||
|
||||
for(int xOffset = -radius; xOffset <= radius; xOffset++) {
|
||||
for(int zOffset = -radius; zOffset <= radius; zOffset++) {
|
||||
int distanceSquared =
|
||||
xOffset * xOffset + zOffset * zOffset;
|
||||
|
||||
if(distanceSquared > circleRadiusSquared) continue;
|
||||
|
||||
Block block = targetBlock.getRelative(xOffset, 0, zOffset);
|
||||
|
||||
if(block.getType() != Material.WATER) continue;
|
||||
if(!(block.getBlockData() instanceof Levelled waterData)) continue;
|
||||
if(waterData.getLevel() != 0) continue;
|
||||
if(!block.getRelative(BlockFace.UP).isEmpty()) continue;
|
||||
|
||||
block.setType(Material.FROSTED_ICE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
protected List<Listener> listeners() {
|
||||
return List.of(new BoatMoveListener());
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.newYearFirework;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.FireworkEffect;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Firework;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.meta.FireworkMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class NewYearFirework extends Appliance {
|
||||
private static final String namespace = NewYearFirework.class.getSimpleName().toLowerCase(Locale.ROOT);
|
||||
public static NamespacedKey fireworkKey = new NamespacedKey(namespace, "newyear_firework".toLowerCase());
|
||||
|
||||
public void spawnFirework(Player player) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Firework firework = player.getWorld().spawn(player.getLocation(), Firework.class);
|
||||
firework.getPersistentDataContainer().set(fireworkKey, PersistentDataType.BOOLEAN, true);
|
||||
FireworkMeta fireworkMeta = firework.getFireworkMeta();
|
||||
fireworkMeta.addEffect(FireworkEffect.builder()
|
||||
.with(FireworkEffect.Type.BALL)
|
||||
.withColor(Color.AQUA, Color.RED, Color.LIME, Color.ORANGE, Color.YELLOW)
|
||||
.withFade(Color.FUCHSIA, Color.WHITE)
|
||||
.trail(true)
|
||||
.flicker(true)
|
||||
.build()
|
||||
);
|
||||
fireworkMeta.setPower((int) (i % 3 + 0.25));
|
||||
firework.setFireworkMeta(fireworkMeta);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldSpawnFirework() {
|
||||
return LocalDate.now().getDayOfYear() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(new NewYearFireworkListener());
|
||||
}
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.newYearFirework;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.entity.Firework;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
class NewYearFireworkListener extends ApplianceListener<NewYearFirework> {
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
if(!this.getAppliance().shouldSpawnFirework()) return;
|
||||
this.getAppliance().spawnFirework(event.getPlayer());
|
||||
}
|
||||
@EventHandler
|
||||
public void onDamage(EntityDamageByEntityEvent event) {
|
||||
if(!(event.getDamager() instanceof Firework firework)) return;
|
||||
if(!firework.getPersistentDataContainer().has(NewYearFirework.fireworkKey)) return;
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.quarzRecipe;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.ShapelessRecipe;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class QuarzRecipe extends Appliance {
|
||||
private NamespacedKey recipeKey;
|
||||
@Override
|
||||
public void onEnable() {
|
||||
this.recipeKey = new NamespacedKey(Main.instance(), "quarz_block_to_quarz");
|
||||
|
||||
ShapelessRecipe recipe = new ShapelessRecipe(this.recipeKey, new ItemStack(Material.QUARTZ, 4));
|
||||
recipe.addIngredient(Material.QUARTZ_BLOCK);
|
||||
|
||||
Bukkit.addRecipe(recipe);
|
||||
Bukkit.getOnlinePlayers().forEach(this::unlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
Bukkit.removeRecipe(this.recipeKey);
|
||||
}
|
||||
|
||||
public void unlock(Player player) {
|
||||
player.discoverRecipe(this.recipeKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(new QuarzRecipeListener());
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.quarzRecipe;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
|
||||
class QuarzRecipeListener extends ApplianceListener<QuarzRecipe> {
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
this.getAppliance().unlock(event.getPlayer());
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.antiPlayerLootDespawn;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class AntiPlayerLootDespawn extends Appliance {
|
||||
private static final int extraCycles = 5;
|
||||
private static final String namespace = AntiPlayerLootDespawnListener.class.getSimpleName().toLowerCase(Locale.ROOT);
|
||||
public static NamespacedKey cyclesKey = new NamespacedKey(namespace, "item_deaths_cycles".toLowerCase());
|
||||
|
||||
public void markItems(List<ItemStack> item) {
|
||||
item.forEach(itemStack ->
|
||||
itemStack.editPersistentDataContainer(persistentDataContainer ->
|
||||
persistentDataContainer.set(cyclesKey, PersistentDataType.INTEGER, extraCycles)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public boolean isItemAboveMaxAge(Item item) {
|
||||
ItemStack itemStack = item.getItemStack();
|
||||
|
||||
int cycles = itemStack.getPersistentDataContainer()
|
||||
.getOrDefault(AntiPlayerLootDespawn.cyclesKey, PersistentDataType.INTEGER, 0);
|
||||
|
||||
itemStack.editPersistentDataContainer(data ->
|
||||
data.set(AntiPlayerLootDespawn.cyclesKey, PersistentDataType.INTEGER, cycles - 1)
|
||||
);
|
||||
|
||||
item.setItemStack(itemStack);
|
||||
return cycles > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull List<Listener> listeners() {
|
||||
return List.of(new AntiPlayerLootDespawnListener());
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.antiPlayerLootDespawn;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.entity.ItemDespawnEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
|
||||
class AntiPlayerLootDespawnListener extends ApplianceListener<AntiPlayerLootDespawn> {
|
||||
|
||||
@EventHandler
|
||||
public void onDeath(PlayerDeathEvent event) {
|
||||
this.getAppliance().markItems(event.getDrops());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onItemDespawn(ItemDespawnEvent event) {
|
||||
event.setCancelled(this.getAppliance().isItemAboveMaxAge(event.getEntity()));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -20,7 +20,7 @@ import org.bukkit.event.Event;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
|
||||
+16
-20
@@ -26,8 +26,10 @@ import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static java.util.Map.entry;
|
||||
import static org.bukkit.Sound.MUSIC_DISC_LAVA_CHICKEN;
|
||||
|
||||
public class ProjectStart extends Appliance {
|
||||
@@ -55,23 +57,17 @@ public class ProjectStart extends Appliance {
|
||||
)
|
||||
);
|
||||
|
||||
private record GameRuleSetting<T>(GameRule<T> rule, T before, T after) {
|
||||
void apply(boolean afterStart) {
|
||||
IteratorUtil.worlds(world -> world.setGameRule(this.rule, afterStart ? this.after : this.before));
|
||||
}
|
||||
}
|
||||
|
||||
private final List<GameRuleSetting<?>> gameRules = List.of(
|
||||
new GameRuleSetting<>(GameRules.ADVANCE_TIME, false, true),
|
||||
new GameRuleSetting<>(GameRules.SPAWN_PHANTOMS, false, true),
|
||||
new GameRuleSetting<>(GameRules.RAIDS, false, true),
|
||||
new GameRuleSetting<>(GameRules.ENTITY_DROPS, false, true),
|
||||
new GameRuleSetting<>(GameRules.SPAWN_PATROLS, false, true),
|
||||
new GameRuleSetting<>(GameRules.SPAWN_WANDERING_TRADERS, false, true),
|
||||
new GameRuleSetting<>(GameRules.ADVANCE_WEATHER, false, true),
|
||||
new GameRuleSetting<>(GameRules.FALL_DAMAGE, false, true),
|
||||
new GameRuleSetting<>(GameRules.FIRE_DAMAGE, false, true),
|
||||
new GameRuleSetting<>(GameRules.FIRE_SPREAD_RADIUS_AROUND_PLAYER, 0, -1)
|
||||
private final Map<GameRule<Boolean>, Boolean> gameRulesAfterStart = Map.ofEntries(
|
||||
entry(GameRule.DO_DAYLIGHT_CYCLE, true),
|
||||
entry(GameRule.DO_INSOMNIA, true),
|
||||
entry(GameRule.DISABLE_RAIDS, false),
|
||||
entry(GameRule.DO_FIRE_TICK, true),
|
||||
entry(GameRule.DO_ENTITY_DROPS, true),
|
||||
entry(GameRule.DO_PATROL_SPAWNING, true),
|
||||
entry(GameRule.DO_TRADER_SPAWNING, true),
|
||||
entry(GameRule.DO_WEATHER_CYCLE, true),
|
||||
entry(GameRule.FALL_DAMAGE, true),
|
||||
entry(GameRule.FIRE_DAMAGE, true)
|
||||
);
|
||||
|
||||
public ProjectStart() {
|
||||
@@ -80,7 +76,7 @@ public class ProjectStart extends Appliance {
|
||||
this.countdown.addCustomAnnouncement(
|
||||
new Countdown.CustomAnnouncements(
|
||||
counter -> counter == this.startMusicAt,
|
||||
_ -> this.glassLocation
|
||||
counter -> this.glassLocation
|
||||
.getWorld()
|
||||
.playSound(this.glassLocation, MUSIC_DISC_LAVA_CHICKEN, SoundCategory.RECORDS, 500f, 1f)
|
||||
)
|
||||
@@ -125,7 +121,7 @@ public class ProjectStart extends Appliance {
|
||||
this.setEnabled(false);
|
||||
|
||||
IteratorUtil.worlds(World::getWorldBorder, worldBorder -> worldBorder.setSize(worldBorder.getMaxSize()));
|
||||
this.gameRules.forEach(setting -> setting.apply(true));
|
||||
IteratorUtil.worlds(world -> IteratorUtil.setGameRules(this.gameRulesAfterStart, false));
|
||||
Bukkit.getWorlds().getFirst().setFullTime(0);
|
||||
|
||||
this.netherFireLocations.forEach(location -> Objects.requireNonNull(this.startWorld).getBlockAt(location).setType(Material.FIRE));
|
||||
@@ -172,7 +168,7 @@ public class ProjectStart extends Appliance {
|
||||
worldBorder.setDamageAmount(0);
|
||||
});
|
||||
|
||||
this.gameRules.forEach(setting -> setting.apply(false));
|
||||
IteratorUtil.worlds(world -> world, world -> IteratorUtil.setGameRules(this.gameRulesAfterStart, true));
|
||||
this.resetAdvancements();
|
||||
|
||||
this.blockCycle.reset();
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ dependencies {
|
||||
implementation project(':common')
|
||||
implementation project(':core')
|
||||
|
||||
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
|
||||
compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
tasks.register('deployVaroPlugin', Copy) {
|
||||
dependsOn ":varo:shadowJar"
|
||||
from { project(":varo").shadowJar.archiveFile }
|
||||
from { project(":varo").shadowJar.archivePath }
|
||||
into file('path') // path to plugins folder
|
||||
rename { fileName -> "varo.jar" }
|
||||
}
|
||||
@@ -10,15 +10,15 @@ tasks.register("uploadVaroPlugin") {
|
||||
|
||||
doLast {
|
||||
def jarFile = project(":varo").tasks.named("shadowJar").get().outputs.files.singleFile
|
||||
providers.exec {
|
||||
exec {
|
||||
commandLine "scp", "-4", "-P", "22", jarFile.absolutePath, "user@host:path/varo.jar"
|
||||
}.result.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('deployCraftAttackPlugin', Copy) {
|
||||
dependsOn ":craftattack:shadowJar"
|
||||
from { project(":craftattack").shadowJar.archiveFile }
|
||||
from { project(":craftattack").shadowJar.archivePath }
|
||||
into file('path') // path to plugins folder
|
||||
rename { fileName -> "craftattack.jar" }
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ dependencies {
|
||||
implementation project(':core')
|
||||
implementation project(':common')
|
||||
|
||||
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
|
||||
compileOnly 'io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT'
|
||||
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
|
||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ import org.bukkit.event.player.PlayerItemConsumeEvent;
|
||||
import org.bukkit.inventory.meta.PotionMeta;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
|
||||
class ForbiddenPotionsListener extends ApplianceListener<ForbiddenItems> {
|
||||
|
||||
Reference in new issue
Block a user