diff --git a/BauSystem/BauSystem_Main/build.gradle.kts b/BauSystem/BauSystem_Main/build.gradle.kts index c6316f7d4..c387bc362 100644 --- a/BauSystem/BauSystem_Main/build.gradle.kts +++ b/BauSystem/BauSystem_Main/build.gradle.kts @@ -47,7 +47,7 @@ dependencies { compileOnly(libs.netty) implementation(libs.luaj) - implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar")) + // implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar")) } widener { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java index 5a8cac696..041e4d837 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java @@ -20,8 +20,7 @@ package de.steamwar.bausystem; import de.steamwar.bausystem.config.BauServer; -import de.steamwar.bausystem.configplayer.Config; -import de.steamwar.bausystem.configplayer.ConfigConverter; +import de.steamwar.bausystem.config.ConfigStore; import de.steamwar.bausystem.features.gui.BauGUI; import de.steamwar.bausystem.features.script.lua.SteamWarLuaPlugin; import de.steamwar.bausystem.features.script.lua.libs.LuaLib; @@ -36,7 +35,6 @@ import de.steamwar.bausystem.region.Region; import de.steamwar.bausystem.region.RegionSystem; import de.steamwar.bausystem.utils.ScoreboardElement; import de.steamwar.bausystem.utils.TickManager; -import de.steamwar.bausystem.worlddata.WorldData; import de.steamwar.command.AbstractValidator; import de.steamwar.command.SWCommandUtils; import de.steamwar.core.CRIUSleepEvent; @@ -112,9 +110,6 @@ public class BauSystem extends JavaPlugin implements Listener { if (any instanceof PanzernAlgorithm) { Panzern.add((PanzernAlgorithm) any); } - if (any instanceof ConfigConverter) { - Config.addConfigConverter((ConfigConverter) any); - } if (any instanceof BoundingBoxLoader) { ((BoundingBoxLoader) any).load(); } @@ -160,9 +155,8 @@ public class BauSystem extends JavaPlugin implements Listener { @Override public void onDisable() { linker.unlink(); - WorldData.write(); RegionSystem.INSTANCE.save(); - Config.getInstance().saveAll(); + ConfigStore.saveAll(); } private AbstractValidator validator(Permission permission) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigConverter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigConverter.java new file mode 100644 index 000000000..a76db0619 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigConverter.java @@ -0,0 +1,47 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2025 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.config; + +/** + * Converts a config of one version into the next version, e.g. ConfigV1 to + * ConfigV2. The converter for the second newest version converts into the + * current version. Converters are registered in the {@link ConfigStore} + * of the config. + * + * @param the config version to convert from + * @param the config version to convert to + */ +public interface ConfigConverter { + + /** + * @return the version this converter converts from + */ + int version(); + + /** + * Converts the config to the next version. If anything goes wrong do not + * silently exit this method, throw an Exception. The {@link ConfigStore} + * will deal with it. + * + * @param config the config to convert + * @return the converted config + */ + T convert(F config); +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStore.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStore.java new file mode 100644 index 000000000..21ff6fcbf --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStore.java @@ -0,0 +1,364 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.config; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import de.steamwar.sql.UserConfig; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.logging.Level; + +/** + * Loads, converts and saves a versioned config stored as JSON. Older versions + * are converted with the registered {@link ConfigConverter}s, legacy formats + * that are not JSON can be read with {@link #legacy(Predicate, Function)}. + *

+ * A store can be bound to where the config is stored: + *

    + *
  • {@link #userConfig(String)}: one config per player in the {@link UserConfig}, use {@link #get(Player)}
  • + *
  • {@link #file(File)}: one config in a file, use {@link #get()}
  • + *
  • unbound: use {@link #load(File)} and {@link #save(File, VersionedConfig)}
  • + *
+ * + * @param the current config version + */ +public class ConfigStore { + + private static final List> STORES = new ArrayList<>(); + + private final Class type; + private final int version; + private final Supplier defaultConfig; + + private final Map> jsonVersions = new HashMap<>(); + private final Map> converters = new HashMap<>(); + private final List legacyFormats = new ArrayList<>(); + private Gson gson = new Gson(); + private Integer unversionedVersion = null; + + private String userConfigKey = null; + private Predicate removeIf = config -> false; + private final Map playerConfigs = new ConcurrentHashMap<>(); + + private File file = null; + private File legacyFile = null; + private Function legacyFileReader = null; + private T fileConfig = null; + + private record LegacyFormat(Predicate matches, Function reader) { + } + + public ConfigStore(Class type, int version, Supplier defaultConfig) { + this.type = type; + this.version = version; + this.defaultConfig = defaultConfig; + jsonVersions.put(version, type); + STORES.add(this); + } + + public ConfigStore gson(Gson gson) { + this.gson = gson; + return this; + } + + /** + * Registers an older config version that is stored as JSON. + */ + public ConfigStore jsonVersion(int version, Class type) { + jsonVersions.put(version, type); + return this; + } + + public ConfigStore converter(ConfigConverter converter) { + if (converters.putIfAbsent(converter.version(), converter) != null) { + throw new IllegalStateException("Duplicate converter for version " + converter.version() + " of " + type.getSimpleName()); + } + return this; + } + + /** + * Registers a legacy format that is not JSON, stored strings matching the predicate are read by the reader. + */ + public ConfigStore legacy(Predicate matches, Function reader) { + legacyFormats.add(new LegacyFormat(matches, reader)); + return this; + } + + /** + * The version of stored JSON without a version field. + */ + public ConfigStore unversioned(int version) { + this.unversionedVersion = version; + return this; + } + + /** + * Binds this store to the {@link UserConfig} with the given key, one config per player. + */ + public ConfigStore userConfig(String key) { + this.userConfigKey = key; + return this; + } + + /** + * Removes the config from the {@link UserConfig} instead of saving it, if the predicate matches. + */ + public ConfigStore removeIf(Predicate removeIf) { + this.removeIf = removeIf; + return this; + } + + /** + * Binds this store to a single file. + */ + public ConfigStore file(File file) { + this.file = file; + return this; + } + + /** + * Legacy file to read, if the file of this store does not exist yet. + */ + public ConfigStore legacyFile(File legacyFile, Function reader) { + this.legacyFile = legacyFile; + this.legacyFileReader = reader; + return this; + } + + /** + * Reads a stored config and converts it to the current version. + * + * @throws RuntimeException if the config could not be read or converted + */ + public T read(String stored) { + for (LegacyFormat legacyFormat : legacyFormats) { + if (legacyFormat.matches().test(stored)) { + return update(legacyFormat.reader().apply(stored)); + } + } + + JsonObject json = JsonParser.parseString(stored).getAsJsonObject(); + int storedVersion; + if (json.has("version")) { + storedVersion = json.get("version").getAsInt(); + } else if (unversionedVersion != null) { + storedVersion = unversionedVersion; + json.addProperty("version", storedVersion); + } else { + throw new IllegalStateException("Missing version of " + type.getSimpleName()); + } + Class storedType = jsonVersions.get(storedVersion); + if (storedType == null) { + throw new IllegalStateException("Unknown version " + storedVersion + " of " + type.getSimpleName()); + } + return update(gson.fromJson(json, storedType)); + } + + public String write(T config) { + return gson.toJson(config); + } + + /** + * Converts a config to the current version. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public T update(VersionedConfig config) { + while (config.getVersion() < version) { + ConfigConverter converter = converters.get(config.getVersion()); + if (converter == null) { + throw new IllegalStateException("No converter found for version " + config.getVersion() + " of " + type.getSimpleName()); + } + VersionedConfig converted = converter.convert(config); + if (converted.getVersion() <= config.getVersion()) { + throw new IllegalStateException("Version did not increase after conversion of version " + config.getVersion() + " of " + type.getSimpleName()); + } + config = converted; + } + if (!type.isInstance(config)) { + throw new IllegalStateException("Version " + config.getVersion() + " is not the current version of " + type.getSimpleName()); + } + return type.cast(config); + } + + /** + * @return the config stored in the file, or the default config if the file is missing or empty + * @throws RuntimeException if the config could not be read or converted + */ + public T load(File file) throws IOException { + if (file.length() == 0) { + return defaultConfig.get(); + } + return read(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } + + public void save(File file, T config) throws IOException { + Files.writeString(file.toPath(), write(config), StandardCharsets.UTF_8); + } + + /** + * Get the config of a player, loads it from the {@link UserConfig} and converts it if necessary. + */ + public T get(Player player) { + requireUserConfig(); + return playerConfigs.computeIfAbsent(player.getUniqueId(), this::loadPlayer); + } + + /** + * Save the config of a player, this does not unload the config. + */ + public void save(Player player) { + requireUserConfig(); + T config = playerConfigs.get(player.getUniqueId()); + if (config != null) { + savePlayer(player.getUniqueId(), config); + } + } + + /** + * Save and unload the config of a player. + */ + public void unload(Player player) { + if (userConfigKey == null) return; + T config = playerConfigs.remove(player.getUniqueId()); + if (config != null) { + savePlayer(player.getUniqueId(), config); + } + } + + private T loadPlayer(UUID uuid) { + String stored = UserConfig.getConfig(uuid, userConfigKey); + if (stored == null) { + return defaultConfig.get(); + } + try { + return read(stored); + } catch (Exception e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not load " + type.getSimpleName() + " of " + uuid, e); + return defaultConfig.get(); + } + } + + private void savePlayer(UUID uuid, T config) { + if (removeIf.test(config)) { + UserConfig.removePlayerConfig(uuid, userConfigKey); + } else { + UserConfig.updatePlayerConfig(uuid, userConfigKey, write(config)); + } + } + + private void requireUserConfig() { + if (userConfigKey == null) { + throw new IllegalStateException(type.getSimpleName() + " is not stored in the UserConfig"); + } + } + + /** + * Get the config of the file of this store, loads it and converts it if necessary. + */ + public T get() { + requireFile(); + if (fileConfig == null) { + fileConfig = loadFile(); + save(); + } + return fileConfig; + } + + /** + * Save the config to the file of this store, if it was loaded. + */ + public void save() { + requireFile(); + if (fileConfig == null) return; + try { + save(file, fileConfig); + } catch (IOException e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not write " + file, e); + } + } + + private T loadFile() { + try { + if (file.length() != 0) { + return load(file); + } + if (legacyFile != null && legacyFile.length() != 0) { + return update(legacyFileReader.apply(Files.readString(legacyFile.toPath(), StandardCharsets.UTF_8))); + } + return defaultConfig.get(); + } catch (Exception e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not load " + file + ", using default " + type.getSimpleName(), e); + backupBrokenFile(); + return defaultConfig.get(); + } + } + + private void backupBrokenFile() { + if (file.length() == 0) return; + try { + Files.copy(file.toPath(), new File(file.getParentFile(), file.getName() + ".broken").toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + Bukkit.getLogger().log(Level.SEVERE, "Could not backup broken " + file, e); + } + } + + private void requireFile() { + if (file == null) { + throw new IllegalStateException(type.getSimpleName() + " is not bound to a file"); + } + } + + static void unloadAll(Player player) { + STORES.forEach(store -> store.unload(player)); + } + + /** + * Saves every loaded config and unloads the configs of the players. + */ + public static void saveAll() { + for (ConfigStore store : STORES) { + if (store.userConfigKey != null) { + store.playerConfigs.forEach((uuid, config) -> saveUnchecked(store, uuid, config)); + store.playerConfigs.clear(); + } + if (store.file != null) { + store.save(); + } + } + } + + @SuppressWarnings("unchecked") + private static void saveUnchecked(ConfigStore store, UUID uuid, Object config) { + store.savePlayer(uuid, (T) config); + } +} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStoreListener.java similarity index 58% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java rename to BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStoreListener.java index b39a49431..67276588f 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/ChattingEvent.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/ConfigStoreListener.java @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2025 SteamWar.de-Serverteam + * Copyright (C) 2026 SteamWar.de-Serverteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,21 +17,29 @@ * along with this program. If not, see . */ -package de.steamwar.core.events; +package de.steamwar.bausystem.config; -import de.steamwar.core.Core; +import de.steamwar.core.CRIUSleepEvent; import de.steamwar.linkage.Linked; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerQuitEvent; +/** + * Saves and unloads the configs of the {@link ConfigStore}s bound to the UserConfig + * when a player quits, configs are loaded on first use. + */ @Linked -public class ChattingEvent implements Listener { +public class ConfigStoreListener implements Listener { - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - private void onChat(AsyncPlayerChatEvent event) { - event.setCancelled(true); - Core.MESSAGE.broadcastPrefixless("LOCAL_CHAT", event.getPlayer().getDisplayName(), event.getMessage()); + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + ConfigStore.unloadAll(event.getPlayer()); + } + + @EventHandler + public void onCRIUSleep(CRIUSleepEvent event) { + ConfigStore.saveAll(); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/VersionedConfig.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/VersionedConfig.java new file mode 100644 index 000000000..ca036e063 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/VersionedConfig.java @@ -0,0 +1,29 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.config; + +/** + * Common interface of every version of a config stored by a {@link ConfigStore}, + * the current version (e.g. Config) and every older version (ConfigV1, ConfigV2, ...). + */ +public interface VersionedConfig { + + int getVersion(); +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionItemDeserializer.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionItemDeserializer.java new file mode 100644 index 000000000..1ce4ab251 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionItemDeserializer.java @@ -0,0 +1,95 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.config.legacy; + +import lombok.experimental.UtilityClass; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Deserializes {@link ItemStack}s that were serialized by YAPION, read with {@link YapionReader}. + */ +@UtilityClass +@Deprecated +public class YapionItemDeserializer { + + private static final String TYPE_IDENTIFIER = "@type"; + + public ItemStack deserializeItem(Object yapion) { + if (yapion == null) return null; + Object result = deserialize(yapion); + if (result instanceof ItemStack itemStack) return itemStack; + throw new IllegalArgumentException("Not an ItemStack: " + result); + } + + private Object deserialize(Object yapion) { + if (yapion instanceof List list) { + List result = new ArrayList<>(); + list.forEach(o -> result.add(deserialize(o))); + return result; + } + if (yapion instanceof YapionReader.YapionMap map) { + Map result = new LinkedHashMap<>(); + map.entries().forEach((key, value) -> result.put(deserialize(key), deserialize(value))); + return result; + } + if (yapion instanceof Map object) { + return deserializeObject(object); + } + return yapion; + } + + private Object deserializeObject(Map object) { + Map data = new LinkedHashMap<>(); + object.forEach((key, value) -> { + if (!TYPE_IDENTIFIER.equals(key)) data.put((String) key, deserialize(value)); + }); + + if (!(object.get(TYPE_IDENTIFIER) instanceof String type)) { + return data; + } + + Class javaType = javaUtilClass(type); + if (javaType != null && Collection.class.isAssignableFrom(javaType)) { + return data.getOrDefault("values", new ArrayList<>()); + } + if (javaType != null && Map.class.isAssignableFrom(javaType)) { + return data.containsKey("values") ? data.get("values") : data; + } + + data.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, type.equals("org.bukkit.inventory.meta.ItemMeta") ? "ItemMeta" : type); + return ConfigurationSerialization.deserializeObject(data); + } + + private Class javaUtilClass(String type) { + if (!type.startsWith("java.util.")) return null; + try { + return Class.forName(type); + } catch (ClassNotFoundException e) { + return null; + } + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionReader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionReader.java new file mode 100644 index 000000000..c3fb84d48 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/legacy/YapionReader.java @@ -0,0 +1,323 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.config.legacy; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Minimal reader for the legacy YAPION format, only used to migrate old data. + *

+ * The result is built from plain java objects: + *

    + *
  • YAPION objects {@code {key(value)}} become a {@link LinkedHashMap} of String to value
  • + *
  • YAPION arrays {@code [a,b]} become a {@link List}
  • + *
  • YAPION maps {@code <(a):(b)>} become a {@link YapionMap}
  • + *
  • YAPION values {@code (value)} become null, Boolean, Number or String
  • + *
+ */ +@Deprecated +public class YapionReader { + + private static final Pattern INTEGER = Pattern.compile("[-+]?\\d+"); + private static final Pattern DECIMAL = Pattern.compile("[-+]?(\\d+\\.\\d*|\\.\\d+|\\d+)([eE][-+]?\\d+)?"); + + public record YapionMap(Map entries) { + } + + public static Map parse(String yapion) { + YapionReader reader = new YapionReader(yapion); + reader.skipWhitespace(); + Map result = reader.readObject(); + reader.skipWhitespace(); + if (reader.index != yapion.length()) { + throw reader.error("Unexpected trailing content"); + } + return result; + } + + public static double getDouble(Map object, String key, double defaultValue) { + return object.get(key) instanceof Number value ? value.doubleValue() : defaultValue; + } + + public static int getInt(Map object, String key, int defaultValue) { + return object.get(key) instanceof Number value ? value.intValue() : defaultValue; + } + + public static boolean getBoolean(Map object, String key, boolean defaultValue) { + return object.get(key) instanceof Boolean value ? value : defaultValue; + } + + public static String getString(Map object, String key, String defaultValue) { + return object.get(key) instanceof String value ? value : defaultValue; + } + + /** + * @return the entries of the array that are YAPION objects + */ + public static List> getObjects(Map object, String key) { + List> objects = new ArrayList<>(); + if (object.get(key) instanceof List array) { + array.forEach(o -> { + if (o instanceof Map map) objects.add(map); + }); + } + return objects; + } + + private final String input; + private int index = 0; + + private YapionReader(String input) { + this.input = input; + } + + private Object readAny() { + return switch (peek()) { + case '{' -> readObject(); + case '[' -> readArray(); + case '<' -> readMap(); + case '(' -> readValue(); + default -> throw error("Expected '{', '[', '<' or '('"); + }; + } + + private Map readObject() { + expect('{'); + Map object = new LinkedHashMap<>(); + while (true) { + skipWhitespace(); + if (peek() == '}') { + index++; + return object; + } + StringBuilder key = new StringBuilder(); + while (true) { + char c = peek(); + if (c == '(' || c == '{' || c == '[' || c == '<') break; + if (c == '}') throw error("Key without value"); + key.append(readChar()); + } + object.put(key.toString(), readAny()); + } + } + + private List readArray() { + expect('['); + List array = new ArrayList<>(); + skipWhitespace(); + if (peek() == ']') { + index++; + return array; + } + while (true) { + int start = index; + skipWhitespace(); + char c = peek(); + if (c == '{' || c == '[' || c == '<' || c == '(') { + array.add(readAny()); + skipWhitespace(); + } else { + // Whitespace is part of plain values + index = start; + StringBuilder value = new StringBuilder(); + while (peek() != ',' && peek() != ']') { + value.append(readChar()); + } + array.add(typed(value.toString())); + } + char separator = input.charAt(index++); + if (separator == ']') return array; + if (separator != ',') throw error("Expected ',' or ']'"); + } + } + + private YapionMap readMap() { + expect('<'); + Map map = new LinkedHashMap<>(); + while (true) { + skipWhitespace(); + if (peek() == '>') { + index++; + return new YapionMap(map); + } + Object key = readAny(); + skipWhitespace(); + expect(':'); + skipWhitespace(); + map.put(key, readAny()); + } + } + + /** + * YAPION does not escape parentheses in values, balanced parentheses are part of the value. + * A backslash only escapes a backslash, a unicode sequence or a control character, otherwise it is kept. + */ + private Object readValue() { + expect('('); + StringBuilder value = new StringBuilder(); + int depth = 0; + while (true) { + char c = peek(); + if (c == '\\') { + value.append(readValueEscape()); + continue; + } + index++; + if (c == '(') { + depth++; + } else if (c == ')') { + if (depth == 0) break; + depth--; + } + value.append(c); + } + return typed(value.toString()); + } + + private String readValueEscape() { + index++; + if (index >= input.length()) return "\\"; + char escaped = input.charAt(index); + switch (escaped) { + case '\\', '(', ')': + index++; + return String.valueOf(escaped); + case 'n': + index++; + return "\n"; + case 't': + index++; + return "\t"; + case 'r': + index++; + return "\r"; + case 'u': + if (index + 5 <= input.length() && input.substring(index + 1, index + 5).matches("[0-9a-fA-F]{4}")) { + char unicode = (char) Integer.parseInt(input.substring(index + 1, index + 5), 16); + index += 5; + return String.valueOf(unicode); + } + return "\\"; + default: + return "\\"; + } + } + + /** + * Reads one character of a key or plain array value, resolving backslash escapes. + * A backslash that does not escape anything is kept, as the old player config + * collapsed escaped backslashes into a single one. + */ + private char readChar() { + char c = input.charAt(index++); + if (c != '\\') return c; + if (index >= input.length()) throw error("Unterminated escape"); + char escaped = input.charAt(index); + switch (escaped) { + case 'n': + index++; + return '\n'; + case 't': + index++; + return '\t'; + case 'r': + index++; + return '\r'; + case 'u': + if (index + 5 <= input.length() && input.substring(index + 1, index + 5).matches("[0-9a-fA-F]{4}")) { + char unicode = (char) Integer.parseInt(input.substring(index + 1, index + 5), 16); + index += 5; + return unicode; + } + return '\\'; + case '\\', ',', ' ', '(', ')', '{', '}', '[', ']', '<', '>', '"', ':', '@': + index++; + return escaped; + default: + return '\\'; + } + } + + private static Object typed(String value) { + if (value.equals("null")) return null; + if (value.equals("true")) return true; + if (value.equals("false")) return false; + if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + return value.substring(1, value.length() - 1); + } + try { + if (INTEGER.matcher(value).matches()) { + long l = Long.parseLong(value); + if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) return (int) l; + return l; + } + if (value.length() > 1) { + String number = value.substring(0, value.length() - 1); + switch (value.charAt(value.length() - 1)) { + case 'L': + if (INTEGER.matcher(number).matches()) return Long.parseLong(number); + break; + case 'S': + if (INTEGER.matcher(number).matches()) return Short.parseShort(number); + break; + case 'B': + if (INTEGER.matcher(number).matches()) return Byte.parseByte(number); + break; + case 'F': + if (DECIMAL.matcher(number).matches()) return Float.parseFloat(number); + break; + case 'D': + if (DECIMAL.matcher(number).matches()) return Double.parseDouble(number); + break; + default: + break; + } + } + if (DECIMAL.matcher(value).matches()) return Double.parseDouble(value); + } catch (NumberFormatException e) { + // Not a number, fall through to String + } + return value; + } + + private void skipWhitespace() { + while (index < input.length() && Character.isWhitespace(input.charAt(index))) { + index++; + } + } + + private char peek() { + if (index >= input.length()) throw error("Unexpected end of input"); + return input.charAt(index); + } + + private void expect(char c) { + if (peek() != c) throw error("Expected '" + c + "'"); + index++; + } + + private IllegalArgumentException error(String message) { + return new IllegalArgumentException(message + " at index " + index); + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/serializer/ItemStackTypeAdapter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/serializer/ItemStackTypeAdapter.java new file mode 100644 index 000000000..9edda3987 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/config/serializer/ItemStackTypeAdapter.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.config.serializer; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; + +import java.io.IOException; +import java.util.Base64; +import java.util.logging.Level; + +/** + * Stores an {@link ItemStack} as Base64 of {@link ItemStack#serializeAsBytes()}, which + * contains the data version and gets upgraded on newer Minecraft versions. + * Empty items are stored as null, items that can't be read are deserialized as null. + */ +public class ItemStackTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, ItemStack itemStack) throws IOException { + if (itemStack == null || itemStack.isEmpty()) { + out.nullValue(); + return; + } + out.value(Base64.getEncoder().encodeToString(itemStack.serializeAsBytes())); + } + + @Override + public ItemStack read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String value = in.nextString(); + try { + return ItemStack.deserializeBytes(Base64.getDecoder().decode(value)); + } catch (Exception e) { + Bukkit.getLogger().log(Level.WARNING, "Could not deserialize ItemStack", e); + return null; + } + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/Config.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/Config.java index 77b8b706b..19aef6d76 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/Config.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/Config.java @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2025 SteamWar.de-Serverteam + * Copyright (C) 2026 SteamWar.de-Serverteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,129 +19,77 @@ package de.steamwar.bausystem.configplayer; -import de.steamwar.bausystem.configplayer.serializer.ConfigurationSerializableSerializer; -import de.steamwar.linkage.Linked; -import de.steamwar.sql.UserConfig; +import com.google.gson.GsonBuilder; +import de.steamwar.bausystem.config.ConfigConverter; +import de.steamwar.bausystem.config.ConfigStore; +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.config.serializer.ItemStackTypeAdapter; +import de.steamwar.bausystem.configplayer.converter.ConfigV1ToV2Converter; +import de.steamwar.bausystem.features.hotbar.DefaultHotbar; +import lombok.AccessLevel; import lombok.Getter; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import yapion.hierarchy.output.StringOutput; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; -import yapion.serializing.SerializeManager; +import lombok.Setter; +import org.bukkit.inventory.ItemStack; import java.util.HashMap; import java.util.Map; -import java.util.UUID; -import java.util.logging.Level; -@Linked -public class Config implements Listener { +/** + * The current player config. It is stored as JSON, fields missing in the + * stored JSON keep the default value of their initializer, so adding a new + * field does not need a new version. + *

+ * When a field gets removed, renamed or its meaning changes, copy this class + * to ConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, + * write a {@link ConfigConverter} from the copy to this class and register it in the {@link #STORE}. + */ +@Getter +@Setter +public class Config implements VersionedConfig { - static { - SerializeManager.add(new ConfigurationSerializableSerializer()); - } + public static final int VERSION = 2; - @Getter - private static Config instance; + public static final ConfigStore STORE = new ConfigStore<>(Config.class, VERSION, Config::new) + .gson(new GsonBuilder().registerTypeHierarchyAdapter(ItemStack.class, new ItemStackTypeAdapter()).create()) + .legacy(s -> s.startsWith("{@version(1)"), ConfigV1::parse) + .converter(new ConfigV1ToV2Converter()) + .userConfig("bausystem"); - { - instance = this; - } - - private final Map playerConfigurations = new HashMap<>(); - - private static final Map CONFIG_CONVERTER_MAP = new HashMap<>(); - - public static void addConfigConverter(ConfigConverter configConverter) { - CONFIG_CONVERTER_MAP.putIfAbsent(configConverter.version(), configConverter); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - get(event.getPlayer()); - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent event) { - save(event.getPlayer()); - playerConfigurations.remove(event.getPlayer().getUniqueId()); - } + @Setter(AccessLevel.NONE) + protected int version = VERSION; /** - * Get a PlayerConfig, optionally loads it from the DataBase and migrates it if necessary. - * - * @param player the player from whom to get the config. - * @return the config object + * BauGui item id to inventory slot */ - public YAPIONObject get(Player player) { - UUID uuid = player.getUniqueId(); - if (!playerConfigurations.containsKey(uuid)) { - String s = UserConfig.getConfig(uuid, "bausystem"); - YAPIONObject yapionObject; - if (s == null) { - yapionObject = ConfigCreator.createDefaultConfig(); - } else { - yapionObject = YAPIONParser.parse(s); - } - yapionObject = update(yapionObject); - playerConfigurations.put(uuid, yapionObject); - return yapionObject; - } - return playerConfigurations.get(uuid); - } - - public void saveAll() { - playerConfigurations.forEach((uuid, yapionObject) -> { - String string = yapionObject.toYAPION(new StringOutput()).getResult().replaceAll("\\+", "\\"); - UserConfig.updatePlayerConfig(uuid, "bausystem", string); - }); - playerConfigurations.clear(); - } + protected Map bauGui = defaultBauGui(); + protected int bauGuiSize = 45; /** - * Save a PlayerConfig, this does not remove the key value mapping from the map. - * - * @param player the player to save the config. + * Slots 0-8 are the hotbar, slots 9-12 the armor contents */ - public void save(Player player) { - UUID uuid = player.getUniqueId(); - if (playerConfigurations.containsKey(uuid)) { - YAPIONObject yapionObject = playerConfigurations.get(uuid); - String string = yapionObject.toYAPION(new StringOutput()).getResult().replaceAll("\\\\+", "\\\\"); - UserConfig.updatePlayerConfig(uuid, "bausystem", string); - } - } + protected ItemStack[] hotbar = DefaultHotbar.defaultHotbar(); - private YAPIONObject update(YAPIONObject yapionObject) { - int version = yapionObject.getPlainValue("@version"); - while (version < ConfigCreator.currentVersion) { - ConfigConverter configConverter = CONFIG_CONVERTER_MAP.getOrDefault(version, null); - if (configConverter == null) { - Bukkit.getLogger().log(Level.SEVERE, "No updater found for version " + version); - return ConfigCreator.createDefaultConfig(); - } - try { - configConverter.update(yapionObject); - } catch (Exception e) { - Bukkit.getLogger().log(Level.SEVERE, e.getMessage(), e); - return ConfigCreator.createDefaultConfig(); - } - int newVersion = yapionObject.getPlainValue("@version"); - if (version == newVersion) { - Bukkit.getLogger().log(Level.SEVERE, "Version Tag was the same after conversion"); - return ConfigCreator.createDefaultConfig(); - } - if (newVersion < version) { - Bukkit.getLogger().log(Level.SEVERE, "Version Tag was earlier after conversion"); - return ConfigCreator.createDefaultConfig(); - } - version = newVersion; - } - return yapionObject; + protected boolean nightVision = false; + protected boolean tntClickDetails = true; + protected boolean detonatorAutostart = false; + protected boolean inventoryFill = false; + protected boolean rayVisualizer = false; + protected boolean smartPlace = false; + protected boolean blockCounter = false; + protected boolean depthMessage = true; + + public static Map defaultBauGui() { + // 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ? + // 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16 + // 18: 4 | 19: ? | 20: 19 | 21: 21 | 22: 9 | 23: 0 | 24: 1 | 25: ? | 26: 11 + // 27: 6 | 28: ? | 29: ? | 30: ? | 31: ? | 32: ? | 33: ? | 34: ? | 35: 18 + // 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ? + Map bauGui = new HashMap<>(); + bauGui.put(10, 2); bauGui.put(3, 3); bauGui.put(7, 4); bauGui.put(17, 5); bauGui.put(15, 6); + bauGui.put(5, 9); bauGui.put(4, 18); bauGui.put(6, 27); + bauGui.put(16, 17); bauGui.put(11, 26); bauGui.put(18, 35); + bauGui.put(19, 20); bauGui.put(21, 21); bauGui.put(9, 22); bauGui.put(0, 23); bauGui.put(1, 24); + bauGui.put(23, 37); bauGui.put(20, 38); bauGui.put(8, 39); bauGui.put(22, 40); bauGui.put(26, 41); bauGui.put(12, 42); bauGui.put(14, 43); + return bauGui; } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigConverter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigConverter.java deleted file mode 100644 index 46119887a..000000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigConverter.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.bausystem.configplayer; - -import yapion.hierarchy.types.YAPIONObject; - -/** - * A new {@link ConfigConverter} should be written when you remove anything - * from the Config or modify any mayor part. When you move anything from - * any key to any other key you should write a new {@link ConfigConverter}. - * For adding any new key you should be able to get the default without - * having it in the Config. Anything you need to change you should also - * change the {@link ConfigCreator} accordingly, to produce the new Config. - */ -public interface ConfigConverter { - - /** - * This describes the version this Converter can convert from. The version - * it should convert to is the version 1 above this number. But this is not - * a necessity. In the config Object as parameter given in {@link #update(YAPIONObject)} - * you should update the @version variable in the root object to the - * new version this converter produced. - * - * @return the version number - */ - int version(); - - /** - * This method should update everything needed to go from a lower config - * version to a higher. It should update the @version variable - * accordingly. Anything else is up the implementation. If anything goes wrong - * do not silently exit this method, throw an Exception. The updater Code will - * deal with it. Never leave the inputted object in a corrupted state. - * - * @param config the config object to update - */ - void update(YAPIONObject config); - -} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigCreator.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigCreator.java deleted file mode 100644 index 9a8872080..000000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigCreator.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.bausystem.configplayer; - -import de.steamwar.bausystem.features.hotbar.DefaultHotbar; -import lombok.experimental.UtilityClass; -import yapion.hierarchy.types.YAPIONObject; - -@UtilityClass -public class ConfigCreator { - - public static final int currentVersion = 1; - - public YAPIONObject createDefaultConfig() { - YAPIONObject yapionObject = new YAPIONObject(); - // This call should never be touched - yapionObject.add("@version", currentVersion); - - // Any initialising goes into here - yapionObject.add("baugui", defaultBauGui()); - - // Default Hotbar Gui - yapionObject.add("hotbar", DefaultHotbar.defaultHotbar()); - return yapionObject; - } - - private YAPIONObject defaultBauGui() { - YAPIONObject yapionObject = new YAPIONObject(); - - // 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ? - // 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16 - // 18: 4 | 19: ? | 20: 19 | 21: 21 | 22: 9 | 23: 0 | 24: 1 | 25: ? | 26: 11 - // 27: 6 | 28: ? | 29: ? | 30: ? | 31: ? | 32: ? | 33: ? | 34: ? | 35: 18 - // 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ? - - yapionObject.add("10", 2).add("3", 3).add("7", 4).add("17", 5).add("15", 6); - yapionObject.add("5", 9).add("4", 18).add("6", 27); - yapionObject.add("16", 17).add("11", 26).add("18", 35); - yapionObject.add("19", 20).add("21", 21).add("9", 22).add("0", 23).add("1", 24); - yapionObject.add("23", 37).add("20", 38).add("8", 39).add("22", 40).add("26", 41).add("12", 42).add("14", 43); - yapionObject.add("size", 45); - return yapionObject; - } -} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigV1.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigV1.java new file mode 100644 index 000000000..ba1f8cd0c --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/ConfigV1.java @@ -0,0 +1,104 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.configplayer; + +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.config.legacy.YapionReader; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The legacy config that was stored in the YAPION format. + */ +@Getter +@Setter +public class ConfigV1 implements VersionedConfig { + + public static final int VERSION = 1; + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + /** + * BauGui item id to inventory slot + */ + protected Map bauGui = new HashMap<>(); + protected int bauGuiSize = 45; + + /** + * Minecraft version to the hotbar serialized by YAPION, read with {@link YapionReader}. + * Use {@link de.steamwar.bausystem.config.legacy.YapionItemDeserializer} to get the items. + */ + protected Map> hotbars = new HashMap<>(); + + protected boolean nightVision = false; + protected boolean tntClickDetails = true; + protected boolean detonatorAutostart = false; + protected boolean inventoryFill = false; + protected boolean rayVisualizer = false; + protected boolean smartPlace = false; + protected boolean blockCounter = false; + protected boolean depthMessage = true; + + public static ConfigV1 parse(String yapion) { + Map object = YapionReader.parse(yapion); + ConfigV1 config = new ConfigV1(); + config.version = (Integer) object.get("@version"); + + if (object.get("baugui") instanceof Map bauGui) { + bauGui.forEach((key, value) -> { + if (key.equals("size")) { + config.bauGuiSize = (Integer) value; + } else { + config.bauGui.put(Integer.parseInt((String) key), (Integer) value); + } + }); + } + + object.forEach((key, value) -> { + if (!(value instanceof List hotbar)) return; + if (key.equals("hotbar")) { + // Hotbars were stored without version prior to 1.19 + config.hotbars.putIfAbsent(19, hotbar); + } else if (key.startsWith("hotbar-")) { + config.hotbars.put(Integer.parseInt(key.substring("hotbar-".length())), hotbar); + } + }); + + config.nightVision = getBoolean(object, "nightvision", config.nightVision); + config.tntClickDetails = getBoolean(object, "tnt_click_details", config.tntClickDetails); + config.detonatorAutostart = getBoolean(object, "detonator-autostart", config.detonatorAutostart); + config.inventoryFill = getBoolean(object, "inventoryfill", config.inventoryFill); + config.rayVisualizer = getBoolean(object, "rayvisualizer", config.rayVisualizer); + config.smartPlace = getBoolean(object, "smartPlace", config.smartPlace); + config.blockCounter = getBoolean(object, "blockCounter", config.blockCounter); + config.depthMessage = getBoolean(object, "depth_message", config.depthMessage); + return config; + } + + private static boolean getBoolean(Map object, String key, boolean defaultValue) { + return object.get(key) instanceof Boolean value ? value : defaultValue; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/.gitkeep b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/ConfigV1ToV2Converter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/ConfigV1ToV2Converter.java new file mode 100644 index 000000000..9886de525 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/converter/ConfigV1ToV2Converter.java @@ -0,0 +1,75 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.configplayer.converter; + +import de.steamwar.bausystem.config.ConfigConverter; +import de.steamwar.bausystem.config.legacy.YapionItemDeserializer; +import de.steamwar.bausystem.configplayer.Config; +import de.steamwar.bausystem.configplayer.ConfigV1; +import de.steamwar.core.Core; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.List; +import java.util.logging.Level; + +@Deprecated +public class ConfigV1ToV2Converter implements ConfigConverter { + + @Override + public int version() { + return ConfigV1.VERSION; + } + + @Override + public Config convert(ConfigV1 old) { + Config config = new Config(); + config.setBauGui(new HashMap<>(old.getBauGui())); + config.setBauGuiSize(old.getBauGuiSize()); + + List hotbar = old.getHotbars().get(Core.getVersion()); + if (hotbar != null) { + config.setHotbar(convertHotbar(hotbar)); + } + + config.setNightVision(old.isNightVision()); + config.setTntClickDetails(old.isTntClickDetails()); + config.setDetonatorAutostart(old.isDetonatorAutostart()); + config.setInventoryFill(old.isInventoryFill()); + config.setRayVisualizer(old.isRayVisualizer()); + config.setSmartPlace(old.isSmartPlace()); + config.setBlockCounter(old.isBlockCounter()); + config.setDepthMessage(old.isDepthMessage()); + return config; + } + + private ItemStack[] convertHotbar(List hotbar) { + ItemStack[] items = new ItemStack[13]; + for (int i = 0; i < Math.min(hotbar.size(), items.length); i++) { + try { + items[i] = YapionItemDeserializer.deserializeItem(hotbar.get(i)); + } catch (Exception e) { + Bukkit.getLogger().log(Level.WARNING, "Could not convert hotbar item " + i, e); + } + } + return items; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/serializer/ConfigurationSerializableSerializer.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/serializer/ConfigurationSerializableSerializer.java deleted file mode 100644 index 2d054c3dc..000000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/configplayer/serializer/ConfigurationSerializableSerializer.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.bausystem.configplayer.serializer; - -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import org.bukkit.configuration.serialization.ConfigurationSerialization; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import yapion.hierarchy.api.groups.YAPIONAnyType; -import yapion.hierarchy.types.YAPIONObject; -import yapion.serializing.api.SerializerObject; -import yapion.serializing.data.DeserializeData; -import yapion.serializing.data.SerializeData; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import static yapion.utils.IdentifierUtils.TYPE_IDENTIFIER; - -public class ConfigurationSerializableSerializer extends SerializerObject { - - @Override - public Class type() { - return ConfigurationSerializable.class; - } - - @Override - public boolean isInterface() { - return true; - } - - @Override - public YAPIONObject serialize(SerializeData serializeData) { - YAPIONObject yapionObject = new YAPIONObject(); - yapionObject.add(TYPE_IDENTIFIER, serializeData.object.getClass().getTypeName()); - if (serializeData.object instanceof ItemStack) { - yapionObject.add(TYPE_IDENTIFIER, ItemStack.class.getTypeName()); - } - if (serializeData.object instanceof ItemMeta) { - yapionObject.add(TYPE_IDENTIFIER, ItemMeta.class.getTypeName()); - } - Map serializeDataMap = serializeData.object.serialize(); - serializeDataMap.forEach((s, o) -> { - YAPIONAnyType yapionAnyType = serializeData.serialize(o); - if (yapionAnyType instanceof YAPIONObject) { - YAPIONObject object = (YAPIONObject) yapionAnyType; - if (object.containsKey(TYPE_IDENTIFIER) && object.getPlainValue(TYPE_IDENTIFIER).equals("com.google.common.collect.RegularImmutableList")) { - object.put(TYPE_IDENTIFIER, ArrayList.class.getTypeName()); - } - } - yapionObject.add(s, yapionAnyType); - }); - return yapionObject; - } - - @Override - public ConfigurationSerializable deserialize(DeserializeData deserializeData) { - Map deserializeDataMap = new HashMap<>(); - deserializeData.object.forEach((s, yapionAnyType) -> { - if (s.equals(TYPE_IDENTIFIER)) { - if (yapionAnyType.toString().equals("(org.bukkit.inventory.meta.ItemMeta)")) { - deserializeDataMap.put("==", "ItemMeta"); - } else { - deserializeDataMap.put("==", deserializeData.deserialize(yapionAnyType)); - } - return; - } - deserializeDataMap.put(s, deserializeData.deserialize(yapionAnyType)); - }); - return ConfigurationSerialization.deserializeObject(deserializeDataMap); - } -} \ No newline at end of file diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/Depth.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/Depth.java index 89f0daa20..61d16843f 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/Depth.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/Depth.java @@ -64,7 +64,7 @@ public class Depth { dimensions.setZ(Math.abs(dimensions.getZ())); RegionUtils.message(region, player -> { - if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) { + if (Config.STORE.get(player).isDepthMessage()) { player.spigot().sendMessage(getMessage(player, dimensions.getBlockX() + 1, dimensions.getBlockY() + 1, dimensions.getBlockZ() + 1, tntCount)); } }); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/DepthCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/DepthCommand.java index b9f713984..8eee56a63 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/DepthCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/cannon/depth/DepthCommand.java @@ -34,11 +34,11 @@ public class DepthCommand extends SWCommand { @Register public void toggle(Player player) { - if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) { - Config.getInstance().get(player).put("depth_message", false); + if (Config.STORE.get(player).isDepthMessage()) { + Config.STORE.get(player).setDepthMessage(false); BauSystem.MESSAGE.send("DEPTH_COUNTER_DISABLE", player); } else { - Config.getInstance().get(player).put("depth_message", true); + Config.STORE.get(player).setDepthMessage(true); BauSystem.MESSAGE.send("DEPTH_COUNTER_ENABLE", player); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/Detonator.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/Detonator.java index 86932f2eb..e04c78e10 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/Detonator.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/Detonator.java @@ -95,7 +95,7 @@ public class Detonator { public static void activateDetonator(DetonatorStorage detonator) { Player p = detonator.getPlayer(); - if (Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false)) { + if (Config.STORE.get(p).isDetonatorAutostart()) { AutostartListener.instance.activate(p); } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/DetonatorCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/DetonatorCommand.java index 5c936c49d..5ac946e10 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/DetonatorCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/detonator/DetonatorCommand.java @@ -76,8 +76,8 @@ public class DetonatorCommand extends SWCommand { @Register(value = "autostart", description = "DETONATOR_HELP_AUTOSTART") public void toggleAutostartTimer(Player p) { - boolean current = Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false); - Config.getInstance().get(p).put("detonator-autostart", !current); + boolean current = Config.STORE.get(p).isDetonatorAutostart(); + Config.STORE.get(p).setDetonatorAutostart(!current); if (!current) { BauSystem.MESSAGE.send("DETONATOR_AUTOSTART_ENABLE", p); } else { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/gui/editor/BauGuiMapping.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/gui/editor/BauGuiMapping.java index 7dfed54dd..1c0b56b1d 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/gui/editor/BauGuiMapping.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/gui/editor/BauGuiMapping.java @@ -28,7 +28,6 @@ import lombok.Setter; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; -import yapion.hierarchy.types.YAPIONObject; import java.util.*; @@ -52,15 +51,15 @@ public class BauGuiMapping { } @Getter - private final YAPIONObject object; + private final Config config; @Getter private final Player owner; @Getter @Setter private boolean saved; - protected BauGuiMapping(YAPIONObject object, Player p) { - this.object = object; + protected BauGuiMapping(Config config, Player p) { + this.config = config; this.owner = p; fromUUID.put(p.getUniqueId(), this); mappings.add(this); @@ -70,19 +69,18 @@ public class BauGuiMapping { BauGuiMapping mapping = fromUUID.get(p.getUniqueId()); if (mapping == null) { - YAPIONObject yapionObject = Config.getInstance().get(p); - mapping = new BauGuiMapping(yapionObject.getObject("baugui"), p); + mapping = new BauGuiMapping(Config.STORE.get(p), p); } return mapping; } public boolean isShown(int id) { - return object.getPlainValueOrDefault(Integer.toString(id), -1) >= 0; + return config.getBauGui().getOrDefault(id, -1) >= 0; } public int getSlot(int id) { - return object.getPlainValue(Integer.toString(id)); + return config.getBauGui().get(id); } public Map getMapping() { @@ -94,7 +92,7 @@ public class BauGuiMapping { } public int getSize() { - return object.getPlainValueOrDefault("size", 45); + return config.getBauGuiSize(); } public void setSize(int size) { @@ -102,7 +100,7 @@ public class BauGuiMapping { return; } - object.add("size", size); + config.setBauGuiSize(size); } private void tick() { @@ -111,16 +109,17 @@ public class BauGuiMapping { public void save() { if (saved) return; - YAPIONObject config = Config.getInstance().get(owner); - config.add("baugui", object); - Config.getInstance().save(owner); + Config current = Config.STORE.get(owner); + current.setBauGui(config.getBauGui()); + current.setBauGuiSize(config.getBauGuiSize()); + Config.STORE.save(owner); saved = true; } private Map internalReadMap() { Map map = new HashMap<>(); for (Map.Entry e : BauGUI.getITEMS().entrySet()) { - Integer value = object.getPlainValueOrDefault(e.getKey().toString(), -1); + Integer value = config.getBauGui().getOrDefault(e.getKey(), -1); if (value == -1) continue; map.put(e.getKey(), value); } @@ -129,10 +128,10 @@ public class BauGuiMapping { private void internalWriteMap(Map map) { for (Map.Entry e : BauGUI.getITEMS().entrySet()) { - object.remove(e.getKey().toString()); + config.getBauGui().remove(e.getKey()); Integer value = map.getOrDefault(e.getKey(), -1); if (value == -1) continue; - object.add(e.getKey().toString(), value); + config.getBauGui().put(e.getKey(), value); } } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hotbar/DefaultHotbar.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hotbar/DefaultHotbar.java index 602f18172..c8340a7c0 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hotbar/DefaultHotbar.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hotbar/DefaultHotbar.java @@ -20,39 +20,23 @@ package de.steamwar.bausystem.features.hotbar; import de.steamwar.bausystem.configplayer.Config; -import de.steamwar.core.Core; import lombok.experimental.UtilityClass; +import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.hierarchy.types.YAPIONValue; -import yapion.parser.YAPIONParser; -import yapion.serializing.YAPIONDeserializer; -import yapion.serializing.YAPIONSerializer; +import org.bukkit.inventory.meta.ItemMeta; -import java.util.HashSet; -import java.util.Set; +import java.util.List; @UtilityClass public class DefaultHotbar { - private final YAPIONArray DEFAULT_HOTBAR = YAPIONParser.parse("{[{@type(org.bukkit.inventory.ItemStack)v(2230)type(WOODEN_AXE)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(WorldEdit Wand)lore{@type(java.util.ArrayList)values[Left click: select pos #1,Right click: select pos #2]}}},{@type(org.bukkit.inventory.ItemStack)v(2230)type(COMPASS)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(Navigation Wand)lore{@type(java.util.ArrayList)values[Left click: jump to location,Right click: pass through walls]}}},null,null,null,null,null,null,{@type(org.bukkit.inventory.ItemStack)v(2230)type(NETHER_STAR)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(§eBau GUI)}},null,null,{@type(org.bukkit.inventory.ItemStack)v(3117)type(ELYTRA)},null]}").getArray(""); - public void updateHotbar(Player p) { ItemStack[] hotbar = new ItemStack[13]; System.arraycopy(p.getInventory().getContents(), 0, hotbar, 0, 9); System.arraycopy(p.getInventory().getArmorContents(), 0, hotbar, 9, 4); - YAPIONArray yapionArray = new YAPIONArray(); - for (ItemStack itemStack : hotbar) { - if (itemStack != null) { - yapionArray.add(YAPIONSerializer.serialize(itemStack)); - } else { - yapionArray.add(new YAPIONValue<>(null)); - } - } - Config.getInstance().get(p).add("hotbar-" + Core.getVersion(), yapionArray); - Config.getInstance().save(p); + Config.STORE.get(p).setHotbar(copy(hotbar)); + Config.STORE.save(p); } public void setHotbar(Player p) { @@ -66,28 +50,34 @@ public class DefaultHotbar { } public ItemStack[] getItems(Player p) { - Config.getInstance().get(p).remap("hotbar", "hotbar-19"); - YAPIONArray yapionArray = Config.getInstance().get(p).getYAPIONArrayOrSetDefault("hotbar-" + Core.getVersion(), defaultHotbar()); + return copy(Config.STORE.get(p).getHotbar()); + } + + public ItemStack[] defaultHotbar() { ItemStack[] hotbar = new ItemStack[13]; - Set invalid = new HashSet<>(); - yapionArray.forEach((integer, yapionAnyType) -> { - if (yapionAnyType instanceof YAPIONValue) { - hotbar[integer] = null; - } else { - try { - hotbar[integer] = YAPIONDeserializer.deserialize((YAPIONObject) yapionAnyType); - } catch (Exception e) { - invalid.add(integer); - hotbar[integer] = null; - } - } - }); - invalid.forEach(i -> yapionArray.set(i, new YAPIONValue<>(null))); - if (!invalid.isEmpty()) Config.getInstance().save(p); + hotbar[0] = item(Material.WOODEN_AXE, "WorldEdit Wand", "Left click: select pos #1", "Right click: select pos #2"); + hotbar[1] = item(Material.COMPASS, "Navigation Wand", "Left click: jump to location", "Right click: pass through walls"); + hotbar[8] = item(Material.NETHER_STAR, "§eBau GUI"); + hotbar[11] = new ItemStack(Material.ELYTRA); return hotbar; } - public YAPIONArray defaultHotbar() { - return DEFAULT_HOTBAR; + private ItemStack item(Material material, String name, String... lore) { + ItemStack itemStack = new ItemStack(material); + ItemMeta meta = itemStack.getItemMeta(); + meta.setDisplayName(name); + if (lore.length > 0) { + meta.setLore(List.of(lore)); + } + itemStack.setItemMeta(meta); + return itemStack; + } + + private ItemStack[] copy(ItemStack[] hotbar) { + ItemStack[] copy = new ItemStack[13]; + for (int i = 0; i < Math.min(hotbar.length, copy.length); i++) { + copy[i] = hotbar[i] == null ? null : hotbar[i].clone(); + } + return copy; } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillBauGuiItem.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillBauGuiItem.java index 13e3af35a..e23d71de6 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillBauGuiItem.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillBauGuiItem.java @@ -49,7 +49,7 @@ public class InventoryFillBauGuiItem extends BauGuiItem { @Override public ItemStack getItem(Player player) { - String loreKey = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false) ? "OTHER_ITEMS_INVENTORY_FILL_LORE_ACTIVE" : "OTHER_ITEMS_INVENTORY_FILL_LORE_INACTIVE"; + String loreKey = Config.STORE.get(player).isInventoryFill() ? "OTHER_ITEMS_INVENTORY_FILL_LORE_ACTIVE" : "OTHER_ITEMS_INVENTORY_FILL_LORE_INACTIVE"; return new SWItem(Material.HOPPER, BauSystem.MESSAGE.parse("OTHER_ITEMS_INVENTORY_FILL_NAME", player), Collections.singletonList(BauSystem.MESSAGE.parse(loreKey, player)), false, clickType -> { }).getItemStack(); } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFiller.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFiller.java index 8312e77c3..483411529 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFiller.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFiller.java @@ -40,7 +40,7 @@ public class InventoryFiller implements Listener { @EventHandler public void onPlayerDropItem(PlayerDropItemEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("inventoryfill", false)) return; + if (!Config.STORE.get(event.getPlayer()).isInventoryFill()) return; if (!event.getPlayer().isSneaking()) return; Block block = event.getPlayer().getTargetBlockExact(5); if (block == null) return; @@ -62,7 +62,7 @@ public class InventoryFiller implements Listener { @EventHandler public void onPlayerItemHeld(PlayerItemHeldEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("inventoryfill", false)) return; + if (!Config.STORE.get(event.getPlayer()).isInventoryFill()) return; if (!event.getPlayer().isSneaking()) return; ItemStack itemStack = event.getPlayer().getInventory().getItemInMainHand(); if (itemStack.getType() == Material.AIR) return; diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillerCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillerCommand.java index 0e9887ab9..4d4b5f054 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillerCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/inventoryfiller/InventoryFillerCommand.java @@ -68,8 +68,8 @@ public class InventoryFillerCommand extends SWCommand { @Register(description = {"INVENTORY_FILL_HELP", "INVENTORY_FILL_INFO"}) public void toggle(Player player) { - boolean inventoryFill = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false); - Config.getInstance().get(player).put("inventoryfill", !inventoryFill); + boolean inventoryFill = Config.STORE.get(player).isInventoryFill(); + Config.STORE.get(player).setInventoryFill(!inventoryFill); if (!inventoryFill) { SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("INVENTORY_FILL_ENABLE", player)); BauSystem.MESSAGE.send("INVENTORY_FILL_INFO", player); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java index 987b2a232..d823bff4e 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java @@ -46,7 +46,7 @@ public class ObserverTracerListener implements Listener { public ObserverTracerListener() { Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> { SWPlayer.allWithSingleComponent(ObserverTracer.class).forEach(pair -> { - if (pair.getPlayer().getGameMode() != GameMode.SPECTATOR) return; + if (pair.getSwPlayer().getPlayer().getGameMode() != GameMode.SPECTATOR) return; pair.getComponent().show(); }); }, 15L, 15L); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java index 2732c1402..af5d4eb07 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java @@ -167,7 +167,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - boolean rayvisualizer = Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("rayvisualizer", false); + boolean rayvisualizer = Config.STORE.get(event.getPlayer()).isRayVisualizer(); if (rayvisualizer) server.addPlayer(event.getPlayer()); } @@ -181,7 +181,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener { Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { for (Player player : Bukkit.getOnlinePlayers()) { if (Permission.BUILD.hasPermission(player)) { - boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false); + boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer(); if (rayvisualizer) server.addPlayer(player); } else { server.removePlayer(player); @@ -192,8 +192,8 @@ public class RayVisualizerCommand extends SWCommand implements Listener { @Register public void toggle(@Validator Player player) { - boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false); - Config.getInstance().get(player).put("rayvisualizer", !rayvisualizer); + boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer(); + Config.STORE.get(player).setRayVisualizer(!rayvisualizer); if (!rayvisualizer) { SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("RAY_VISUALIZER_ENABLE", player)); server.addPlayer(player); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorElement.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorElement.java index 87719f2ab..27538d7b1 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorElement.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorElement.java @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import de.steamwar.inventory.InvCallback; import de.steamwar.inventory.SWItem; import lombok.Getter; @@ -28,7 +29,6 @@ import lombok.Setter; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; import java.util.ArrayList; import java.util.Comparator; @@ -121,9 +121,9 @@ public abstract class SimulatorElement { public abstract String getType(); - public void saveExtra(YAPIONObject elementObject) { + public void saveExtra(SimulatorConfig.Element element) { } - public void loadExtra(YAPIONObject elementObject) { + public void loadExtra(SimulatorConfig.Element element) { } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorPhase.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorPhase.java index b8c951abd..2eae0d824 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorPhase.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/SimulatorPhase.java @@ -20,10 +20,10 @@ package de.steamwar.bausystem.features.simulator.data; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import lombok.Getter; import lombok.Setter; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; import java.util.function.BiConsumer; @@ -38,9 +38,9 @@ public abstract class SimulatorPhase { public abstract void toSimulatorActions(Vector position, BiConsumer tickStart, BiConsumer tickEnd); - public void saveExtra(YAPIONObject phaseObject) { + public void saveExtra(SimulatorConfig.Phase phase) { } - public void loadExtra(YAPIONObject phaseObject) { + public void loadExtra(SimulatorConfig.Phase phase) { } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/observer/ObserverPhase.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/observer/ObserverPhase.java index f69ff797b..73c00d191 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/observer/ObserverPhase.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/observer/ObserverPhase.java @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data.observer; import de.steamwar.bausystem.features.simulator.data.SimulatorPhase; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -31,7 +32,6 @@ import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.block.data.type.Observer; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; import java.util.function.BiConsumer; @@ -70,12 +70,12 @@ public final class ObserverPhase extends SimulatorPhase { } @Override - public void saveExtra(YAPIONObject phaseObject) { - phaseObject.add("orientation", orientation.name()); + public void saveExtra(SimulatorConfig.Phase phase) { + phase.setOrientation(orientation); } @Override - public void loadExtra(YAPIONObject phaseObject) { - orientation = BlockFace.valueOf(phaseObject.getPlainValue("orientation")); + public void loadExtra(SimulatorConfig.Phase phase) { + if (phase.getOrientation() != null) orientation = phase.getOrientation(); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTElement.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTElement.java index 735ca2927..a8353907e 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTElement.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTElement.java @@ -24,13 +24,13 @@ import de.steamwar.bausystem.features.simulator.data.SimulatorElement; import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; import de.steamwar.bausystem.features.simulator.gui.SimulatorTNTGui; import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import de.steamwar.inventory.InvCallback; import de.steamwar.inventory.SWItem; import lombok.Getter; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; public final class TNTElement extends SimulatorElement { @@ -100,14 +100,14 @@ public final class TNTElement extends SimulatorElement { } @Override - public void saveExtra(YAPIONObject elementObject) { - elementObject.add("alignmentX", alignment.getX()); - elementObject.add("alignmentZ", alignment.getZ()); + public void saveExtra(SimulatorConfig.Element element) { + element.setAlignmentX(alignment.getX()); + element.setAlignmentZ(alignment.getZ()); } @Override - public void loadExtra(YAPIONObject elementObject) { - alignment.setX(elementObject.getDoubleOrSetDefault("alignmentX", 0)); - alignment.setZ(elementObject.getDoubleOrSetDefault("alignmentZ", 0)); + public void loadExtra(SimulatorConfig.Element element) { + alignment.setX(element.getAlignmentX() != null ? element.getAlignmentX() : 0); + alignment.setZ(element.getAlignmentZ() != null ? element.getAlignmentZ() : 0); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTPhase.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTPhase.java index bb5be0cee..bb0f6eb20 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTPhase.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/data/tnt/TNTPhase.java @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data.tnt; import de.steamwar.bausystem.features.simulator.data.SimulatorPhase; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; +import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig; import de.steamwar.bausystem.region.Region; import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.FreezeMode; @@ -31,7 +32,6 @@ import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.TNTPrimed; import org.bukkit.util.Vector; -import yapion.hierarchy.types.YAPIONObject; import java.util.function.BiConsumer; @@ -83,18 +83,18 @@ public final class TNTPhase extends SimulatorPhase { } @Override - public void saveExtra(YAPIONObject phaseObject) { - phaseObject.add("count", count); - phaseObject.add("xJump", xJump); - phaseObject.add("yJump", yJump); - phaseObject.add("zJump", zJump); + public void saveExtra(SimulatorConfig.Phase phase) { + phase.setCount(count); + phase.setXJump(xJump); + phase.setYJump(yJump); + phase.setZJump(zJump); } @Override - public void loadExtra(YAPIONObject phaseObject) { - count = phaseObject.getPlainValue("count"); - xJump = phaseObject.getPlainValue("xJump"); - yJump = phaseObject.getPlainValue("yJump"); - zJump = phaseObject.getPlainValue("zJump"); + public void loadExtra(SimulatorConfig.Phase phase) { + if (phase.getCount() != null) count = phase.getCount(); + if (phase.getXJump() != null) xJump = phase.getXJump(); + if (phase.getYJump() != null) yJump = phase.getYJump(); + if (phase.getZJump() != null) zJump = phase.getZJump(); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimFormatSimulatorLoader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimFormatSimulatorLoader.java index 5257ce12e..9310fa2d1 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimFormatSimulatorLoader.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimFormatSimulatorLoader.java @@ -19,6 +19,7 @@ package de.steamwar.bausystem.features.simulator.storage; +import com.google.gson.JsonParseException; import de.steamwar.bausystem.features.simulator.data.Simulator; import de.steamwar.bausystem.features.simulator.data.SimulatorElement; import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; @@ -29,12 +30,7 @@ import de.steamwar.bausystem.features.simulator.data.redstone.RedstoneElement; import de.steamwar.bausystem.features.simulator.data.redstone.RedstonePhase; import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement; import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase; -import org.bukkit.Material; import org.bukkit.util.Vector; -import yapion.exceptions.YAPIONException; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; import java.io.File; import java.io.IOException; @@ -51,44 +47,41 @@ public class SimFormatSimulatorLoader implements SimulatorLoader { return Optional.empty(); } - YAPIONObject yapionObject; + SimulatorConfig config; try { - yapionObject = YAPIONParser.parse(file); - } catch (YAPIONException | IOException e) { + config = SimulatorConfig.STORE.load(file); + } catch (JsonParseException | IllegalStateException | IOException e) { return Optional.empty(); } String name = file.getName().substring(0, file.getName().length() - ".sim".length()); Simulator simulator = new Simulator(name); - loadSimulator(yapionObject, simulator); + loadSimulator(config, simulator); return Optional.of(Collections.singletonList(simulator)); } - private void loadSimulator(YAPIONObject simulatorObject, Simulator simulator) { - simulator.setMaterial(Material.valueOf(simulatorObject.getPlainValue("material"))); - simulator.setAutoTrace(simulatorObject.getPlainValue("autoTrace")); - simulator.setAutoTestblock(simulatorObject.getPlainValueOrDefault("autoTestblock", false)); + private void loadSimulator(SimulatorConfig config, Simulator simulator) { + if (config.getMaterial() != null) simulator.setMaterial(config.getMaterial()); + simulator.setAutoTrace(config.isAutoTrace()); + simulator.setAutoTestblock(config.isAutoTestblock()); - YAPIONArray groups = simulatorObject.getArray("groups"); - groups.streamObject().forEach(groupObject -> { + config.getGroups().forEach(groupConfig -> { SimulatorGroup simulatorGroup = new SimulatorGroup(); - loadGroup(groupObject, simulatorGroup); + loadGroup(groupConfig, simulatorGroup); simulator.add(simulatorGroup); }); } - private void loadGroup(YAPIONObject groupObject, SimulatorGroup group) { - group.setMaterial(Material.valueOf(groupObject.getPlainValue("material"))); - group.setDisabled(groupObject.getPlainValue("disabled")); + private void loadGroup(SimulatorConfig.Group groupConfig, SimulatorGroup group) { + if (groupConfig.getMaterial() != null) group.setMaterial(groupConfig.getMaterial()); + group.setDisabled(groupConfig.isDisabled()); - YAPIONArray elements = groupObject.getArray("elements"); - elements.streamObject().forEach(elementObject -> { + groupConfig.getElements().forEach(elementConfig -> { SimulatorElement element; Supplier phaseConstructor; - Vector position = new Vector(elementObject.getDouble("x"), elementObject.getDouble("y"), elementObject.getDouble("z")); - String type = elementObject.getPlainValue("type"); - switch (type) { + Vector position = new Vector(elementConfig.getX(), elementConfig.getY(), elementConfig.getZ()); + switch (String.valueOf(elementConfig.getType())) { case "TNT": element = new TNTElement(position); phaseConstructor = TNTPhase::new; @@ -109,28 +102,27 @@ public class SimFormatSimulatorLoader implements SimulatorLoader { if (element == null) { return; } - loadElement(elementObject, element, phaseConstructor); + loadElement(elementConfig, element, phaseConstructor); group.add(element); }); } - private void loadElement(YAPIONObject elementObject, SimulatorElement element, Supplier phaseConstructor) { - element.setMaterial(Material.valueOf(elementObject.getPlainValue("material"))); - element.setDisabled(elementObject.getPlainValue("disabled")); - element.loadExtra(elementObject); + private void loadElement(SimulatorConfig.Element elementConfig, SimulatorElement element, Supplier phaseConstructor) { + if (elementConfig.getMaterial() != null) element.setMaterial(elementConfig.getMaterial()); + element.setDisabled(elementConfig.isDisabled()); + element.loadExtra(elementConfig); - YAPIONArray phases = elementObject.getArray("phases"); - phases.streamObject().forEach(phaseObject -> { + elementConfig.getPhases().forEach(phaseConfig -> { SimulatorPhase phase = phaseConstructor.get(); - loadPhase(phaseObject, phase); + loadPhase(phaseConfig, phase); ((SimulatorElement) element).add(phase); }); } - private void loadPhase(YAPIONObject phaseObject, SimulatorPhase phase) { - phase.setTickOffset(phaseObject.getPlainValue("tickOffset")); - phase.setLifetime(phaseObject.getPlainValue("lifetime")); - phase.setOrder(phaseObject.getPlainValue("order")); - phase.loadExtra(phaseObject); + private void loadPhase(SimulatorConfig.Phase phaseConfig, SimulatorPhase phase) { + phase.setTickOffset(phaseConfig.getTickOffset()); + phase.setLifetime(phaseConfig.getLifetime()); + phase.setOrder(phaseConfig.getOrder()); + phase.loadExtra(phaseConfig); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorConfig.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorConfig.java new file mode 100644 index 000000000..a1c56f867 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorConfig.java @@ -0,0 +1,117 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.features.simulator.storage; + +import de.steamwar.bausystem.config.ConfigStore; +import de.steamwar.bausystem.config.VersionedConfig; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Material; +import org.bukkit.block.BlockFace; + +import java.util.ArrayList; +import java.util.List; + +/** + * The current stored form of a simulator, saved as JSON in a .sim file. Files + * saved before the version was stored have no version and are version 1. + * Fields missing in the stored JSON keep the default value of their initializer, + * so adding a new field does not need a new version. + *

+ * When a field gets removed, renamed or its meaning changes, copy this class + * to SimulatorConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a + * {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to + * this class and register it in the {@link #STORE}. + */ +@Getter +@Setter +public class SimulatorConfig implements VersionedConfig { + + public static final int VERSION = 1; + + /** + * Files saved before the version was stored are version 1 + */ + public static final ConfigStore STORE = new ConfigStore<>(SimulatorConfig.class, VERSION, SimulatorConfig::new) + .unversioned(1); + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + protected Material material = Material.BARREL; + protected boolean autoTrace = false; + protected boolean autoTestblock = false; + protected List groups = new ArrayList<>(); + + @Getter + @Setter + public static class Group { + + protected Material material = Material.CHEST; + protected boolean disabled = false; + protected List elements = new ArrayList<>(); + } + + @Getter + @Setter + public static class Element { + + /** + * One of "TNT", "Redstone" or "Observer" + */ + protected String type; + protected Material material; + protected boolean disabled = false; + protected double x; + protected double y; + protected double z; + + /** + * Only used by TNT + */ + protected Double alignmentX; + protected Double alignmentZ; + + protected List phases = new ArrayList<>(); + } + + @Getter + @Setter + public static class Phase { + + protected int tickOffset; + protected int lifetime; + protected int order; + + /** + * Only used by TNT + */ + protected Integer count; + protected Boolean xJump; + protected Boolean yJump; + protected Boolean zJump; + + /** + * Only used by Observer + */ + protected BlockFace orientation; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorFormatSimulatorLoader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorFormatSimulatorLoader.java index 658154404..a7f52ede8 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorFormatSimulatorLoader.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorFormatSimulatorLoader.java @@ -19,24 +19,30 @@ package de.steamwar.bausystem.features.simulator.storage; +import de.steamwar.bausystem.config.legacy.YapionReader; import de.steamwar.bausystem.features.simulator.data.Simulator; import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement; import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase; import org.bukkit.Material; import org.bukkit.util.Vector; -import yapion.exceptions.YAPIONException; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; +import static de.steamwar.bausystem.config.legacy.YapionReader.*; + +/** + * Loads the legacy .simulator files stored as YAPION, they are deleted after loading + * and saved in the current format by the {@link SimulatorSaver}. + */ +@Deprecated public class SimulatorFormatSimulatorLoader implements SimulatorLoader { @Override @@ -45,20 +51,19 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { return Optional.empty(); } - YAPIONObject yapionObject; + Map yapionObject; try { - yapionObject = YAPIONParser.parse(file); - } catch (YAPIONException | IOException e) { + yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IllegalArgumentException | IOException e) { return Optional.empty(); } String name = file.getName().substring(0, file.getName().length() - ".simulator".length()); Simulator simulator = new Simulator(name); - simulator.setMaterial(Material.valueOf(yapionObject.getStringOrDefault("material", "BARREL"))); + simulator.setMaterial(Material.valueOf(getString(yapionObject, "material", "BARREL"))); - YAPIONArray tntElements = yapionObject.getArrayOrDefault("tntElements", new YAPIONArray()); - for (YAPIONObject element : tntElements.streamObject().collect(Collectors.toList())) { + for (Map element : getObjects(yapionObject, "tntElements")) { if (element.containsKey("elements")) { simulator.add(loadGroup(element)); } else { @@ -70,27 +75,26 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { return Optional.of(Collections.singletonList(simulator)); } - private SimulatorGroup loadGroup(YAPIONObject element) { - double x = element.getDoubleOrDefault("x", 0); - double y = element.getDoubleOrDefault("y", 0); - double z = element.getDoubleOrDefault("z", 0); - int tickOffset = element.getIntOrDefault("tickOffset", 0); + private SimulatorGroup loadGroup(Map element) { + double x = getDouble(element, "x", 0); + double y = getDouble(element, "y", 0); + double z = getDouble(element, "z", 0); + int tickOffset = getInt(element, "tickOffset", 0); SimulatorGroup simulatorGroup = new SimulatorGroup(); - simulatorGroup.setDisabled(element.getBooleanOrDefault("disabled", false)); + simulatorGroup.setDisabled(getBoolean(element, "disabled", false)); - YAPIONArray elements = element.getArrayOrDefault("elements", new YAPIONArray()); - for (YAPIONObject e : elements.streamObject().collect(Collectors.toList())) { + for (Map e : getObjects(element, "elements")) { simulatorGroup.add(loadElement(e, x, y, z, tickOffset)); } return simulatorGroup; } - private TNTElement loadElement(YAPIONObject element, double dx, double dy, double dz, int dTickOffset) { - TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x") + dx, element.getDouble("y") + dy, element.getDouble("z") + dz)); - tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); - tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); + private TNTElement loadElement(Map element, double dx, double dy, double dz, int dTickOffset) { + TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x") + dx, requireDouble(element, "y") + dy, requireDouble(element, "z") + dz)); + tntElement.setDisabled(getBoolean(element, "disabled", false)); + tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT"))); TNTPhase tntPhase = loadPhase(element); tntPhase.setTickOffset(tntPhase.getTickOffset() + dTickOffset); @@ -99,12 +103,12 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { return tntElement; } - private SimulatorGroup loadElement(YAPIONObject element) { + private SimulatorGroup loadElement(Map element) { SimulatorGroup simulatorGroup = new SimulatorGroup(); - TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x"), element.getDouble("y"), element.getDouble("z"))); - tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); - tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); + TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x"), requireDouble(element, "y"), requireDouble(element, "z"))); + tntElement.setDisabled(getBoolean(element, "disabled", false)); + tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT"))); simulatorGroup.add(tntElement); tntElement.add(loadPhase(element)); @@ -112,15 +116,15 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { return simulatorGroup; } - private TNTPhase loadPhase(YAPIONObject element) { + private TNTPhase loadPhase(Map element) { TNTPhase tntPhase = new TNTPhase(); - tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80)); - tntPhase.setCount(element.getIntOrDefault("count", 1)); - tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0)); - tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false)); - tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false)); - tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false)); - switch (element.getStringOrDefault("order", "REPEATER")) { + tntPhase.setLifetime(getInt(element, "fuseTicks", 80)); + tntPhase.setCount(getInt(element, "count", 1)); + tntPhase.setTickOffset(getInt(element, "tickOffset", 0)); + tntPhase.setXJump(getBoolean(element, "xVelocity", false)); + tntPhase.setYJump(getBoolean(element, "yVelocity", false)); + tntPhase.setZJump(getBoolean(element, "zVelocity", false)); + switch (getString(element, "order", "REPEATER")) { case "REPEATER": tntPhase.setOrder(0); break; @@ -133,4 +137,8 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader { } return tntPhase; } + + private double requireDouble(Map element, String key) { + return ((Number) element.get(key)).doubleValue(); + } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorSaver.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorSaver.java index 1f3345e48..bfa278e77 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorSaver.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/SimulatorSaver.java @@ -21,9 +21,6 @@ package de.steamwar.bausystem.features.simulator.storage; import de.steamwar.bausystem.features.simulator.data.Simulator; import lombok.experimental.UtilityClass; -import yapion.hierarchy.output.FileOutput; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; import java.io.File; import java.io.IOException; @@ -36,48 +33,42 @@ public class SimulatorSaver { directory.mkdirs(); } - YAPIONObject simulatorObject = new YAPIONObject(); - simulatorObject.add("material", simulator.getMaterial().name()); - simulatorObject.add("autoTrace", simulator.isAutoTrace()); - simulatorObject.add("autoTestblock", simulator.isAutoTestblock()); + SimulatorConfig config = new SimulatorConfig(); + config.setMaterial(simulator.getMaterial()); + config.setAutoTrace(simulator.isAutoTrace()); + config.setAutoTestblock(simulator.isAutoTestblock()); - YAPIONArray groups = new YAPIONArray(); simulator.getGroups().forEach(group -> { - YAPIONObject groupObject = new YAPIONObject(); - groupObject.add("material", group.getMaterial().name()); - groupObject.add("disabled", group.isDisabled()); + SimulatorConfig.Group groupConfig = new SimulatorConfig.Group(); + groupConfig.setMaterial(group.getMaterial()); + groupConfig.setDisabled(group.isDisabled()); - YAPIONArray elements = new YAPIONArray(); group.getElements().forEach(element -> { - YAPIONObject elementObject = new YAPIONObject(); - elementObject.add("type", element.getType()); - elementObject.add("material", element.getMaterial().name()); - elementObject.add("disabled", element.isDisabled()); - elementObject.add("x", element.getPosition().getX()); - elementObject.add("y", element.getPosition().getY()); - elementObject.add("z", element.getPosition().getZ()); - element.saveExtra(elementObject); + SimulatorConfig.Element elementConfig = new SimulatorConfig.Element(); + elementConfig.setType(element.getType()); + elementConfig.setMaterial(element.getMaterial()); + elementConfig.setDisabled(element.isDisabled()); + elementConfig.setX(element.getPosition().getX()); + elementConfig.setY(element.getPosition().getY()); + elementConfig.setZ(element.getPosition().getZ()); + element.saveExtra(elementConfig); - YAPIONArray phases = new YAPIONArray(); element.getPhases().forEach(phase -> { - YAPIONObject phaseObject = new YAPIONObject(); - phaseObject.add("tickOffset", phase.getTickOffset()); - phaseObject.add("lifetime", phase.getLifetime()); - phaseObject.add("order", phase.getOrder()); - phase.saveExtra(phaseObject); - phases.add(phaseObject); + SimulatorConfig.Phase phaseConfig = new SimulatorConfig.Phase(); + phaseConfig.setTickOffset(phase.getTickOffset()); + phaseConfig.setLifetime(phase.getLifetime()); + phaseConfig.setOrder(phase.getOrder()); + phase.saveExtra(phaseConfig); + elementConfig.getPhases().add(phaseConfig); }); - elementObject.add("phases", phases); - elements.add(elementObject); + groupConfig.getElements().add(elementConfig); }); - groupObject.add("elements", elements); - groups.add(groupObject); + config.getGroups().add(groupConfig); }); - simulatorObject.add("groups", groups); File file = new File(directory, simulator.getName() + ".sim"); try { - simulatorObject.toJSONLossy(new FileOutput(file)).close(); + SimulatorConfig.STORE.save(file, config); } catch (IOException e) { // Ignore } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/YAPIONFormatSimulatorLoader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/YAPIONFormatSimulatorLoader.java index 6111eb328..a3c773c06 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/YAPIONFormatSimulatorLoader.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/storage/YAPIONFormatSimulatorLoader.java @@ -19,6 +19,7 @@ package de.steamwar.bausystem.features.simulator.storage; +import de.steamwar.bausystem.config.legacy.YapionReader; import de.steamwar.bausystem.features.simulator.data.Simulator; import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement; @@ -26,18 +27,23 @@ import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase; import de.steamwar.sql.SteamwarUser; import org.bukkit.Material; import org.bukkit.util.Vector; -import yapion.exceptions.YAPIONException; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; +import static de.steamwar.bausystem.config.legacy.YapionReader.*; + +/** + * Loads the legacy per player .yapion files containing multiple simulators, they are + * deleted after loading and saved in the current format by the {@link SimulatorSaver}. + */ +@Deprecated public class YAPIONFormatSimulatorLoader implements SimulatorLoader { @Override @@ -46,10 +52,10 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader { return Optional.empty(); } - YAPIONObject yapionObject; + Map yapionObject; try { - yapionObject = YAPIONParser.parse(file); - } catch (YAPIONException | IOException e) { + yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IllegalArgumentException | IOException e) { return Optional.empty(); } @@ -57,28 +63,28 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader { SteamwarUser steamwarUser = SteamwarUser.byId(Integer.parseInt(name)); List simulators = new ArrayList<>(); - for (String s : yapionObject.getKeys()) { + for (String s : yapionObject.keySet()) { String newName = steamwarUser.getUserName() + (s.isEmpty() ? "" : "_" + s); - YAPIONArray content = yapionObject.getArray(s); + List> content = getObjects(yapionObject, s); if (content.isEmpty()) continue; Simulator simulator = new Simulator(newName); - for (YAPIONObject element : content.streamObject().collect(Collectors.toList())) { + for (Map element : content) { SimulatorGroup simulatorGroup = new SimulatorGroup(); simulator.add(simulatorGroup); - TNTElement tntElement = new TNTElement(new Vector(element.getDouble("positionX"), element.getDouble("positionY"), element.getDouble("positionZ"))); - tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); // IDK if this existed back then? - tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); + TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "positionX"), requireDouble(element, "positionY"), requireDouble(element, "positionZ"))); + tntElement.setDisabled(getBoolean(element, "disabled", false)); // IDK if this existed back then? + tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT"))); simulatorGroup.add(tntElement); TNTPhase tntPhase = new TNTPhase(); - tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80)); - tntPhase.setCount(element.getIntOrDefault("count", 1)); - tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0)); - tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false)); - tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false)); - tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false)); - tntPhase.setOrder(element.getBooleanOrDefault("comparator", false) ? 2 : 0); + tntPhase.setLifetime(getInt(element, "fuseTicks", 80)); + tntPhase.setCount(getInt(element, "count", 1)); + tntPhase.setTickOffset(getInt(element, "tickOffset", 0)); + tntPhase.setXJump(getBoolean(element, "xVelocity", false)); + tntPhase.setYJump(getBoolean(element, "yVelocity", false)); + tntPhase.setZJump(getBoolean(element, "zVelocity", false)); + tntPhase.setOrder(getBoolean(element, "comparator", false) ? 2 : 0); tntElement.add(tntPhase); } simulators.add(simulator); @@ -87,4 +93,8 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader { file.delete(); return Optional.of(simulators); } + + private double requireDouble(Map element, String key) { + return ((Number) element.get(key)).doubleValue(); + } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfig.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfig.java new file mode 100644 index 000000000..c422807c9 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfig.java @@ -0,0 +1,64 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.features.slaves.laufbau; + +import de.steamwar.bausystem.config.ConfigStore; +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.features.slaves.laufbau.converter.LaufbauConfigV1ToV2Converter; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +/** + * The current laufbau config of a player. It is stored as JSON, fields missing + * in the stored JSON keep the default value of their initializer, so adding a + * new field does not need a new version. + *

+ * When a field gets removed, renamed or its meaning changes, copy this class + * to LaufbauConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a + * {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to + * this class and register it in the {@link #STORE}. + */ +@Getter +@Setter +public class LaufbauConfig implements VersionedConfig { + + public static final int VERSION = 2; + + /** + * LaufbauConfigV1 is stored as Base64 of gzipped YAPION, the current version as JSON + */ + public static final ConfigStore STORE = new ConfigStore<>(LaufbauConfig.class, VERSION, LaufbauConfig::new) + .legacy(s -> !s.startsWith("{"), LaufbauConfigV1::parse) + .converter(new LaufbauConfigV1ToV2Converter()) + .userConfig("bausystem-laufbau") + .removeIf(config -> config.getDeactivated().isEmpty()); + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + /** + * Identifiers of the deactivated {@link BlockBoundingBox}es, see {@link LaufbauUtils#identifier(BlockBoundingBox)} + */ + protected Set deactivated = new HashSet<>(); +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfigV1.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfigV1.java new file mode 100644 index 000000000..d372ed5df --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauConfigV1.java @@ -0,0 +1,65 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.features.slaves.laufbau; + +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.config.legacy.YapionReader; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.GZIPInputStream; + +/** + * The legacy laufbau config that was stored as Base64 of the gzipped YAPION, + * an object with the deactivated identifiers as keys. + */ +@Getter +@Setter +@Deprecated +public class LaufbauConfigV1 implements VersionedConfig { + + public static final int VERSION = 1; + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + protected Set deactivated = new HashSet<>(); + + public static LaufbauConfigV1 parse(String base64) { + String yapion; + try (InputStream inputStream = new GZIPInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(base64)))) { + yapion = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + LaufbauConfigV1 config = new LaufbauConfigV1(); + config.deactivated.addAll(YapionReader.parse(yapion).keySet()); + return config; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauUtils.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauUtils.java index a642c22dd..45b944823 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauUtils.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/LaufbauUtils.java @@ -20,63 +20,22 @@ package de.steamwar.bausystem.features.slaves.laufbau; import de.steamwar.bausystem.BauSystem; -import de.steamwar.core.SWPlayer; import de.steamwar.inventory.SWItem; -import de.steamwar.sql.UserConfig; -import lombok.Cleanup; import lombok.SneakyThrows; import lombok.experimental.UtilityClass; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.meta.ItemMeta; -import yapion.hierarchy.output.StreamOutput; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.YAPIONParser; -import yapion.parser.options.StreamOptions; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; @UtilityClass public class LaufbauUtils { - public static class LaufbauComponent implements SWPlayer.Component { - - private YAPIONObject yapionObject = new YAPIONObject(); - - @Override - @SneakyThrows - public void onMount(SWPlayer player) { - String config = UserConfig.getConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau"); - if (config == null) { - return; - } - byte[] bytes = Base64.getDecoder().decode(config); - @Cleanup GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(bytes)); - yapionObject = new YAPIONParser(gzipInputStream, new StreamOptions().stopOnStreamEnd(true)).parse().result(); - } - - @Override - @SneakyThrows - public void onUnmount(SWPlayer player) { - if (yapionObject.isEmpty()) { - UserConfig.removePlayerConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau"); - return; - } - @Cleanup ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - @Cleanup GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); - yapionObject.toYAPION(new StreamOutput(gzipOutputStream)).close(); - UserConfig.updatePlayerConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau", Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray())); - } - } - public static Cuboid pixelCuboid(double pixelX, double pixelY, double pixelZ, double pixelDX, double pixelDY, double pixelDZ) { return new Cuboid(pixelToBlock(pixelX), pixelToBlock(pixelY), pixelToBlock(pixelZ), pixelToBlock(pixelDX), pixelToBlock(pixelDY), pixelToBlock(pixelDZ)); } @@ -123,7 +82,7 @@ public class LaufbauUtils { return false; } String identifier = identifier(bb); - return SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject.containsKey(identifier); + return LaufbauConfig.STORE.get(p).getDeactivated().contains(identifier); } @SneakyThrows @@ -132,11 +91,9 @@ public class LaufbauUtils { return; } String identifier = identifier(bb); - YAPIONObject yapionObject = SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject; - if (yapionObject.containsKey(identifier)) { - yapionObject.remove(identifier); - } else { - yapionObject.put(identifier, new YAPIONObject()); + Set deactivated = LaufbauConfig.STORE.get(p).getDeactivated(); + if (!deactivated.remove(identifier)) { + deactivated.add(identifier); } } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/converter/LaufbauConfigV1ToV2Converter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/converter/LaufbauConfigV1ToV2Converter.java new file mode 100644 index 000000000..6e1c7aacd --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/slaves/laufbau/converter/LaufbauConfigV1ToV2Converter.java @@ -0,0 +1,40 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.features.slaves.laufbau.converter; + +import de.steamwar.bausystem.config.ConfigConverter; +import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfig; +import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfigV1; + +@Deprecated +public class LaufbauConfigV1ToV2Converter implements ConfigConverter { + + @Override + public int version() { + return LaufbauConfigV1.VERSION; + } + + @Override + public LaufbauConfig convert(LaufbauConfigV1 old) { + LaufbauConfig config = new LaufbauConfig(); + config.getDeactivated().addAll(old.getDeactivated()); + return config; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceCommand.java index 4eac9b8f6..b92445737 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceCommand.java @@ -35,8 +35,8 @@ public class SmartPlaceCommand extends SWCommand { @Register(description = {"SMART_PLACE_HELP", "SMART_PLACE_INFO"}) public void genericToggle(Player p) { - boolean smartPlace = Config.getInstance().get(p).getPlainValueOrDefault("smartPlace", false); - Config.getInstance().get(p).put("smartPlace", !smartPlace); + boolean smartPlace = Config.STORE.get(p).isSmartPlace(); + Config.STORE.get(p).setSmartPlace(!smartPlace); if (!smartPlace) { SWUtils.sendToActionbar(p, BauSystem.MESSAGE.parse("SMART_PLACE_ENABLE", p)); BauSystem.MESSAGE.send("SMART_PLACE_INFO", p); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java index 6f70fc551..3554a7e56 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java @@ -25,6 +25,7 @@ import de.steamwar.bausystem.Permission; import de.steamwar.bausystem.configplayer.Config; import de.steamwar.linkage.Linked; import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; +import net.minecraft.world.InteractionHand; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -87,7 +88,9 @@ public class SmartPlaceListener implements Listener { public SmartPlaceListener() { TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> { if (!Permission.BUILD.hasPermission(player)) return packet; - if (!Config.getInstance().get(player).getPlainValueOrDefault("smartPlace", false)) return packet; + if (!Config.STORE.get(player).isSmartPlace()) return packet; + ItemStack usedItem = packet.getHand() == InteractionHand.MAIN_HAND ? player.getInventory().getItemInMainHand() : player.getInventory().getItemInOffHand(); + if (usedItem.getType() == Material.DEBUG_STICK) return packet; RayTraceResult rayTraceResult = player.rayTraceBlocks(6); Block block = rayTraceResult != null ? rayTraceResult.getHitBlock() : null; BlockFace blockFace = rayTraceResult != null ? rayTraceResult.getHitBlockFace() : null; @@ -127,7 +130,7 @@ public class SmartPlaceListener implements Listener { @EventHandler public void onPlayerInteract(PlayerInteractEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("smartPlace", false)) return; + if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return; WAS_EXECUTED.add(event.getPlayer()); if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) return; @@ -146,7 +149,7 @@ public class SmartPlaceListener implements Listener { @EventHandler public void onBlockPlace(BlockPlaceEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("smartPlace", false)) return; + if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return; if (!SMART_PLACING.contains(event.getPlayer())) { if (CONTAINERS.contains(event.getBlockAgainst().getType())) { SoundGroup soundGroup = event.getBlockPlaced().getBlockData().getSoundGroup(); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/testblock/blockcounter/BlockCounter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/testblock/blockcounter/BlockCounter.java index 9e3e53e0c..2ca166945 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/testblock/blockcounter/BlockCounter.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/testblock/blockcounter/BlockCounter.java @@ -35,18 +35,15 @@ public class BlockCounter { public final Map blockCountMap = new HashMap<>(); public boolean isActive(final Player p) { - if (!Config.getInstance().get(p).containsKey("blockCounter")) { - return false; - } - return Config.getInstance().get(p).getPlainValue("blockCounter"); + return Config.STORE.get(p).isBlockCounter(); } public void enable(final Player p) { - Config.getInstance().get(p).put("blockCounter", true); + Config.STORE.get(p).setBlockCounter(true); } public void disable(final Player p) { - Config.getInstance().get(p).put("blockCounter", false); + Config.STORE.get(p).setBlockCounter(false); } public String getMessage(Player player, int count, int tntCount, long tick, long lastTick) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tracer/rendering/TraceEntity.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tracer/rendering/TraceEntity.java index 160e4d58d..2ec576f6c 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tracer/rendering/TraceEntity.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tracer/rendering/TraceEntity.java @@ -36,13 +36,10 @@ import org.bukkit.entity.Player; import org.bukkit.util.Transformation; import org.joml.Quaternionf; import org.joml.Vector3f; -import yapion.hierarchy.types.YAPIONValue; import java.util.List; import java.util.stream.Collectors; -import static de.steamwar.bausystem.features.util.TNTClickListener.TNT_CLICK_DETAILS; - /** * Wrapper for the rendering of a record bundle */ @@ -102,7 +99,7 @@ public class TraceEntity extends RBlockDisplay { * @param player the player the message should be printed for */ public void printIntoChat(Player player) { - if (!Config.getInstance().get(player).getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) { + if (!Config.STORE.get(player).isTntClickDetails()) { return; } TNTPoint representative = records.get(0); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NightVisionCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NightVisionCommand.java index 2f8840471..d43502ad3 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NightVisionCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NightVisionCommand.java @@ -30,7 +30,6 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import yapion.hierarchy.types.YAPIONObject; @Linked public class NightVisionCommand extends SWCommand implements Listener { @@ -41,15 +40,15 @@ public class NightVisionCommand extends SWCommand implements Listener { @Register(description = "NIGHT_VISION_HELP") public void genericCommand(Player p) { - YAPIONObject yapionObject = Config.getInstance().get(p); - boolean value = !yapionObject.getBooleanOrDefault("nightvision", false); - yapionObject.put("nightvision", value); + Config config = Config.STORE.get(p); + boolean value = !config.isNightVision(); + config.setNightVision(value); setNightVision(p, value); } @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { - setNightVision(event.getPlayer(), Config.getInstance().get(event.getPlayer()).getBooleanOrDefault("nightvision", false)); + setNightVision(event.getPlayer(), Config.STORE.get(event.getPlayer()).isNightVision()); } private void setNightVision(Player p, boolean value) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java index dd143f0ae..91a79f5e9 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java @@ -53,7 +53,7 @@ public class NoClipCommand extends SWCommand implements Listener { @Override public void onUnmount(SWPlayer player) { - player.setGameMode(GameMode.CREATIVE); + player.getPlayer().setGameMode(GameMode.CREATIVE); } } @@ -115,6 +115,7 @@ public class NoClipCommand extends SWCommand implements Listener { Abilities abilities = (((CraftPlayer) player).getHandle()).getAbilities(); abilities.mayBuild = true; abilities.mayfly = true; + abilities.instabuild = true; swPlayer.setComponent(new NoClipData()); BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/TNTClickListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/TNTClickListener.java index ca17f6a5c..8af264a11 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/TNTClickListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/TNTClickListener.java @@ -32,26 +32,22 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.inventory.EquipmentSlot; -import yapion.hierarchy.types.YAPIONObject; -import yapion.hierarchy.types.YAPIONValue; @Linked public class TNTClickListener extends SWCommand implements Listener { - public static final String TNT_CLICK_DETAILS = "tnt_click_details"; - public TNTClickListener() { super("tntdetails"); } @Register(description = "TNT_DETAILS_COMMAND") public void toggle(Player player) { - YAPIONObject yapionObject = Config.getInstance().get(player); - if (yapionObject.getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) { - yapionObject.put(TNT_CLICK_DETAILS, false); + Config config = Config.STORE.get(player); + if (config.isTntClickDetails()) { + config.setTntClickDetails(false); BauSystem.MESSAGE.send("TNT_DETAILS_OFF", player); } else { - yapionObject.put(TNT_CLICK_DETAILS, true); + config.setTntClickDetails(true); BauSystem.MESSAGE.send("TNT_DETAILS_ON", player); } } @@ -60,7 +56,7 @@ public class TNTClickListener extends SWCommand implements Listener { public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; if (event.getHand() != EquipmentSlot.HAND) return; - if (!Config.getInstance().get(event.getPlayer()).getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) { + if (!Config.STORE.get(event.getPlayer()).isTntClickDetails()) { return; } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/Warp.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/Warp.java index 696dac812..f21109278 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/Warp.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/Warp.java @@ -25,7 +25,6 @@ import lombok.Getter; import org.bukkit.*; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent; -import yapion.hierarchy.types.YAPIONObject; import java.util.*; @@ -56,27 +55,22 @@ public class Warp { warpMap.put(name, this); } - public Warp(String name, YAPIONObject object) { + public Warp(String name, WorldData.WarpEntry warp) { this.name = name; - double x = object.getPlainValue("x"); - double y = object.getPlainValue("y"); - double z = object.getPlainValue("z"); - float yaw = object.getPlainValue("yaw"); - float pitch = object.getPlainValue("pitch"); - location = new Location(Bukkit.getWorlds().get(0), x, y, z, yaw, pitch); - mat = Material.getMaterial(object.getPlainValue("material")); + location = new Location(Bukkit.getWorlds().get(0), warp.getX(), warp.getY(), warp.getZ(), warp.getYaw(), warp.getPitch()); + mat = Material.getMaterial(warp.getMaterial()); warpMap.put(name, this); } - public YAPIONObject output() { - YAPIONObject yapionObject = new YAPIONObject(); - yapionObject.add("x", location.getX()); - yapionObject.add("y", location.getY()); - yapionObject.add("z", location.getZ()); - yapionObject.add("yaw", location.getYaw()); - yapionObject.add("pitch", location.getPitch()); - yapionObject.add("material", mat.toString()); - return yapionObject; + public WorldData.WarpEntry output() { + WorldData.WarpEntry warp = new WorldData.WarpEntry(); + warp.setX(location.getX()); + warp.setY(location.getY()); + warp.setZ(location.getZ()); + warp.setYaw(location.getYaw()); + warp.setPitch(location.getPitch()); + warp.setMaterial(mat.toString()); + return warp; } public static Set getWarpNames() { @@ -101,7 +95,7 @@ public class Warp { public void delete() { warpMap.remove(name); - WorldData.getWarpData().remove(name); + WorldData.STORE.get().getWarps().remove(name); } public void teleport(Player player) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/WarpCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/WarpCommand.java index e20a70b72..0c0712766 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/WarpCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/warp/WarpCommand.java @@ -34,9 +34,9 @@ import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.entity.Player; -import yapion.hierarchy.types.YAPIONObject; import java.util.ArrayList; +import java.util.Map; @Linked public class WarpCommand extends SWCommand implements Disable, Enable { @@ -135,17 +135,15 @@ public class WarpCommand extends SWCommand implements Disable, Enable { @Override public void enable() { - WorldData.getWarpData().forEach((name, yapionAnyType) -> { - new Warp(name, (YAPIONObject) yapionAnyType); - }); + WorldData.STORE.get().getWarps().forEach(Warp::new); Warp.enable(); } @Override public void disable() { - YAPIONObject yapionObject = WorldData.getWarpData(); + Map warps = WorldData.STORE.get().getWarps(); for (Warp warp : Warp.getWarps()) { - yapionObject.add(warp.getName(), warp.output()); + warps.put(warp.getName(), warp.output()); } } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java index e74922169..a56a255ec 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/BauScoreboard.java @@ -131,6 +131,7 @@ public class BauScoreboard implements Listener { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { + SWScoreboard.impl.removeScoreboard(event.getPlayer()); ADDITIONAL_SCOREBOARD_LINES.remove(event.getPlayer()); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/region/RegionData.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/region/RegionData.java index a1d94ed4e..715dc1d61 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/region/RegionData.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/region/RegionData.java @@ -20,78 +20,39 @@ package de.steamwar.bausystem.region; import de.steamwar.bausystem.region.flags.Flag; +import de.steamwar.bausystem.worlddata.WorldData; import de.steamwar.sql.SchematicNode; import lombok.NonNull; -import yapion.hierarchy.types.YAPIONObject; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.function.Function; public abstract class RegionData { - private final List> properties = new ArrayList<>(); - - protected final YAPIONObject data; - protected final YAPIONObject flagData; + protected final WorldData.RegionEntry data; + protected final Map flagData; protected final Runnable onChange; protected final Map, Flag.Value> flagMap = new HashMap<>(); - private final class Property { - private final String field; - private final Function loader; - private final Function writer; + private SchematicNode testblockSchematic; - private T value; - - public Property(String field, Function loader, Function writer) { - this.field = field; - this.loader = loader; - this.writer = writer; - properties.add(this); - } - - public void load() { - if (flagData.containsKey(field)) { - value = loader.apply(flagData.getPlainValue(field)); - } else { - value = null; - } - } - - public T get() { - return value; - } - - public void set(T value) { - this.value = value; - if (value == null) { - flagData.remove(field); - } else { - flagData.put(field, writer.apply(value)); - } - } - } - - private Property testblockSchematic = new Property<>("testblockSchematic", SchematicNode::byId, SchematicNode::getId); - - protected RegionData(YAPIONObject data, Runnable onChange) { + protected RegionData(WorldData.RegionEntry data, Runnable onChange) { this.data = data; - this.flagData = data.getObjectOrSetDefault("flagStorage", new YAPIONObject()); + this.flagData = data.getFlags(); this.onChange = onChange; initialize(); for (final Flag flag : Flag.getFlags()) { if (!has(flag).isWritable()) continue; try { - String s = flagData.getPlainValue(flag.name()); + String s = flagData.get(flag.name()); flagMap.put(flag, flag.valueOfValue(s)); } catch (Exception e) { flagMap.put(flag, (Flag.Value) flag.getDefaultValue()); } } - properties.forEach(Property::load); + if (data.getTestblockSchematic() != null) { + testblockSchematic = SchematicNode.byId(data.getTestblockSchematic()); + } } protected void initialize() { @@ -126,7 +87,8 @@ public abstract class RegionData { flagData.remove(flag.name()); } } - properties.forEach(property -> property.set(null)); + testblockSchematic = null; + data.setTestblockSchematic(null); onChange.run(); } @@ -135,11 +97,12 @@ public abstract class RegionData { } public SchematicNode getTestblockSchematic() { - return testblockSchematic.get(); + return testblockSchematic; } public void setTestblockSchematic(SchematicNode schematic) { - testblockSchematic.set(schematic); + testblockSchematic = schematic; + data.setTestblockSchematic(schematic == null ? null : schematic.getId()); onChange.run(); } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/SimulatorData.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/SimulatorData.java deleted file mode 100644 index 2c1a35731..000000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/SimulatorData.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.bausystem.worlddata; - -import de.steamwar.sql.SteamwarUser; -import lombok.SneakyThrows; -import lombok.experimental.UtilityClass; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import yapion.hierarchy.output.FileOutput; -import yapion.hierarchy.types.YAPIONArray; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.InputStreamCharsets; -import yapion.parser.YAPIONParser; -import yapion.parser.options.FileOptions; - -import java.io.File; -import java.util.List; - -@UtilityClass -public class SimulatorData { - - private final File simulatorsDir = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "simulators"); - - static { - simulatorsDir.mkdirs(); - } - - @SneakyThrows - public YAPIONObject getSimulator(Player player) { - File file = getFile(player); - if (!file.exists()) { - return new YAPIONObject(); - } - return YAPIONParser.parse(file, new FileOptions().charset(InputStreamCharsets.UTF_8)); - } - - @SneakyThrows - private void internalSaveSimulator(Player player, YAPIONObject yapionObject) { - File file = getFile(player); - yapionObject.toYAPION(new FileOutput(file)).close(); - } - - @SneakyThrows - void saveSimulator(SteamwarUser steamwarUser, YAPIONObject yapionObject) { - File file = new File(simulatorsDir, steamwarUser.getId() + ".yapion"); - yapionObject.toYAPION(new FileOutput(file)).close(); - } - - public List listSimulator(Player player) { - List strings = getSimulator(player).getKeys(); - strings.remove(""); - return strings; - } - - public void saveSimulator(Player player, String name) { - YAPIONObject yapionObject = getSimulator(player); - yapionObject.put(name, yapionObject.getArray("").copy()); - internalSaveSimulator(player, yapionObject); - } - - public void saveSimulator(Player player, YAPIONArray simulator) { - YAPIONObject yapionObject = getSimulator(player); - yapionObject.put("", simulator); - internalSaveSimulator(player, yapionObject); - } - - public void loadSimulator(Player player, String name) { - YAPIONObject yapionObject = getSimulator(player); - yapionObject.put("", yapionObject.getArray(name).copy()); - internalSaveSimulator(player, yapionObject); - } - - public void removeSimulator(Player player, String name) { - YAPIONObject yapionObject = getSimulator(player); - yapionObject.remove(name); - internalSaveSimulator(player, yapionObject); - } - - private File getFile(Player player) { - SteamwarUser steamwarUser = SteamwarUser.get(player.getUniqueId()); - return new File(simulatorsDir, steamwarUser.getId() + ".yapion"); - } -} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldData.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldData.java index bd6611728..d47f097a6 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldData.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldData.java @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2025 SteamWar.de-Serverteam + * Copyright (C) 2026 SteamWar.de-Serverteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,58 +19,83 @@ package de.steamwar.bausystem.worlddata; -import lombok.experimental.UtilityClass; +import com.google.gson.GsonBuilder; +import de.steamwar.bausystem.config.ConfigStore; +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.worlddata.converter.WorldDataV1ToV2Converter; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; import org.bukkit.Bukkit; -import yapion.hierarchy.output.FileOutput; -import yapion.hierarchy.types.YAPIONObject; -import yapion.parser.InputStreamCharsets; -import yapion.parser.YAPIONParser; -import yapion.parser.options.StreamOptions; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; +import java.util.HashMap; +import java.util.Map; -@UtilityClass -public class WorldData { +/** + * The current world data. It is stored as JSON, fields missing in the + * stored JSON keep the default value of their initializer, so adding a new + * field does not need a new version. + *

+ * When a field gets removed, renamed or its meaning changes, copy this class + * to WorldDataV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a + * {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to this class and register it in the {@link #STORE}. + */ +@Getter +@Setter +public class WorldData implements VersionedConfig { - private final File optionsFile = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.yapion"); - private YAPIONObject worldData; + public static final int VERSION = 2; - public YAPIONObject getWorldData() { - if (worldData == null) { - read(); - } - return worldData; + public static final ConfigStore STORE = new ConfigStore<>(WorldData.class, VERSION, WorldData::new) + .gson(new GsonBuilder().setPrettyPrinting().create()) + .converter(new WorldDataV1ToV2Converter()) + .file(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.json")) + .legacyFile(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.yapion"), WorldDataV1::parse); + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + /** + * Region name to the data of the region + */ + protected Map regions = new HashMap<>(); + protected RegionEntry globalRegion = new RegionEntry(); + + /** + * Warp name to warp + */ + protected Map warps = new HashMap<>(); + + @Getter + @Setter + public static class RegionEntry { + + /** + * Overrides the prototype of the region config, null if not overridden + */ + protected String prototype = null; + + /** + * Flag name to flag value name + */ + protected Map flags = new HashMap<>(); + + /** + * Id of the testblock SchematicNode, null if the default testblock is used + */ + protected Integer testblockSchematic = null; } - public YAPIONObject getRegionsData() { - return getWorldData().getYAPIONObjectOrSetDefault("regions", new YAPIONObject()); - } + @Getter + @Setter + public static class WarpEntry { - public YAPIONObject getWarpData() { - return getWorldData().getYAPIONObjectOrSetDefault("warps", new YAPIONObject()); - } - - private void read() { - worldData = new YAPIONObject(); - if (optionsFile.length() != 0) { - try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(optionsFile))) { - worldData = YAPIONParser.parse(bufferedInputStream, new StreamOptions().charset(InputStreamCharsets.UTF_8)); - } catch (IOException e) { - // Ignored - } - } else { - write(); - } - } - - public void write() { - try { - worldData.toYAPION(new FileOutput(optionsFile)).close(); - } catch (IOException e) { - e.printStackTrace(); - } + protected double x; + protected double y; + protected double z; + protected float yaw; + protected float pitch; + protected String material; } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldDataV1.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldDataV1.java new file mode 100644 index 000000000..b75492461 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/WorldDataV1.java @@ -0,0 +1,111 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.worlddata; + +import de.steamwar.bausystem.config.VersionedConfig; +import de.steamwar.bausystem.config.legacy.YapionReader; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; + +/** + * The legacy world data that was stored in the YAPION format in options.yapion. + */ +@Getter +@Setter +@Deprecated +public class WorldDataV1 implements VersionedConfig { + + public static final int VERSION = 1; + + @Setter(AccessLevel.NONE) + protected int version = VERSION; + + /** + * Region name to the data of the region, the global region is stored with the name "global" + */ + protected Map regions = new HashMap<>(); + + /** + * Warp name to warp + */ + protected Map warps = new HashMap<>(); + + @Getter + @Setter + public static class RegionEntry { + + protected String prototype = null; + + /** + * Flag name to flag value name and "testblockSchematic" to the SchematicNode id + */ + protected Map flagStorage = new HashMap<>(); + } + + @Getter + @Setter + public static class WarpEntry { + + protected double x; + protected double y; + protected double z; + protected float yaw; + protected float pitch; + protected String material; + } + + public static WorldDataV1 parse(String yapion) { + Map object = YapionReader.parse(yapion); + WorldDataV1 worldData = new WorldDataV1(); + + if (object.get("regions") instanceof Map regions) { + regions.forEach((name, value) -> { + if (!(value instanceof Map region)) return; + RegionEntry entry = new RegionEntry(); + if (region.get("prototype") instanceof String prototype) { + entry.prototype = prototype; + } + if (region.get("flagStorage") instanceof Map flagStorage) { + flagStorage.forEach((key, flag) -> entry.flagStorage.put((String) key, flag)); + } + worldData.regions.put((String) name, entry); + }); + } + + if (object.get("warps") instanceof Map warps) { + warps.forEach((name, value) -> { + if (!(value instanceof Map warp)) return; + WarpEntry entry = new WarpEntry(); + entry.x = ((Number) warp.get("x")).doubleValue(); + entry.y = ((Number) warp.get("y")).doubleValue(); + entry.z = ((Number) warp.get("z")).doubleValue(); + entry.yaw = ((Number) warp.get("yaw")).floatValue(); + entry.pitch = ((Number) warp.get("pitch")).floatValue(); + entry.material = (String) warp.get("material"); + worldData.warps.put((String) name, entry); + }); + } + return worldData; + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/converter/WorldDataV1ToV2Converter.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/converter/WorldDataV1ToV2Converter.java new file mode 100644 index 000000000..31bd3f859 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/worlddata/converter/WorldDataV1ToV2Converter.java @@ -0,0 +1,75 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.worlddata.converter; + +import de.steamwar.bausystem.config.ConfigConverter; +import de.steamwar.bausystem.worlddata.WorldData; +import de.steamwar.bausystem.worlddata.WorldDataV1; + +@Deprecated +public class WorldDataV1ToV2Converter implements ConfigConverter { + + private static final String GLOBAL_REGION = "global"; + private static final String TESTBLOCK_SCHEMATIC = "testblockSchematic"; + + @Override + public int version() { + return WorldDataV1.VERSION; + } + + @Override + public WorldData convert(WorldDataV1 old) { + WorldData worldData = new WorldData(); + old.getRegions().forEach((name, region) -> { + if (name.equals(GLOBAL_REGION)) { + worldData.setGlobalRegion(convert(region)); + } else { + worldData.getRegions().put(name, convert(region)); + } + }); + old.getWarps().forEach((name, warp) -> worldData.getWarps().put(name, convert(warp))); + return worldData; + } + + private WorldData.RegionEntry convert(WorldDataV1.RegionEntry old) { + WorldData.RegionEntry region = new WorldData.RegionEntry(); + region.setPrototype(old.getPrototype()); + old.getFlagStorage().forEach((key, value) -> { + if (value == null) return; + if (key.equals(TESTBLOCK_SCHEMATIC)) { + region.setTestblockSchematic(((Number) value).intValue()); + } else { + region.getFlags().put(key, value.toString()); + } + }); + return region; + } + + private WorldData.WarpEntry convert(WorldDataV1.WarpEntry old) { + WorldData.WarpEntry warp = new WorldData.WarpEntry(); + warp.setX(old.getX()); + warp.setY(old.getY()); + warp.setZ(old.getZ()); + warp.setYaw(old.getYaw()); + warp.setPitch(old.getPitch()); + warp.setMaterial(old.getMaterial()); + return warp; + } +} diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedGlobalRegionData.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedGlobalRegionData.java index 065e2e203..5412adf79 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedGlobalRegionData.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedGlobalRegionData.java @@ -25,12 +25,12 @@ import de.steamwar.bausystem.region.flags.ColorMode; import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.ProtectMode; import de.steamwar.bausystem.region.flags.TNTMode; +import de.steamwar.bausystem.worlddata.WorldData; import lombok.NonNull; -import yapion.hierarchy.types.YAPIONObject; public class FixedGlobalRegionData extends RegionData { - public FixedGlobalRegionData(YAPIONObject data, Runnable onChange) { + public FixedGlobalRegionData(WorldData.RegionEntry data, Runnable onChange) { super(data, onChange); } diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java index 62b83882f..913e1a1f0 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegion.java @@ -26,6 +26,7 @@ import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.TestblockMode; import de.steamwar.bausystem.utils.FlatteningWrapper; import de.steamwar.bausystem.utils.PasteBuilder; +import de.steamwar.bausystem.worlddata.WorldData; import de.steamwar.core.Core; import de.steamwar.sql.GameModeConfig; import de.steamwar.sql.SchematicType; @@ -139,7 +140,7 @@ public class FixedRegion implements Region { } } - public FixedRegion(String name, FixedRegionData flagStorage, Prototype prototype, YAPIONObject regionConfig, YAPIONObject regionData) { + public FixedRegion(String name, FixedRegionData flagStorage, Prototype prototype, YAPIONObject regionConfig, WorldData.RegionEntry regionData) { this.name = name; uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)); this.flagStorage = flagStorage; @@ -325,7 +326,7 @@ public class FixedRegion implements Region { } floorLevel = prototype.getFloorOffset() != 0 ? minPoint.getY() + prototype.getFloorOffset() : 0; - waterLevel = prototype.getWaterOffset() != 0 ? minPoint.getX() + prototype.getWaterOffset() : 0; + waterLevel = prototype.getWaterOffset() != 0 ? minPoint.getY() + prototype.getWaterOffset() : 0; File found = null; File baseFile = new File(BauSystem.getInstance().getDataFolder().getParentFile(), "FightSystem"); diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegionData.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegionData.java index c2de70421..ffe7c327c 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegionData.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/FixedRegionData.java @@ -22,12 +22,12 @@ package de.steamwar.bausystem.region.fixed; import de.steamwar.bausystem.region.RegionData; import de.steamwar.bausystem.region.RegionFlagPolicy; import de.steamwar.bausystem.region.flags.Flag; +import de.steamwar.bausystem.worlddata.WorldData; import lombok.NonNull; -import yapion.hierarchy.types.YAPIONObject; public class FixedRegionData extends RegionData { - public FixedRegionData(YAPIONObject data, Runnable onChange) { + public FixedRegionData(WorldData.RegionEntry data, Runnable onChange) { super(data, onChange); } diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/Prototype.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/Prototype.java index 04cf7ee08..0fe5f2b57 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/Prototype.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/Prototype.java @@ -210,14 +210,14 @@ public class Prototype { } } - public static void generateRegion(String name, YAPIONObject regionConfig, YAPIONObject regionData) { + public static void generateRegion(String name, YAPIONObject regionConfig, WorldData.RegionEntry regionData) { Prototype prototype; - if (regionData.containsKey("prototype", String.class)) { - prototype = PROTOTYPE_MAP.get(regionData.getPlainValue("prototype")); + if (regionData.getPrototype() != null) { + prototype = PROTOTYPE_MAP.get(regionData.getPrototype()); } else { prototype = PROTOTYPE_MAP.get(regionConfig.getPlainValue("prototype")); } - FixedRegionData flagStorage = new FixedRegionData(regionData, WorldData::write); + FixedRegionData flagStorage = new FixedRegionData(regionData, WorldData.STORE::save); FixedRegionSystem.addRegion(new FixedRegion(name, flagStorage, prototype, regionConfig, regionData)); } } diff --git a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/loader/RegionLoader.java b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/loader/RegionLoader.java index 3bfe538da..d19699e48 100644 --- a/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/loader/RegionLoader.java +++ b/BauSystem/BauSystem_RegionFixed/src/de/steamwar/bausystem/region/fixed/loader/RegionLoader.java @@ -28,7 +28,6 @@ import org.bukkit.Bukkit; import yapion.hierarchy.diff.DiffChange; import yapion.hierarchy.diff.YAPIONDiff; import yapion.hierarchy.types.YAPIONObject; -import yapion.hierarchy.types.YAPIONType; import yapion.parser.YAPIONParser; import java.io.BufferedInputStream; @@ -55,7 +54,7 @@ public class RegionLoader { } loaded = yapionObject; - YAPIONObject optionsYapionObject = WorldData.getRegionsData(); + WorldData worldData = WorldData.STORE.get(); yapionObject.forEach((key, yapionAnyType) -> { if (key.equals("global")) { return; @@ -65,21 +64,10 @@ public class RegionLoader { } YAPIONObject regionConfig = (YAPIONObject) yapionAnyType; - YAPIONObject regionData = new YAPIONObject(); - if (optionsYapionObject.containsKey(key, YAPIONType.OBJECT)) { - regionData = optionsYapionObject.getObject(key); - } else { - optionsYapionObject.add(key, regionData); - } - + WorldData.RegionEntry regionData = worldData.getRegions().computeIfAbsent(key, k -> new WorldData.RegionEntry()); Prototype.generateRegion(key, regionConfig, regionData); }); - YAPIONObject globalOptions = optionsYapionObject.getObject("global"); - if (globalOptions == null) { - globalOptions = new YAPIONObject(); - optionsYapionObject.add("global", globalOptions); - } - FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(globalOptions, WorldData::write)); + FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(worldData.getGlobalRegion(), WorldData.STORE::save)); } } diff --git a/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java b/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java new file mode 100644 index 000000000..642cf3358 --- /dev/null +++ b/CommonCore/Network/src/de/steamwar/network/packets/server/ChatRecordPacket.java @@ -0,0 +1,40 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.network.packets.server; + +import de.steamwar.network.packets.NetworkPacket; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * A chat message written on an arena. The arena only records it for the replay and never displays it. + */ +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Getter +public class ChatRecordPacket extends NetworkPacket { + private static final long serialVersionUID = -4718360932583127458L; + private int playerId; + private String message; +} diff --git a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java index 82d07ef56..bf6f4ab75 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java +++ b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java @@ -19,6 +19,7 @@ package de.steamwar.sql; +import de.steamwar.sql.internal.SQLConfig; import lombok.ToString; import java.io.File; @@ -250,6 +251,11 @@ public final class GameModeConfig { Times = new TimesConfig(loader.with("Times")); // Arena would be here to be in config order but needs Schematic.Size and EnterStages loaded afterwards Schematic = new SchematicConfig<>(loader.with("Schematic")); + for (int i = 0; i < Schematic.Limited.size(); i++) { + if (Schematic.Limited.get(i).Materials.isEmpty()) { + SQLConfig.impl.getLogger().warning(file.getName() + ": Schematic.Limited[" + i + "] matches no blocks, check its 'Materials' entry"); + } + } GameName = loader.getString("GameName", defaultGameName.apply(file)); ActiveMonths = loader.getIntList("ActiveMonths"); TeamChatPrefix = loader.getString("TeamChatPrefix", "+"); @@ -719,7 +725,10 @@ public final class GameModeConfig { public final double MaxDesignBlastResistance; /** - * List of limited block materials + * List of limited block materials
+ * Every inventory block is counted towards exactly one rule: a filled block towards a rule whose + * {@link BlockRule#Content} allows all of its items, an empty block towards a rule without + * {@link BlockRule#Content}. Empty inventory blocks without such a rule are unlimited. * * @implSpec {@link SQLWrapper#getInventoryMaterials)} are all disallowed by default to have any content but are unlimited in number. * @implSpec Any Block that has more Blast Resistance than {@link #MaxBlastResistance} will be defaulted to 0. @@ -775,18 +784,13 @@ public final class GameModeConfig { } public boolean isInventory(M material) { - return Limited.stream() - .filter(rule -> rule.Materials.contains(material)) - .anyMatch(rule -> rule.Content.loaded); + return SQLWrapper.impl.getInventoryMaterials().contains(material) || Limited.stream().anyMatch(rule -> rule.Content.loaded && rule.Materials.contains(material)); } - public Integer getItemAmount(M material, M item) { + public List> getRules(M material) { return Limited.stream() .filter(rule -> rule.Materials.contains(material)) - .filter(rule -> rule.Content.Items.contains(item)) - .mapToInt(rule -> rule.Content.Amount) - .max() - .orElse(0); + .collect(Collectors.toList()); } @ToString diff --git a/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java b/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java index 565687fe7..47a6f0a67 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java +++ b/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java @@ -46,7 +46,7 @@ public interface SQLWrapper { } /** - * Every inventory material - the {@code all} group usable in {@code GameModeConfig}'s + * Every inventory material - the {@code inventory} group usable in {@code GameModeConfig}'s * {@code Limited}. Expected to be a superset of {@link #getStorageMaterials()}. */ default Set getInventoryMaterials() { diff --git a/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt b/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt index 0d19c712c..09b7bf689 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/SteamwarUser.kt @@ -307,12 +307,12 @@ class SteamwarUser(id: EntityID) : IntEntity(id) { punishments[punishment] = Punishment.createPunishment(this@SteamwarUser.id.value, from, punishment, reason, time, perma) } - fun setLocale(locale: Locale?, manualeLocale: Boolean) { + fun setLocale(locale: Locale?, manualLocale: Boolean) { if (locale == null || (this.manualLocale && !manualLocale)) return useDb { this@SteamwarUser.locale = locale - this@SteamwarUser.manualLocale = manualeLocale + this@SteamwarUser.manualLocale = manualLocale } } diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties index c546644f7..1354f2e15 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.properties @@ -109,7 +109,6 @@ TEAM_READY = §aTeam ready TEAM_NOT_READY = §c§mTeam ready SKIP_READY = §aSkipping to next event SKIP_NOT_READY = §c§mSkipping to next event -TEAM_CHAT = {0}{1}§8» {0}{2} CHOOSE_KIT = §eChoose kit RESPAWN = §eRespawn MANAGE_PLAYERS = §cManage players diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java index 0119bf402..b7820066d 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightTeam.java @@ -256,10 +256,6 @@ public class FightTeam { broadcast(player -> FightSystem.getMessage().send(message, player, params)); } - public void broadcastChat(Player sender, String message) { - broadcast(player -> FightSystem.getMessage().sendPrefixless("TEAM_CHAT", player, ChatMessageType.CHAT, prefix, sender.getName(), message)); - } - private void broadcast(Consumer f) { players.forEach((uuid, fightPlayer) -> { Player player = Bukkit.getPlayer(uuid); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java index 617684d89..d7d19338b 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/listener/Chat.java @@ -19,50 +19,41 @@ package de.steamwar.fightsystem.listener; -import de.steamwar.fightsystem.ArenaMode; import de.steamwar.fightsystem.Config; import de.steamwar.fightsystem.FightSystem; import de.steamwar.fightsystem.fight.Fight; import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.record.GlobalRecorder; -import de.steamwar.fightsystem.states.FightState; -import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.linkage.Linked; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.server.ChatRecordPacket; +import de.steamwar.sql.SteamwarUser; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; import java.util.logging.Level; +/** + * Chat is handled by Velocity. Messages written on this server are only sent here to be recorded for the replay. + */ @Linked -public class Chat implements Listener { +public class Chat extends PacketHandler { - public Chat() { - new StateDependentListener(ArenaMode.All, FightState.All, this); - } + @Handler + public void handleChatRecord(ChatRecordPacket packet) { + SteamwarUser user = SteamwarUser.byId(packet.getPlayerId()); + String message = packet.getMessage(); + FightSystem.getPlugin().getLogger().log(Level.INFO, user.getUserName() + "» " + message); - @EventHandler - public void handlePlayerChat(AsyncPlayerChatEvent event) { - Player player = event.getPlayer(); - String message = event.getMessage(); - - FightSystem.getPlugin().getLogger().log(Level.INFO, player.getName() + "» " + message); - FightTeam fightTeam = Fight.getPlayerTeam(player); + Player player = Bukkit.getPlayer(user.getUUID()); + FightTeam fightTeam = player != null ? Fight.getPlayerTeam(player) : null; if (fightTeam != null) { - String teamName = fightTeam.getColoredName(); - if (message.startsWith(Config.GameModeConfig.TeamChatPrefix)) { - fightTeam.broadcastChat(player, message.substring(1)); - } else { - broadcastChat("PARTICIPANT_CHAT", teamName, player.getName(), message); - } - } else if (Config.isReferee(player)) { - broadcastChat("FIGHTLEADER_CHAT", player.getName(), message); + GlobalRecorder.getInstance().chat("PARTICIPANT_CHAT", fightTeam.getColoredName(), user.getUserName(), message); + } else if (player != null && Config.isReferee(player)) { + GlobalRecorder.getInstance().chat("FIGHTLEADER_CHAT", user.getUserName(), message); } else { - broadcastChat("SPECTATOR_CHAT", player.getName(), message); + GlobalRecorder.getInstance().chat("SPECTATOR_CHAT", user.getUserName(), message); } - - event.setCancelled(true); } public static void broadcastChat(String message, Object... params) { diff --git a/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java b/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java deleted file mode 100644 index cf7a991c5..000000000 --- a/MissileWars/src/de/steamwar/misslewars/listener/ChatListener.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.misslewars.listener; - -import de.steamwar.linkage.Linked; -import de.steamwar.misslewars.FightState; -import de.steamwar.misslewars.MWTeam; -import de.steamwar.misslewars.MissileWars; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.AsyncPlayerChatEvent; - -import java.util.EnumSet; - -@Linked -public class ChatListener extends BasicListener { - - public ChatListener() { - super(EnumSet.allOf(FightState.class)); - } - - @EventHandler - public void onChat(AsyncPlayerChatEvent e) { - e.setCancelled(true); - - Player p = e.getPlayer(); - MWTeam team = MissileWars.getTeam(p); - String message = e.getPlayer().getDisplayName() + "§8» §7" + e.getMessage(); - if (team == null) { - Bukkit.broadcastMessage(message); - } else { - if (e.getMessage().startsWith("+")) { - String teamMessage = "§8[" + team.getColorCode() + "TEAM§8] " + e.getPlayer().getDisplayName() + "§8» §7" + e.getMessage().substring(1); - team.getPlayers().forEach(player -> player.sendMessage(teamMessage)); - } else { - Bukkit.broadcastMessage(message); - } - } - } -} diff --git a/SchematicSystem/src/SchematicSystem.properties b/SchematicSystem/src/SchematicSystem.properties index c72bcf961..adb68d095 100644 --- a/SchematicSystem/src/SchematicSystem.properties +++ b/SchematicSystem/src/SchematicSystem.properties @@ -262,9 +262,8 @@ AUTO_CHECKER_RESULT_BLOCKS = §7Blocks: §c{0}§7, Max: §e{1} AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unknown block: §c{0} AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2} AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Forbidden block: §c{0} -AUTO_CHECKER_RESULT_WIND_CHARGES = §7Windcharges: §c{0}§7, Max: §e2048 -AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER = §7Dispenser: §c[{0}, {1}, {2}]§7, Windcharges: §c{3}§7 AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Forbidden Item: [{0}, {1}, {2}] -> §c{3} +AUTO_CHECKER_RESULT_MIXED_ITEMS = §7Items of different rules in one inventory: [{0}, {1}, {2}] -> §c{3} AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Defunct NBT: §7[{0}, {1}, {2}] AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} in Design: [{1}, {2}, {3}] AUTO_CHECKER_RESULT_ENTITY = §7Entity: §7[{0}, {1}, {2}] diff --git a/SchematicSystem/src/SchematicSystem_de.properties b/SchematicSystem/src/SchematicSystem_de.properties index d584c602c..6d54d9bc1 100644 --- a/SchematicSystem/src/SchematicSystem_de.properties +++ b/SchematicSystem/src/SchematicSystem_de.properties @@ -242,9 +242,8 @@ AUTO_CHECKER_RESULT_BLOCKS = §7Blöcke: §c{0}§7, Max: §e{1} AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unbekannter Block: §c{0} AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2} AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Verbotener Block: §c{0} -AUTO_CHECKER_RESULT_WIND_CHARGES = §7Windcharges: §c{0}§7, Max: §e2048 -AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER = §7Werfer: §c[{0}, {1}, {2}]§7, Windcharges: §c{3}§7 AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Verbotener gegenstand: [{0}, {1}, {2}] -> §c{3} +AUTO_CHECKER_RESULT_MIXED_ITEMS = §7Gegenstände verschiedener Regeln in einem Inventar: [{0}, {1}, {2}] -> §c{3} AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Keine NBT-Daten: §c[{0}, {1}, {2}] AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} im Design: [{1}, {2}, {3}] AUTO_CHECKER_RESULT_RECORD = §7Schallplatte: §c[{0}, {1}, {2}] diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java index cb23bbbdc..1d0b6bc7b 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java @@ -25,6 +25,7 @@ import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.world.block.BaseBlock; import de.steamwar.sql.GameModeConfig; +import de.steamwar.sql.GameModeConfig.SchematicConfig.BlockRule; import lombok.Getter; import lombok.ToString; import org.bukkit.Material; @@ -62,6 +63,7 @@ public class AutoChecker { public AutoChecker.BlockScanResult scan(Clipboard clipboard, GameModeConfig type) { AutoChecker.BlockScanResult result = new AutoChecker.BlockScanResult(); + Map>> ruleCandidates = new HashMap<>(); BlockVector3 min = clipboard.getMinimumPoint(); BlockVector3 max = clipboard.getMaximumPoint(); for (int x = min.x(); x <= max.x(); x++) { @@ -75,13 +77,13 @@ public class AutoChecker { BlockPos pos = new BlockPos(x, y, z); + result.getBlockCounts().merge(material, 1, Integer::sum); if (type.Schematic.isInventory(material)) { - checkInventory(result, block, material, pos, type); - if (!result.getInventoryItemCounts().getOrDefault(pos, Collections.emptyMap()).isEmpty()) { - result.getBlockCounts().merge(material, 1, Integer::sum); + result.getInventoryBlockType().put(pos, material); + List> candidates = checkInventory(result, block, material, pos, type); + if (!candidates.isEmpty()) { + ruleCandidates.put(pos, candidates); } - } else { - result.getBlockCounts().merge(material, 1, Integer::sum); } if (x == min.x() || x == max.x() || y == max.y() || z == min.z() || z == max.z()) { @@ -90,28 +92,28 @@ public class AutoChecker { } } } + assignRules(result, ruleCandidates); return result; } - private void checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig type) { + /** + * Scans the inventory and returns the rules this block may be counted towards. + * An empty list means the block is either faulty (reported in the result) or unlimited. + */ + private List> checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig type) { CompoundTag nbt = block.getNbtData(); if (nbt == null) { result.getDefunctNbt().add(pos); - return; + return Collections.emptyList(); } - if (material == Material.JUKEBOX && nbt.getValue().containsKey("RecordItem")) { result.getRecords().add(pos); - return; + return Collections.emptyList(); } - List items = nbt.getList("Items", CompoundTag.class); - if (items.isEmpty()) return; // Leeres Inventar - Map itemCounts = new EnumMap<>(Material.class); - int windChargeCount = 0; - for (CompoundTag item : items) { + for (CompoundTag item : nbt.getList("Items", CompoundTag.class)) { if (!item.containsKey("id")) { result.getDefunctNbt().add(pos); continue; @@ -121,22 +123,69 @@ public class AutoChecker { if (itemType == null) // Leere Slots continue; - if (type.Schematic.Type.getName().equals("wargearseason26") && material == Material.DISPENSER && itemType == Material.WIND_CHARGE) { - windChargeCount += item.getInt("count"); - } else if (type.Schematic.getItemAmount(material, itemType) == 0) { - result.getForbiddenItems().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType); - } else { - itemCounts.merge(itemType, item.getInt("count"), Integer::sum); - } + itemCounts.merge(itemType, item.getInt("count"), Integer::sum); if (item.containsKey("tag")) { result.getForbiddenNbt().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType); } } - if (!itemCounts.isEmpty()) { - result.getInventoryItemCounts().put(pos, itemCounts); - result.getInventoryBlockType().put(pos, material); + + List> rules = type.Schematic.getRules(material); + if (itemCounts.isEmpty()) { // Leeres Inventar + return rules.stream().filter(rule -> !rule.Content.loaded).collect(Collectors.toList()); } - result.getWindChargeCount().put(pos, windChargeCount); + result.getInventoryItemCounts().put(pos, itemCounts); + + List> contentRules = rules.stream().filter(rule -> rule.Content.loaded).collect(Collectors.toList()); + List> itemsFitting = contentRules.stream() + .filter(rule -> rule.Content.Items.containsAll(itemCounts.keySet())) + .collect(Collectors.toList()); + List> fitting = itemsFitting.stream() + .filter(rule -> itemCounts.values().stream().allMatch(count -> count <= rule.Content.Amount)) + .collect(Collectors.toList()); + if (!fitting.isEmpty()) { + return fitting; + } + + if (!itemsFitting.isEmpty()) { + BlockRule best = Collections.max(itemsFitting, Comparator.comparingInt(rule -> rule.Content.Amount)); + itemCounts.forEach((itemType, count) -> { + if (count > best.Content.Amount) { + result.getTooManyItems().computeIfAbsent(pos, blockVector3 -> new EnumMap<>(Material.class)).put(itemType, best.Content.Amount); + } + }); + return Collections.singletonList(best); + } + + Set allowed = contentRules.stream().flatMap(rule -> rule.Content.Items.stream()).collect(Collectors.toSet()); + Set forbidden = itemCounts.keySet().stream().filter(itemType -> !allowed.contains(itemType)).collect(Collectors.toSet()); + if (!forbidden.isEmpty()) { + result.getForbiddenItems().put(pos, forbidden); + } + + Set rest = itemCounts.keySet().stream().filter(allowed::contains).collect(Collectors.toSet()); + if (!rest.isEmpty()) { + // Every remaining item is allowed on its own, but no single rule allows all of them together + BlockRule best = Collections.max(contentRules, Comparator.comparingLong(rule -> rest.stream().filter(rule.Content.Items::contains).count())); + Set mixed = rest.stream().filter(itemType -> !best.Content.Items.contains(itemType)).collect(Collectors.toSet()); + if (!mixed.isEmpty()) { + result.getMixedItems().put(pos, mixed); + } + } + return Collections.emptyList(); + } + + /** + * Counts every block towards one of its candidate rules, preferring the rule with the most capacity left. + * Blocks with fewer candidates are assigned first. + */ + private void assignRules(AutoChecker.BlockScanResult result, Map>> ruleCandidates) { + ruleCandidates.entrySet().stream() + .sorted(Comparator.comparingInt(entry -> entry.getValue().size())) + .forEach(entry -> { + BlockRule rule = Collections.max(entry.getValue(), Comparator.comparingLong(candidate -> (long) candidate.Amount - result.getRuleCounts().getOrDefault(candidate, 0))); + result.getRuleCounts().merge(rule, 1, Integer::sum); + result.getRuleBlockTypes().computeIfAbsent(rule, r -> EnumSet.noneOf(Material.class)).add(result.getInventoryBlockType().get(entry.getKey())); + }); } @Getter @@ -148,8 +197,11 @@ public class AutoChecker { private final Map> designBlocks = new EnumMap<>(Material.class); private final Map> inventoryItemCounts = new HashMap<>(); private final Map inventoryBlockType = new HashMap<>(); - private final Map windChargeCount = new HashMap<>(); + private final Map, Integer> ruleCounts = new HashMap<>(); + private final Map, Set> ruleBlockTypes = new HashMap<>(); + private final Map> tooManyItems = new HashMap<>(); private final Map> forbiddenItems = new HashMap<>(); + private final Map> mixedItems = new HashMap<>(); private final Map> forbiddenNbt = new HashMap<>(); } } diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java index c59f17aba..ca6842449 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java @@ -31,6 +31,7 @@ import org.bukkit.entity.Player; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Builder @Getter @@ -46,12 +47,12 @@ public class AutoCheckerResult { public boolean isOk() { return blockScanResult.getRecords().isEmpty() && blockScanResult.getForbiddenItems().isEmpty() && + blockScanResult.getMixedItems().isEmpty() && blockScanResult.getForbiddenNbt().isEmpty() && isSizeOk() && isBlockCountOk() && isLimitedBlocksOK() && isInventoryItemsOK() && - isWindchargeCountOK() && !type.isAfterDeadline() && entities.isEmpty() && isDesignBlastResistanceOK(); @@ -62,23 +63,8 @@ public class AutoCheckerResult { !type.isAfterDeadline(); } - public boolean isWindchargeCountOK() { - if (type.Schematic.Type.getName().equals("wargearseason26")) { - int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0); - return windChargesCount <= 2048; - } else { - return true; - } - } - public boolean isInventoryItemsOK() { - return blockScanResult.getInventoryItemCounts().entrySet().stream().allMatch(posEntry -> { - Material inventory = blockScanResult.getInventoryBlockType().get(posEntry.getKey()); - return posEntry.getValue().entrySet().stream().allMatch(itemEntry -> { - int cap = type.Schematic.getItemAmount(inventory, itemEntry.getKey()); - return itemEntry.getValue() <= cap; - }); - }); + return blockScanResult.getTooManyItems().isEmpty(); } public boolean hasWarnings() { @@ -107,7 +93,10 @@ public class AutoCheckerResult { public boolean isLimitedBlocksOK() { return blockScanResult.getBlockCounts().entrySet().stream() - .allMatch(entry -> entry.getValue() <= type.Schematic.getMaxCount(entry.getKey())); + .filter(entry -> !type.Schematic.isInventory(entry.getKey())) + .allMatch(entry -> entry.getValue() <= type.Schematic.getMaxCount(entry.getKey())) && + blockScanResult.getRuleCounts().entrySet().stream() + .allMatch(entry -> entry.getValue() <= entry.getKey().Amount); } public boolean isDesignBlastResistanceOK() { @@ -132,6 +121,7 @@ public class AutoCheckerResult { } if (!isLimitedBlocksOK()) { blockScanResult.getBlockCounts().forEach((mat, count) -> { + if (type.Schematic.isInventory(mat)) return; int maxCount = type.Schematic.getMaxCount(mat); if (count > maxCount) { if (maxCount == 0) { @@ -141,30 +131,25 @@ public class AutoCheckerResult { } } }); - } - - if (!isWindchargeCountOK()) { - int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0); - SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES", p, windChargesCount, 2048); - blockScanResult.getWindChargeCount().entrySet().stream().filter(blockVector3IntegerEntry -> blockVector3IntegerEntry.getValue() > 0).forEach(blockVector3IntegerEntry -> { - SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3IntegerEntry.getKey()), - blockVector3IntegerEntry.getKey().getBlockX(), - blockVector3IntegerEntry.getKey().getBlockY(), - blockVector3IntegerEntry.getKey().getBlockZ(), - blockVector3IntegerEntry.getValue()); + blockScanResult.getRuleCounts().forEach((rule, count) -> { + if (count <= rule.Amount) return; + String blocks = blockScanResult.getRuleBlockTypes().get(rule).stream().map(Material::name).collect(Collectors.joining(", ")); + if (rule.Amount == 0) { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK", p, blocks); + } else { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_BLOCK", p, blocks, count, rule.Amount); + } }); } - blockScanResult.getInventoryItemCounts().forEach((pos, itemCounts) -> { + blockScanResult.getTooManyItems().forEach((pos, caps) -> { Material inventory = blockScanResult.getInventoryBlockType().get(pos); - itemCounts.forEach((itemType, count) -> { - int cap = type.Schematic.getItemAmount(inventory, itemType); - if (count <= cap) return; + caps.forEach((itemType, cap) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_INVENTORY_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(pos), pos.getBlockX(), pos.getBlockY(), pos.getBlockZ(), - count, + blockScanResult.getInventoryItemCounts().get(pos).get(itemType), cap, itemType.name(), inventory.name()); @@ -177,6 +162,9 @@ public class AutoCheckerResult { blockScanResult.getForbiddenItems().forEach((blockVector3, materials) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); }); + blockScanResult.getMixedItems().forEach((blockVector3, materials) -> { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_MIXED_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); + }); blockScanResult.getForbiddenNbt().forEach((blockVector3, materials) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM_NBT", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); }); diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java b/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java index e0b0b2ce9..17525d8a7 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java @@ -48,6 +48,7 @@ import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandHelp.*; import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandUtils.togglePublic; @@ -171,8 +172,8 @@ public class SchematicCommand extends SWCommand { Map> toBeCheckedInvs = new HashMap<>(); - toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenItems()); - toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenNbt()); + Stream.of(result.getBlockScanResult().getForbiddenItems(), result.getBlockScanResult().getMixedItems(), result.getBlockScanResult().getForbiddenNbt()) + .forEach(invs -> invs.forEach((pos, materials) -> toBeCheckedInvs.computeIfAbsent(pos, p -> new HashSet<>()).addAll(materials))); for (Map.Entry> entry : toBeCheckedInvs.entrySet()) { BlockPos pos = entry.getKey(); @@ -197,16 +198,13 @@ public class SchematicCommand extends SWCommand { clipboard.setBlock(vector, block.toBaseBlock(builder.build())); } - for (Map.Entry> posEntry : result.getBlockScanResult().getInventoryItemCounts().entrySet()) { + for (Map.Entry> posEntry : result.getBlockScanResult().getTooManyItems().entrySet()) { BlockPos pos = posEntry.getKey(); - Material inventory = result.getBlockScanResult().getInventoryBlockType().get(pos); + Map itemCounts = result.getBlockScanResult().getInventoryItemCounts().get(pos); for (Map.Entry itemEntry : posEntry.getValue().entrySet()) { Material itemType = itemEntry.getKey(); - Integer cap = type.Schematic.getItemAmount(inventory, itemType); - if (itemEntry.getValue() <= cap) { - continue; - } + int cap = itemEntry.getValue(); BlockVector3 vector = BlockVector3.at(pos.getX(), pos.getY(), pos.getZ()); BaseBlock block = clipboard.getFullBlock(vector); @@ -215,7 +213,7 @@ public class SchematicCommand extends SWCommand { List items = new ArrayList<>(tag.getList("Items", CompoundTag.class)); Collections.reverse(items); // To let the first item be in the inventory List list = new ArrayList<>(); - int diff = itemEntry.getValue() - cap; + int diff = itemCounts.get(itemType) - cap; for (CompoundTag item : items) { if (item == null) { continue; diff --git a/SpigotCore/SpigotCore_Main/src/SpigotCore.properties b/SpigotCore/SpigotCore_Main/src/SpigotCore.properties index e7e13eeb2..566226600 100644 --- a/SpigotCore/SpigotCore_Main/src/SpigotCore.properties +++ b/SpigotCore/SpigotCore_Main/src/SpigotCore.properties @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -LOCAL_CHAT = §eLocal §r{0}§8»§7 {1} COMMAND_SYSTEM_ERROR = §cError executing the command! diff --git a/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties b/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties index b7cba3c4f..c1cf8e755 100644 --- a/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties +++ b/SpigotCore/SpigotCore_Main/src/SpigotCore_de.properties @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -LOCAL_CHAT = §eLokal §r{0}§8»§7 {1} COMMAND_SYSTEM_ERROR = §cFehler beim Ausführen des Befehls! diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/SWPlayer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/SWPlayer.java index 692c2031d..f5cbffe4c 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/SWPlayer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/SWPlayer.java @@ -21,8 +21,8 @@ package de.steamwar.core; import de.steamwar.message.Message; import lombok.Data; +import lombok.Getter; import lombok.NonNull; -import lombok.experimental.Delegate; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.ClickEvent; import org.bukkit.Bukkit; @@ -99,11 +99,11 @@ public class SWPlayer { @Data public static final class SWPlayerWithComponent { - private final SWPlayer player; + private final SWPlayer swPlayer; private final T component; } - @Delegate + @Getter private final Player player; private final Map, Component> components = new HashMap<>(); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java index 9d42538d8..eb1f180ae 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java @@ -20,6 +20,7 @@ package de.steamwar.core.worldedit; import com.comphenix.tinyprotocol.TinyProtocol; +import com.sk89q.worldedit.IncompleteRegionException; import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.WorldEditPlugin; @@ -29,16 +30,13 @@ import com.sk89q.worldedit.regions.selector.limit.PermissiveSelectorLimits; import com.sk89q.worldedit.world.World; import de.steamwar.core.Core; import de.steamwar.core.SWPlayer; -import de.steamwar.entity.CWireframe; -import de.steamwar.entity.REntityServer; import de.steamwar.inventory.SWItem; import net.minecraft.core.BlockPos; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.*; -import org.bukkit.Bukkit; -import org.bukkit.FluidCollisionMode; -import org.bukkit.Location; -import org.bukkit.Material; +import org.bukkit.*; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -46,6 +44,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.RayTraceResult; @@ -64,6 +63,16 @@ public class SteamEditWand implements Listener { }; public static final ItemStack ITEM = new SWItem(MATERIAL, NAME).setLore(LORE).getItemStack(); + static { + ItemMeta itemMeta = ITEM.getItemMeta(); + itemMeta.addAttributeModifier(Attribute.BLOCK_INTERACTION_RANGE, new AttributeModifier( + NamespacedKey.fromString("range", Core.getInstance()), + 35.0, + AttributeModifier.Operation.ADD_NUMBER + )); + ITEM.setItemMeta(itemMeta); + } + private boolean noWand(Player player) { return player.getInventory().getItemInMainHand().getType() != SteamEditWand.MATERIAL; } @@ -86,15 +95,6 @@ public class SteamEditWand implements Listener { TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, this::rightClick); TinyProtocol.instance.addFilter(ServerboundPickItemFromBlockPacket.class, this::middleClick); TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, this::swapHands); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, this::movePlayerPacket); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Rot.class, this::movePlayerPacket); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, this::movePlayerPacket); - - Bukkit.getScheduler().runTaskTimer(Core.getInstance(), () -> { - for (Player player : Bukkit.getOnlinePlayers()) { - if (noWand(player)) SWPlayer.of(player).removeComponent(SWEditCursor.class); - } - }, 1, 1); } @EventHandler @@ -103,35 +103,6 @@ public class SteamEditWand implements Listener { event.setCancelled(true); } - public static class SWEditCursor implements SWPlayer.Component { - private Player player; - private REntityServer entityServer = new REntityServer(); - private CWireframe wireframe; - - @Override - public void onMount(SWPlayer player) { - this.player = player.getPlayer(); - entityServer.addPlayer(player.getPlayer()); - wireframe = new CWireframe(entityServer); - } - - @Override - public void onUnmount(SWPlayer player) { - entityServer.close(); - } - - public void setLocation(Location location) { - float distance = (float) player.getLocation().distanceSquared(location); - wireframe.setPos1And2(location, location); - float width = distance / 1600; - width = ((int) (width * 16)) / 16F; - width = Math.max(width, 1 / 16F); - width = Math.min(width, 1 / 2F); - wireframe.setWidth(width); - wireframe.setBlock(WorldEditRendererCUIEditor.Type.CURSOR.getMaterial(player).createBlockData()); - } - } - private Packet leftClick(Player player, Packet packet) { if (noWand(player)) return packet; if (player.getOpenInventory().getType() != InventoryType.CREATIVE) return packet; @@ -338,7 +309,11 @@ public class SteamEditWand implements Listener { } private void selectPos1(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos1) { - if (!pos1.equals(regionSelector.getPrimaryPosition())) { + try { + if (!pos1.equals(regionSelector.getPrimaryPosition())) { + SWPlayer.of(player).setComponent(new Pos1Message(regionSelector, player, session, pos1)); + } + } catch (IncompleteRegionException e) { SWPlayer.of(player).setComponent(new Pos1Message(regionSelector, player, session, pos1)); } regionSelector.selectPrimary(pos1, PermissiveSelectorLimits.getInstance()); @@ -362,7 +337,11 @@ public class SteamEditWand implements Listener { } private void selectPos2(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos2) { - if (regionSelector.getVertices().size() == 2 && !pos2.equals(regionSelector.getVertices().get(1))) { + try { + if (regionSelector.getVertices().size() == 2 && !pos2.equals(regionSelector.getVertices().get(1))) { + SWPlayer.of(player).setComponent(new Pos2Message(regionSelector, player, session, pos2)); + } + } catch (IncompleteRegionException e) { SWPlayer.of(player).setComponent(new Pos2Message(regionSelector, player, session, pos2)); } regionSelector.selectSecondary(pos2, PermissiveSelectorLimits.getInstance()); @@ -377,30 +356,4 @@ public class SteamEditWand implements Listener { }); return null; } - - public ServerboundMovePlayerPacket movePlayerPacket(Player player, ServerboundMovePlayerPacket packet) { - SWPlayer swPlayer = SWPlayer.of(player); - - if (noWand(player)) { - swPlayer.removeComponent(SWEditCursor.class); - return packet; - } - - double distance = WorldEditRendererCUIEditor.Type.CURSOR.getWidth(player).blocks; - RayTraceResult result = player.rayTraceBlocks(distance, FluidCollisionMode.NEVER); - Block block = result == null ? null : result.getHitBlock(); - if (result == null || block == null) { - swPlayer.removeComponent(SWEditCursor.class); - return packet; - } - - if (!swPlayer.hasComponent(SWEditCursor.class)) { - swPlayer.setComponent(new SWEditCursor()); - } - - swPlayer.getComponent(SWEditCursor.class).ifPresent(cursor -> { - cursor.setLocation(block.getLocation()); - }); - return packet; - } } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java index c7c50e88b..16a332672 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/scoreboard/SWScoreboard.java @@ -67,7 +67,9 @@ public class SWScoreboard { } public void removeScoreboard(Player player) { - player.getScoreboard().getObjective(SIDEBAR).unregister(); + if (player.getScoreboard().getObjective(SIDEBAR) != null) { + player.getScoreboard().getObjective(SIDEBAR).unregister(); + } playerBoards.remove(player); } } diff --git a/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties b/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties index 311e52637..fc06646ac 100644 --- a/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties +++ b/TNTLeague/src/de/steamwar/tntleague/TNTLeague.properties @@ -53,6 +53,3 @@ DEALER_REDSTONE = §eRedstone DEALER_BLOCKS = §eBlocks DEALER_TOOLS = §eTools DEALER_ANGLES = §eAngle Blocks - -PARTICIPANT_CHAT = {0} {1}§8» §7{2} -SPECTATOR_CHAT = §7{0}§8» §7{1} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt b/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt index b5f361acc..c03538124 100644 --- a/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt +++ b/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt @@ -22,7 +22,6 @@ package de.steamwar.tntleague.events import de.steamwar.tntleague.config.TNTLeagueWorldConfig import de.steamwar.tntleague.game.TNTLeagueGame import de.steamwar.tntleague.game.TNTLeagueTeam -import de.steamwar.tntleague.message import de.steamwar.tntleague.plugin import org.bukkit.GameMode import org.bukkit.Material @@ -90,20 +89,4 @@ object GlobalListener : Listener { null -> e.respawnLocation = TNTLeagueWorldConfig.lobby } } - - - @EventHandler - fun handlePlayerChat(event: AsyncPlayerChatEvent) { - val player = event.player - val msg = event.message - - val fightTeam = TNTLeagueGame.getTeam(player) - if (fightTeam != null) { - message.broadcastPrefixless("PARTICIPANT_CHAT", fightTeam.name, player.name, msg) - } else { - message.broadcastPrefixless("SPECTATOR_CHAT", player.name, msg) - } - - event.isCancelled = true - } } \ No newline at end of file diff --git a/TowerRun/src/TowerRun.properties b/TowerRun/src/TowerRun.properties index f008ae6cf..55fe89eb1 100644 --- a/TowerRun/src/TowerRun.properties +++ b/TowerRun/src/TowerRun.properties @@ -34,6 +34,3 @@ GAME_TIME = §a{0}:{1} CATCH_UP_WARNING = §4!! §cYou should catch up §4!! COMMAND_START = §aThe game will start soon§8! - -PARTICIPANT_CHAT = {0} {1}§8» §7{2} -SPECTATOR_CHAT = §7{0}§8» §7{1} \ No newline at end of file diff --git a/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java b/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java index 96248e403..324fddcad 100644 --- a/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java +++ b/TowerRun/src/de/steamwar/towerrun/listener/GlobalListener.java @@ -20,19 +20,15 @@ package de.steamwar.towerrun.listener; import de.steamwar.linkage.Linked; -import de.steamwar.towerrun.TowerRun; import de.steamwar.towerrun.config.WorldConfig; import de.steamwar.towerrun.game.TowerRunGame; import de.steamwar.towerrun.game.TowerRunPlayer; -import de.steamwar.towerrun.state.GameState; import de.steamwar.towerrun.state.GameStateBukkitListener; import de.steamwar.towerrun.state.GameStates; import org.bukkit.Bukkit; -import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; @@ -71,25 +67,4 @@ public class GlobalListener extends GameStateBukkitListener implements Listener public void onPlayerRespawn(PlayerRespawnEvent event) { event.setRespawnLocation(WorldConfig.SPAWN); } - - @EventHandler - public void onAsyncPlayerChat(AsyncPlayerChatEvent event) { - TowerRunPlayer player = TowerRunPlayer.get(event.getPlayer()); - if (GameState.getCurrentState() == GameStates.RUNNING && (TowerRunGame.isAlive(player) || TowerRunGame.isEscaped(player))) { - String prefix; - if (TowerRunGame.isAlive(player)) { - if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) { - prefix = "§c☠"; - } else { - prefix = "§6❤"; - } - } else { - prefix = "§a✔"; - } - TowerRun.getMessage().broadcastPrefixless("PARTICIPANT_CHAT", prefix, event.getPlayer().getName(), event.getMessage()); - } else { - TowerRun.getMessage().broadcastPrefixless("SPECTATOR_CHAT", event.getPlayer().getName(), event.getMessage()); - } - event.setCancelled(true); - } } diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java index 88902b9a3..711bab091 100644 --- a/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java @@ -138,14 +138,16 @@ public class Subserver { writer.println(command); } + private ProcessHandle serverProcess() { + if (checkpoint) { + return process.children().findAny().orElseGet(process::toHandle); + } + return process.toHandle(); + } + public void sleep() { try { - long pid = process.pid(); - if (checkpoint) { - pid = process.children().findAny().map(ProcessHandle::pid).orElse(pid); - } - - Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(pid)}); + Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(serverProcess().pid())}); } catch (IOException e) { logger.log(Level.SEVERE, "Failed to send SIGUSR1 to subserver.", e); } @@ -162,7 +164,7 @@ public class Subserver { public void stop() { try { - process.destroy(); + serverProcess().destroy(); if (!process.waitFor(1, TimeUnit.MINUTES)) { forceStop(); } else if (thread.isAlive()) { @@ -177,6 +179,7 @@ public class Subserver { public void forceStop() { try { logger.log(Level.SEVERE, () -> serverName + " did not stop correctly, forcibly stopping!"); + serverProcess().destroyForcibly(); process.destroyForcibly(); if (thread.isAlive()) thread.join(); diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties index 6afbaf7e9..4f988ef19 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -430,7 +430,6 @@ TEAM_HELP_NAME = §8/§7team changename §8- §7Change your team name. TEAM_HELP_COLOR = §8/§7team changecolor §8- §7Change your team color. TEAM_HELP_LEADER = §8/§7team promote §8- §7Promote someone to leader. TEAM_HELP_STEP_BACK = §8/§7team stepback §8- §7Demote yourself from leader. -TEAM_HELP_SERVER = §8/§7team server §8[§eIP/address§8] §8(§7port§8) §8- §7Set the address for your teamserver. #Team Create TEAM_CREATE_USAGE = §8/§7team create §8[§eteam shortcut§8] §8[§eteam name§8] @@ -574,12 +573,59 @@ CHAT_BC_USAGE = §8/§7bc §8[§emessage§8] CHAT_NO_RECEIVER = §cNobody receives your message CHAT_EMPTY = §cDon\'t write meaningless empty messages. -CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §8TC §e{0}§8» §f{2} -CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_SERVERTEAM_SENDER = §e{0} +CHAT_SERVERTEAM = §8» §f{2} +CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} +CHAT_GLOBAL = §8» {7}{2} +CHAT_TEAM_SENDER = §e{0} +CHAT_TEAM = §8» §f{2} +CHAT_MSG_SENDER = §e{0} +CHAT_MSG_SEPARATOR = §8» +CHAT_MSG_RECEIVER = §e{0} +CHAT_MSG = §7{0} +CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} +CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1} +CHAT_FIGHT_PARTICIPANT = §8» §7{0} +CHAT_FIGHT_SPECTATOR_SENDER = §7{1} +CHAT_FIGHT_SPECTATOR = §8» §7{0} +CHAT_FIGHT_REFEREE_SENDER = §e{1} +CHAT_FIGHT_REFEREE = §8» §e{0} +CHAT_FIGHT_TEAM = §8» §f{0} +CHAT_ICON = {0}{1} +CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2} +CHAT_HOVER_LOCAL = {0}\n§7Server: §e{1} +CHAT_ICON_GLOBAL = 🌍 +CHAT_ICON_LOCAL = ⌂ +CHAT_ICON_FIGHT = ⚔ +CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0} +CHAT_ICON_FIGHT_TEAM = {0}⚑ +CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1} +CHAT_ICON_TEAM = 👥 +CHAT_ICON_SERVERTEAM = ⚙ +CHAT_ICON_MSG = ✉ +CHAT_CHANNEL_GLOBAL = §eGlobal +CHAT_CHANNEL_ARENA = §eFight +CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} +CHAT_CHANNEL_TEAM = §eTeamChat +CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat +CHAT_CHANNEL_MSG = §eMSG §7{0} +CHAT_CHANNEL_ALERT = §cAlert +CHAT_CHANNEL_LOCAL = §eLocal +CHAT_CHANNEL_DEFAULT = §ais your default channel +CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default. +CHAT_NO_WRITE = §cYou cannot write into this channel. +CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team. +GLOBAL_USAGE = §8/§7global §8[§emessage§8] +CHAT_USAGE = §8/§7chat §8[§echannel§8] [§ejoin§8|§eleave§8] +CHAT_MSG_USAGE = §8/§7chat §8[§echannel§8] §7msg §8[§emessage§8] +CHAT_ARENAS_USAGE = §8/§7chat §7arenas §8[§ejoin§8|§eleave§8] +CHAT_ARENAS_JOINED = §7You now read the chats of all fights. +CHAT_ARENAS_LEFT = §7You no longer read the chats of all fights. +CHAT_GLOBAL_AFTER_WRITING = §7You read §e{0} §7on this server until you leave it. +CHAT_CHANNEL_UNKNOWN = §cUnknown chat channel. +CHAT_CHANNEL_JOINED = §7You are reading §e{0} §7again. +CHAT_CHANNEL_LEFT = §7You no longer read §e{0}§7. +CHAT_CHANNEL_LEAVE_DEFAULT = §cYou cannot leave the channel you are writing into. Switch to another channel first. #CheckListner CHECK_UNCHECKED = §7You still have §e{0} §7unchecked schematic§8(§7s§8)! diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties index 5921259a9..d352cdf37 100644 --- a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -399,7 +399,6 @@ TEAM_HELP_NAME = §8/§7team changename §8- §7Ändere deinen Teamnamen. TEAM_HELP_COLOR = §8/§7team changecolor §8- §7Ändere deine Teamfarbe. TEAM_HELP_LEADER = §8/§7team promote §8- §7Ernenne jemanden zum Teamleader. TEAM_HELP_STEP_BACK = §8/§7team stepback §8- §7Tritt als Leader zurück. -TEAM_HELP_SERVER = §8/§7team server §8[§eIP/Adresse§8] §8(§7Port§8) §8- §7Setzt Adresse des Teamservers. #Team Create TEAM_CREATE_USAGE = §8/§7team create §8[§eTeamkürzel§8] §8[§eTeamname§8] @@ -539,12 +538,59 @@ CHAT_BC_USAGE = §8/§7bc §8[§eNachricht§8] CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten. -CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} -CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} -CHAT_TEAM = §8TC §e{0}§8» §f{2} -CHAT_MSG = §e{0}§8»§e{1} §7{2} +CHAT_SERVERTEAM_SENDER = §e{0} +CHAT_SERVERTEAM = §8» §f{2} +CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0} +CHAT_GLOBAL = §8» {7}{2} +CHAT_TEAM_SENDER = §e{0} +CHAT_TEAM = §8» §f{2} +CHAT_MSG_SENDER = §e{0} +CHAT_MSG_SEPARATOR = §8» +CHAT_MSG_RECEIVER = §e{0} +CHAT_MSG = §7{0} +CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2} +CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1} +CHAT_FIGHT_PARTICIPANT = §8» §7{0} +CHAT_FIGHT_SPECTATOR_SENDER = §7{1} +CHAT_FIGHT_SPECTATOR = §8» §7{0} +CHAT_FIGHT_REFEREE_SENDER = §e{1} +CHAT_FIGHT_REFEREE = §8» §e{0} +CHAT_FIGHT_TEAM = §8» §f{0} +CHAT_ICON = {0}{1} +CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2} +CHAT_HOVER_LOCAL = {0}\n§7Server: §e{1} +CHAT_ICON_GLOBAL = 🌍 +CHAT_ICON_LOCAL = ⌂ +CHAT_ICON_FIGHT = ⚔ +CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0} +CHAT_ICON_FIGHT_TEAM = {0}⚑ +CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1} +CHAT_ICON_TEAM = 👥 +CHAT_ICON_SERVERTEAM = ⚙ +CHAT_ICON_MSG = ✉ +CHAT_CHANNEL_GLOBAL = §eGlobal +CHAT_CHANNEL_ARENA = §eFight +CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1} +CHAT_CHANNEL_TEAM = §eTeamChat +CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat +CHAT_CHANNEL_MSG = §eMSG §7{0} +CHAT_CHANNEL_ALERT = §cAlert +CHAT_CHANNEL_LOCAL = §eLokal +CHAT_CHANNEL_DEFAULT = §aist dein Standardkanal +CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben. +CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben. +CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam. +GLOBAL_USAGE = §8/§7global §8[§eNachricht§8] +CHAT_USAGE = §8/§7chat §8[§eKanal§8] [§ejoin§8|§eleave§8] +CHAT_MSG_USAGE = §8/§7chat §8[§eKanal§8] §7msg §8[§eNachricht§8] +CHAT_ARENAS_USAGE = §8/§7chat §7arenas §8[§ejoin§8|§eleave§8] +CHAT_ARENAS_JOINED = §7Du liest jetzt die Chats aller Fights mit. +CHAT_ARENAS_LEFT = §7Du liest die Chats aller Fights nicht mehr mit. +CHAT_GLOBAL_AFTER_WRITING = §7Du liest §e{0} §7auf diesem Server mit, bis du ihn verlässt. +CHAT_CHANNEL_UNKNOWN = §cUnbekannter Chatkanal. +CHAT_CHANNEL_JOINED = §7Du liest §e{0} §7wieder mit. +CHAT_CHANNEL_LEFT = §7Du liest §e{0} §7nicht mehr mit. +CHAT_CHANNEL_LEAVE_DEFAULT = §cDu kannst den Kanal, in den du schreibst, nicht verlassen. Wechsle zuerst in einen anderen Kanal. #CheckListner CHECK_UNCHECKED = §7Du hast noch §e{0} §7ungeprüfte Schematic§8(§7s§8)! diff --git a/VelocityCore/src/de/steamwar/messages/Chatter.java b/VelocityCore/src/de/steamwar/messages/Chatter.java index 5cc74f23b..fc34a4f7f 100644 --- a/VelocityCore/src/de/steamwar/messages/Chatter.java +++ b/VelocityCore/src/de/steamwar/messages/Chatter.java @@ -23,8 +23,6 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; import de.steamwar.velocitycore.VelocityCore; @@ -61,13 +59,6 @@ public interface Chatter { return new ChatterGroup(allStream()); } - static ChatterGroup globalChat() { - return new ChatterGroup(Stream.concat(Stream.of(Chatter.console()), allPlayers().filter(player -> { - Subserver subserver = Subserver.getSubserver(player); - return !(Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null)); - }).map(Chatter::of))); - } - static ChatterGroup serverteam() { return new ChatterGroup(allStream() .filter(player -> player.user().hasPerm(UserPerm.TEAM)) diff --git a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java index 5790e53e3..0c2c0aa8d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java @@ -31,8 +31,10 @@ import net.kyori.adventure.text.event.ClickEvent; import java.sql.Timestamp; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.concurrent.TimeUnit; import static de.steamwar.persistent.Storage.eventServer; @@ -43,8 +45,15 @@ public class EventStarter { @Getter private static final Map spectatePorts = new HashMap<>(); - public static void addSpectateServer(int port, String command) { + private static final Set spectateServers = new HashSet<>(); + + public static void addSpectateServer(int port, String command, String serverName) { spectatePorts.put(port, command); + spectateServers.add(serverName); + } + + public static boolean isSpectateServer(String serverName) { + return spectateServers.contains(serverName); } public EventStarter() { diff --git a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java index 6240f0c0f..1701a10d3 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java +++ b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java @@ -236,7 +236,7 @@ public class VelocityCore implements ReloadablePlugin { String cmd = cmds.remove(0); if (server.getSpectatePort() != 0) { - EventStarter.addSpectateServer(server.getSpectatePort(), cmd); + EventStarter.addSpectateServer(server.getSpectatePort(), cmd, entry.getKey()); } new ServerSwitchCommand(cmd, entry.getKey(), server.getSpectatePort() != 0, cmds.toArray(new String[0])); diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java new file mode 100644 index 000000000..a86964fb0 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/AlertChannel.java @@ -0,0 +1,71 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class AlertChannel extends ChatChannel { + + @Override + public boolean switchable() { + return false; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_ALERT"); + } + + @Override + public Stream readers() { + return Arrays.stream(Chatter.broadcast().getChatters()); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return receiver.parse(true, new Message("ALERT", message.replace('&', '§'))); + } + + /** + * Alerts are system messages: they are shown even with hidden chat and bypass the chat filter. + */ + @Override + public void send(Chatter sender, String message, boolean mirror) { + if (!canWrite(sender)) return; + + readers().forEach(reader -> reader.sendMessage(format(reader, sender, message))); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java new file mode 100644 index 000000000..4bc9d9193 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatChannel.java @@ -0,0 +1,146 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + +public abstract class ChatChannel { + + /** + * @return the id under which this channel is stored as default channel of a player, null if it cannot be stored + */ + public String persistId() { + return null; + } + + public boolean switchable() { + return true; + } + + /** + * @return the channel name shown in channel related system messages + */ + public abstract Message tag(Chatter receiver); + + /** + * @return the text shown when hovering the channel icon + */ + public Message hover(Chatter receiver) { + return tag(receiver); + } + + /** + * @return the icon always shown in front of messages of this channel, null if the message format already shows the channel. + * The icon is highlighted if this is the default channel of the receiver. + */ + protected Message icon(Chatter receiver) { + return null; + } + + /** + * @return the text put into the chat input when clicking the channel icon to write a single message, null if there is none + */ + protected String suggestion(Chatter receiver) { + return null; + } + + /** + * @return the channel icon with its name on hover and its write command on click + */ + Component iconComponent(Chatter receiver, String color) { + Message icon = icon(receiver); + Component component = icon == null ? receiver.parse(tag(receiver)) : receiver.parse(new Message("CHAT_ICON", color, icon)); + component = component.hoverEvent(HoverEvent.showText(receiver.parse(hover(receiver)))); + + String suggestion = suggestion(receiver); + if (suggestion != null) { + component = component.clickEvent(ClickEvent.suggestCommand(suggestion)); + } + return component; + } + + public abstract Stream readers(); + + /** + * Checks if the sender is allowed to write into this channel and informs the sender otherwise. + */ + public abstract boolean canWrite(Chatter sender); + + protected abstract Component format(Chatter receiver, Chatter sender, String message); + + /** + * Called after a message was delivered to all readers. + * + * @param mirror false if the message originates from the mirror of this channel and should not be sent back + * @return true if the message was delivered to a receiver outside the game + */ + protected boolean delivered(Chatter sender, String message, boolean mirror) { + return false; + } + + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(AuditLog.SERVER_NAME_VELOCITY, null, sender, message); + } + + public void send(Chatter sender, String message) { + send(sender, message, true); + } + + public void send(Chatter sender, String message, boolean mirror) { + if (!canWrite(sender)) return; + + SteamwarUser user = sender.user(); + String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; + if (ChatFilter.filtered(sender, coloredMessage)) return; + + Map receivers = new LinkedHashMap<>(); + readers().forEach(reader -> receivers.putIfAbsent(reader.user().getId(), reader)); + receivers.putIfAbsent(user.getId(), sender); + + boolean noReceiver = true; + for (Chatter receiver : receivers.values()) { + if (!receiver.chatShown()) continue; + + Component msg = ChatFormat.withMentionHovers(receiver, format(receiver, sender, coloredMessage)); + if (icon(receiver) != null) { + Component icon = iconComponent(receiver, ChatManager.getDefault(receiver) == this ? "§e" : "§7"); + msg = Component.text().append(icon).append(Component.text(" ")).append(msg).build(); + } + receiver.sendMessage(msg); + + if (receiver.user().getId() != user.getId()) noReceiver = false; + } + + if (!delivered(sender, coloredMessage, mirror) && noReceiver) { + sender.system("CHAT_NO_RECEIVER"); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java new file mode 100644 index 000000000..51c2841c1 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFilter.java @@ -0,0 +1,76 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.PunishmentCommand; + +import java.util.concurrent.TimeUnit; + +public class ChatFilter { + + private ChatFilter() {} + + public static boolean filtered(Chatter sender, String message) { + if (!sender.chatShown()) { + sender.system("CHAT_RECEIVE"); + return true; + } + + if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { + sender.system("CHAT_EMPTY"); + return true; + } + + SteamwarUser user = sender.user(); + if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) { + sender.system("CHAT_NO_LINKS"); + return true; + } + + if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true; + + if (message.contains("LIXFEL")) { + specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); + } + if (message.contains("YOYONOW")) { + specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); + } + if (message.contains("CHAOSCAOT")) { + specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); + } + + return false; + } + + private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { + sender.system("CHAT_LIXFEL_ACTION_BAR"); + for (int i = 0; i < delay.length; i++) { + int finalI = i; + VelocityCore.schedule(() -> sender.prefixless("CHAT_SPECIAL_ALERT", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java new file mode 100644 index 000000000..8f4a477e7 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatFormat.java @@ -0,0 +1,158 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.server.PingPacket; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.Team; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.util.SteamwarPrefix; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.TextReplacementConfig; +import net.kyori.adventure.text.event.HoverEvent; + +import java.util.Arrays; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class ChatFormat { + + private static final Pattern MENTION = Pattern.compile("@(\\w{1,16})"); + + private ChatFormat() {} + + /** + * Formats a message with the rank and team prefixes of the sender. The sender part ({@code _SENDER}) + * shows the team of the sender on hover. + */ + public static Component player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) { + SteamwarUser user = sender.user(); + UserPerm.Prefix prefix = user.prefix(); + + String teamPrefix = ""; + if (user.hasPerm(UserPerm.TEAM) && useSwPrefix(user)) { + teamPrefix = "§eS§8W "; + } else if (user.getTeam() != 0) { + Team team = Team.byId(user.getTeam()); + teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " "; + } + + String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; + Object[] params = { + user, + msgReceiver == null ? receiver : msgReceiver, + highlightMentions(message, chatColorCode, receiver), + teamPrefix, + "", + prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), + prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", + chatColorCode}; + + Component senderPart = withTeamHover(receiver, user, receiver.parse(new Message(format + "_SENDER", params))); + return Component.text().append(senderPart).append(receiver.parse(new Message(format, params))).build(); + } + + /** + * Formats a fight chat message. The fight team name is only hoverable in event fights, where it is the actual team. + * + * @param coloredTeamName the fight team of the sender, null if the sender is not in a fight team + */ + public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String senderFormat, String format, String message) { + TextComponent.Builder line = Component.text(); + String nameColor = ""; + if (coloredTeamName != null) { + Component team = Chatter.SERIALIZER.deserialize(coloredTeamName); + Team eventTeam = ChatManager.eventTeam(server, coloredTeamName); + if (eventTeam != null) { + team = team.hoverEvent(HoverEvent.showText(teamHover(receiver, eventTeam))); + } + line.append(team).append(Component.text(" ")); + nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : ""; + } + + line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(senderFormat, nameColor, sender.user())))); + line.append(receiver.parse(new Message(format, message))); + return line.build(); + } + + /** + * Shows the team of the user when hovering the component, the SteamWar team for server team members using the SW prefix. + */ + public static Component withTeamHover(Chatter receiver, SteamwarUser user, Component component) { + if (useSwPrefix(user)) { + return component.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", "SteamWar", "§e", "S§8W")))); + } + if (user.getTeam() == 0) return component; + + return component.hoverEvent(HoverEvent.showText(teamHover(receiver, Team.byId(user.getTeam())))); + } + + private static Component teamHover(Chatter receiver, Team team) { + return receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel())); + } + + private static boolean useSwPrefix(SteamwarUser user) { + if (!user.hasPerm(UserPerm.TEAM)) { + return false; + } + + UserPerm.Prefix prefix = user.prefix(); + + boolean hasTeamPrefix = user.getTeam() != 0; + boolean hasSwPrefix = prefix != UserPerm.emptyPrefix; + + if (!hasSwPrefix) return false; + if (!hasTeamPrefix) return true; + + return SteamwarPrefix.usesSWPrefix(user); + } + + /** + * Shows the team of every mentioned player ({@code @name}) on hover, like at the sender name. + */ + public static Component withMentionHovers(Chatter receiver, Component component) { + return component.replaceText(TextReplacementConfig.builder() + .match(MENTION) + .replacement((match, builder) -> { + SteamwarUser user = SteamwarUser.get(match.group(1)); + return user == null ? builder : withTeamHover(receiver, user, builder.build()); + }) + .build()); + } + + public static String highlightMentions(String message, String returnColor, Chatter player) { + if (!message.contains("@")) return message; + + String mark = "@" + player.user().getUserName(); + return Arrays.stream(message.split(" ")).map(cur -> { + if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) { + NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId())); + return "§e" + cur + returnColor; + } + return cur; + }).collect(Collectors.joining(" ")); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java new file mode 100644 index 000000000..f65be1b3a --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ChatManager.java @@ -0,0 +1,514 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.Event; +import de.steamwar.sql.Referee; +import de.steamwar.sql.Team; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserConfig; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.EventStarter; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; +import de.steamwar.velocitycore.tablist.TablistManager; +import lombok.Getter; +import net.kyori.adventure.text.Component; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +/** + * Keeps track of all chat channels and the default channel every player writes into. + */ +@Linked +public class ChatManager extends BasicListener { + + private static final String CONFIG_KEY = "chat.channel"; + private static final String HIDDEN_KEY = "chat.hidden"; + private static final String ARENAS_KEY = "chat.arenas"; + private static final long REFEREE_CACHE_MILLIS = 60_000; + + @Getter + private static final GlobalChannel global = new GlobalChannel(); + @Getter + private static final ServerTeamChannel serverTeam = new ServerTeamChannel(); + @Getter + private static final AlertChannel alert = new AlertChannel(); + + private static final Map locals = new ConcurrentHashMap<>(); + private static final Map fightTeams = new ConcurrentHashMap<>(); + private static final Map teams = new ConcurrentHashMap<>(); + private static final Set directs = ConcurrentHashMap.newKeySet(); + + private static final Map defaults = new ConcurrentHashMap<>(); + private static final Map persisted = new ConcurrentHashMap<>(); + private static final Set globalOnArena = ConcurrentHashMap.newKeySet(); + // Players reading the global chat on their current fight server until they leave it (after /global or /chat global join) + private static final Set globalOnServer = ConcurrentHashMap.newKeySet(); + private static final Set announced = ConcurrentHashMap.newKeySet(); + private static final Map> spies = new ConcurrentHashMap<>(); + private static final Map allArenas = new ConcurrentHashMap<>(); + private static final Map> leftArenas = new ConcurrentHashMap<>(); + private static final Map> hidden = new ConcurrentHashMap<>(); + + private static int refereeEvent = -1; + private static long refereesLoaded = 0; + private static Set referees = Collections.emptySet(); + + public static LocalChannel local(RegisteredServer server) { + locals.keySet().removeIf(ChatManager::unregistered); + return locals.computeIfAbsent(server, LocalChannel::new); + } + + public static FightTeamChannel fightTeam(RegisteredServer server, boolean blue) { + fightTeams.keySet().removeIf(ChatManager::unregistered); + return fightTeams.computeIfAbsent(server, s -> new FightTeamChannel[]{new FightTeamChannel(s, true), new FightTeamChannel(s, false)})[blue ? 0 : 1]; + } + + public static TeamChannel team(int team) { + return teams.computeIfAbsent(team, TeamChannel::new); + } + + public static DirectChannel direct(UUID first, UUID second) { + for (DirectChannel channel : directs) { + if (channel.contains(first) && channel.partner(first).equals(second)) { + return channel; + } + } + + DirectChannel channel = new DirectChannel(first, second); + directs.add(channel); + return channel; + } + + private static boolean unregistered(RegisteredServer server) { + return VelocityCore.getProxy().getServer(server.getServerInfo().getName()).orElse(null) != server; + } + + /** + * @return true for arenas and event spectate servers, which use a fight chat instead of the global chat + */ + public static boolean isArena(RegisteredServer server) { + return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName()); + } + + /** + * @return all servers using a fight chat (arenas and event spectate servers) + */ + public static Stream fightServers() { + return VelocityCore.getProxy().getAllServers().stream().filter(ChatManager::isArena); + } + + /** + * @return the local channel of the server the player is on, null while the player is on no server + */ + public static LocalChannel currentLocal(Player player) { + return currentServer(player).map(ChatManager::local).orElse(null); + } + + static boolean onServer(Chatter chatter, RegisteredServer server) { + Player player = chatter.getPlayer(); + return player != null && server.getPlayersConnected().contains(player); + } + + /** + * Sends the message into the channel or makes it the default channel of the player if there is no message. + */ + public static void sendOrSwitch(Chatter sender, ChatChannel channel, String[] message, String usage) { + if (message.length > 0) { + channel.send(sender, String.join(" ", message)); + } else if (sender instanceof PlayerChatter player) { + switchTo(player, channel); + } else { + sender.system(usage); + } + } + + private static Optional currentServer(Player player) { + return player.getCurrentServer().map(ServerConnection::getServer); + } + + static boolean readsGlobal(Player player) { + UUID uuid = player.getUniqueId(); + return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(uuid) || globalOnServer.contains(uuid); + } + + /** + * Shows the global chat to a player who wrote into it on a fight server until they leave the server. + */ + public static void readGlobalAfterWriting(PlayerChatter player) { + if (!currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) return; + if (!reads(player, global.persistId())) return; + + if (globalOnServer.add(player.user().getUUID())) { + player.system("CHAT_GLOBAL_AFTER_WRITING", global.tag(player)); + } + } + + /** + * @return the moderators reading the fight chats of the server without being on it + */ + static Stream spies(RegisteredServer server) { + return Chatter.allPlayers() + .map(Chatter::of) + .filter(chatter -> !onServer(chatter, server)) + .filter(chatter -> chatter.user().hasPerm(UserPerm.MODERATION)) + .filter(chatter -> readsArena(chatter.user().getUUID(), server)) + .map(Chatter.class::cast); + } + + private static boolean readsArena(UUID uuid, RegisteredServer server) { + Set joined = spies.get(uuid); + if (joined != null && joined.contains(server)) return true; + + Set left = leftArenas.get(uuid); + return readsAllArenas(uuid) && (left == null || !left.contains(server)); + } + + private static boolean readsAllArenas(UUID uuid) { + return allArenas.computeIfAbsent(uuid, u -> "true".equals(UserConfig.getConfig(u, ARENAS_KEY))); + } + + /** + * Lets a moderator read the fight chats of every fight server automatically, saved across relogs. + */ + public static void setReadAllArenas(PlayerChatter player, boolean read) { + UUID uuid = player.user().getUUID(); + allArenas.put(uuid, read); + leftArenas.remove(uuid); + UserConfig.updatePlayerConfig(uuid, ARENAS_KEY, read ? "true" : null); + player.system(read ? "CHAT_ARENAS_JOINED" : "CHAT_ARENAS_LEFT"); + } + + /** + * @return true if the user is a referee of the event fight running on the server + */ + static boolean isReferee(RegisteredServer server, int userId) { + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if (subserver == null || !EventStarter.getEventServer().containsValue(subserver)) return false; + + Event event = Event.get(); + return event != null && referees(event.getEventID()).contains(userId); + } + + private static synchronized Set referees(int eventId) { + if (eventId != refereeEvent || System.currentTimeMillis() - refereesLoaded > REFEREE_CACHE_MILLIS) { + referees = Referee.get(eventId); + refereeEvent = eventId; + refereesLoaded = System.currentTimeMillis(); + } + return referees; + } + + private static Set hidden(UUID uuid) { + return hidden.computeIfAbsent(uuid, u -> { + Set channels = ConcurrentHashMap.newKeySet(); + String config = UserConfig.getConfig(u, HIDDEN_KEY); + if (config != null && !config.isEmpty()) { + channels.addAll(Arrays.asList(config.split(","))); + } + return channels; + }); + } + + /** + * @return false if the player left the channel with the given persist id as reader + */ + static boolean reads(Chatter chatter, String persistId) { + Player player = chatter.getPlayer(); + return player == null || !hidden(player.getUniqueId()).contains(persistId); + } + + private static void saveHidden(UUID uuid) { + Set channels = hidden(uuid); + UserConfig.updatePlayerConfig(uuid, HIDDEN_KEY, channels.isEmpty() ? null : String.join(",", channels)); + } + + /** + * @return true if the channel is the fight chat of an arena the player is not on, which moderators can read without joining the arena + */ + private static boolean isForeignArena(PlayerChatter player, ChatChannel channel) { + return channel instanceof LocalChannel local && local.isArena() && !onServer(player, local.getServer()); + } + + public static void join(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (isForeignArena(player, channel)) { + RegisteredServer server = ((LocalChannel) channel).getServer(); + spies.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(server); + Set left = leftArenas.get(uuid); + if (left != null) left.remove(server); + player.system("CHAT_CHANNEL_JOINED", channel.tag(player)); + return; + } + + if (hidden(uuid).remove(channel.persistId())) { + saveHidden(uuid); + } + if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) { + globalOnServer.add(uuid); + } + player.system("CHAT_CHANNEL_JOINED", channel.tag(player)); + } + + public static void leave(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (isForeignArena(player, channel)) { + RegisteredServer server = ((LocalChannel) channel).getServer(); + Set servers = spies.get(uuid); + if (servers != null) servers.remove(server); + // Excludes the arena for this session while reading all arenas + leftArenas.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(server); + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + return; + } + + String id = channel.persistId(); + if (id.equals(getDefault(player).persistId())) { + player.system("CHAT_CHANNEL_LEAVE_DEFAULT"); + return; + } + + // On fight servers leaving only stops reading the global chat on this server, it does not leave the global chat everywhere + if (channel == global && globalOnServer.remove(uuid)) { + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + return; + } + + if (hidden(uuid).add(id)) { + saveHidden(uuid); + } + player.system("CHAT_CHANNEL_LEFT", channel.tag(player)); + } + + public static FightInfoPacket getFightInfo(RegisteredServer server) { + return TablistManager.getFightInfo(server); + } + + /** + * @return the team of an event fight on the server whose colored name is shown, null if the server runs no event fight + */ + static Team eventTeam(RegisteredServer server, String coloredTeamName) { + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if (subserver == null) return null; + + String kuerzel = coloredTeamName.replaceAll("§.", ""); + for (Map.Entry entry : new ArrayList<>(EventStarter.getEventServer().entrySet())) { + if (entry.getValue() != subserver) continue; + + Team team = Team.byId(entry.getKey()); + if (team != null && team.getTeamKuerzel().equals(kuerzel)) return team; + } + return null; + } + + /** + * Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default. + */ + public static void updateFightInfo(RegisteredServer server, FightInfoPacket previous, FightInfoPacket packet) { + + Set joined = new HashSet<>(packet.getBluePlayers()); + joined.addAll(packet.getRedPlayers()); + if (previous != null) { + previous.getBluePlayers().forEach(joined::remove); + previous.getRedPlayers().forEach(joined::remove); + } + + for (Player player : server.getPlayersConnected()) { + if (joined.contains(SteamwarUser.get(player.getUniqueId()).getId())) { + setDefault(Chatter.of(player), local(server)); + } + } + } + + /** + * @return the channel the receiver writes into by default; non-player chatters (console, Discord) use the global chat + */ + public static ChatChannel getDefault(Chatter chatter) { + Player player = chatter.getPlayer(); + if (player == null) return global; + + return defaults.computeIfAbsent(player.getUniqueId(), uuid -> initialDefault(player, currentServer(player).orElse(null))); + } + + private static ChatChannel initialDefault(Player player, RegisteredServer server) { + if (server != null && isArena(server)) return local(server); + return resolvePersisted(player, server); + } + + private static ChatChannel resolvePersisted(Player player, RegisteredServer server) { + String id = persisted.computeIfAbsent(player.getUniqueId(), uuid -> Optional.ofNullable(UserConfig.getConfig(uuid, CONFIG_KEY)).orElse(global.persistId())); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + return switch (id) { + case "local" -> server != null && !isArena(server) ? local(server) : global; + case "team" -> user.getTeam() != 0 ? team(user.getTeam()) : global; + case "serverteam" -> user.hasPerm(UserPerm.TEAM) ? serverTeam : global; + default -> global; + }; + } + + /** + * Makes the channel the default channel of the player because the player chose it. + */ + public static void switchTo(PlayerChatter player, ChatChannel channel) { + if (!channel.switchable() || isForeignArena(player, channel)) { + player.system("CHAT_CHANNEL_NOT_SWITCHABLE"); + return; + } + if (!channel.canWrite(player)) return; + + UUID uuid = player.user().getUUID(); + if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) { + globalOnArena.add(uuid); + } else if (channel != global) { + globalOnArena.remove(uuid); + } + + String id = channel.persistId(); + if (id != null && !id.equals(persisted.get(uuid))) { + persisted.put(uuid, id); + UserConfig.updatePlayerConfig(uuid, CONFIG_KEY, id); + } + if (id != null && hidden(uuid).remove(id)) { + saveHidden(uuid); + } + + defaults.put(uuid, channel); + notifyDefault(player, channel); + } + + private static void setDefault(PlayerChatter player, ChatChannel channel) { + UUID uuid = player.user().getUUID(); + if (channel != global) globalOnArena.remove(uuid); + + ChatChannel previous = defaults.put(uuid, channel); + if (announced.add(uuid)) { + notifyDefault(player, channel); + } else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) { + notifyDefault(player, channel); + } + } + + /** + * Tells the player which channel they write into, showing the channel icon with its name on hover. + */ + public static void notifyDefault(PlayerChatter player, ChatChannel channel) { + player.sendMessage(Component.text() + .append(channel.iconComponent(player, "§a")) + .append(Component.text(" ")) + .append(player.parse(new Message("CHAT_CHANNEL_DEFAULT"))) + .build()); + } + + /** + * Handles a chat message the player typed without a command. + */ + public static void chat(PlayerChatter sender, String message) { + ChatChannel channel = getDefault(sender); + if (channel instanceof TeamChannel team && team.getTeam() != sender.user().getTeam()) { + channel = resolvePersisted(sender.getPlayer(), currentServer(sender.getPlayer()).orElse(null)); + setDefault(sender, channel); + } + + if (message.startsWith("+")) { + channel = quickChannel(sender, channel); + message = message.substring(1); + } + + channel.send(sender, message); + channel.audit(sender.user(), message); + } + + /** + * The channel a message prefixed with '+' is sent to: the local chat when writing into the global chat, + * the fight team chat when writing into the local/fight chat. + */ + private static ChatChannel quickChannel(PlayerChatter sender, ChatChannel current) { + RegisteredServer server = currentServer(sender.getPlayer()).orElse(null); + if (server == null) return current; + + if (current instanceof LocalChannel local) { + FightTeamChannel blue = fightTeam(server, true); + if (blue.isMember(sender)) return blue; + + FightTeamChannel red = fightTeam(server, false); + if (red.isMember(sender)) return red; + + // Spectators on fight servers stay in the fight chat + return local.isArena() ? local : global; + } + + return local(server); + } + + @Subscribe + public void onServerPostConnect(ServerPostConnectEvent event) { + Player player = event.getPlayer(); + RegisteredServer server = currentServer(player).orElse(null); + if (server == null) return; + + UUID uuid = player.getUniqueId(); + globalOnArena.remove(uuid); + globalOnServer.remove(uuid); + + ChatChannel current = defaults.get(uuid); + ChatChannel next; + if (isArena(server)) { + next = local(server); + } else if (!announced.contains(uuid) || current instanceof LocalChannel || (event.getPreviousServer() != null && isArena(event.getPreviousServer()))) { + next = resolvePersisted(player, server); + } else { + next = current; + } + + setDefault(Chatter.of(player), next); + } + + @Subscribe + public void onDisconnect(DisconnectEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + defaults.remove(uuid); + persisted.remove(uuid); + globalOnArena.remove(uuid); + globalOnServer.remove(uuid); + announced.remove(uuid); + spies.remove(uuid); + allArenas.remove(uuid); + leftArenas.remove(uuid); + hidden.remove(uuid); + + directs.removeIf(channel -> channel.contains(uuid)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java new file mode 100644 index 000000000..5b89b8baa --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/DirectChannel.java @@ -0,0 +1,128 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.IgnoreSystem; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.VelocityCore; +import net.kyori.adventure.text.Component; + +import java.util.UUID; +import java.util.stream.Stream; + +import static de.steamwar.persistent.Storage.lastChats; + +/** + * A temporary channel between two players, closed as soon as one of them goes offline. + */ +public class DirectChannel extends ChatChannel { + + private final UUID first; + private final UUID second; + + DirectChannel(UUID first, UUID second) { + this.first = first; + this.second = second; + } + + public boolean contains(UUID uuid) { + return first.equals(uuid) || second.equals(uuid); + } + + public UUID partner(UUID uuid) { + return first.equals(uuid) ? second : first; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID()))); + } + + @Override + public boolean switchable() { + return false; + } + + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_MSG"); + } + + @Override + protected String suggestion(Chatter receiver) { + return "/msg " + SteamwarUser.get(partner(receiver.user().getUUID())).getUserName() + " "; + } + + @Override + public Stream readers() { + return Stream.of(first, second) + .map(uuid -> VelocityCore.getProxy().getPlayer(uuid).orElse(null)) + .filter(player -> player != null) + .map(Chatter::of); + } + + @Override + public boolean canWrite(Chatter sender) { + UUID partner = partner(sender.user().getUUID()); + if (VelocityCore.getProxy().getPlayer(partner).isEmpty()) { + sender.system("MSG_OFFLINE"); + return false; + } + + if (IgnoreSystem.isIgnored(partner, sender.user().getUUID())) { + sender.system("MSG_IGNORED"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + SteamwarUser partner = SteamwarUser.get(partner(sender.user().getUUID())); + return Component.text() + .append(ChatFormat.withTeamHover(receiver, sender.user(), receiver.parse(new Message("CHAT_MSG_SENDER", sender.user())))) + .append(receiver.parse(new Message("CHAT_MSG_SEPARATOR"))) + .append(ChatFormat.withTeamHover(receiver, partner, receiver.parse(new Message("CHAT_MSG_RECEIVER", partner)))) + .append(Component.text(" ")) + .append(receiver.parse(new Message("CHAT_MSG", ChatFormat.highlightMentions(message, "§7", receiver)))) + .build(); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + Player senderPlayer = VelocityCore.getProxy().getPlayer(sender.user().getUUID()).orElse(null); + Player partnerPlayer = VelocityCore.getProxy().getPlayer(partner(sender.user().getUUID())).orElse(null); + if (senderPlayer != null && partnerPlayer != null) { + lastChats.put(senderPlayer, partnerPlayer); + lastChats.put(partnerPlayer, senderPlayer); + } + return false; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/msg " + SteamwarUser.get(partner(sender.getUUID())).getUserName() + " " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java new file mode 100644 index 000000000..49b75cfab --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/FightTeamChannel.java @@ -0,0 +1,118 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import lombok.Getter; +import net.kyori.adventure.text.Component; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +/** + * The team chat of the blue or red team of a fight. + */ +public class FightTeamChannel extends ChatChannel { + + @Getter + private final RegisteredServer server; + private final boolean blue; + + FightTeamChannel(RegisteredServer server, boolean blue) { + this.server = server; + this.blue = blue; + } + + @Override + public boolean switchable() { + return false; + } + + private List members() { + FightInfoPacket info = ChatManager.getFightInfo(server); + if (info == null) return Collections.emptyList(); + return blue ? info.getBluePlayers() : info.getRedPlayers(); + } + + private String teamName() { + FightInfoPacket info = ChatManager.getFightInfo(server); + if (info == null) return ""; + return blue ? info.getBlueName() : info.getRedName(); + } + + public boolean isMember(Chatter chatter) { + return members().contains(chatter.user().getId()); + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_FIGHT_TEAM", server.getServerInfo().getName(), teamName()); + } + + @Override + protected Message icon(Chatter receiver) { + String teamName = teamName(); + String color = teamName.length() >= 2 && teamName.charAt(0) == '§' ? teamName.substring(0, 2) : "§7"; + if (ChatManager.onServer(receiver, server)) { + return new Message("CHAT_ICON_FIGHT_TEAM", color); + } + return new Message("CHAT_ICON_FIGHT_TEAM_FOREIGN", color, server.getServerInfo().getName()); + } + + @Override + protected String suggestion(Chatter receiver) { + // Messages starting with '+' go into the fight team chat while writing into the fight chat + return ChatManager.onServer(receiver, server) ? "+" : null; + } + + @Override + public Stream readers() { + List members = members(); + return Stream.concat( + server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> members.contains(chatter.user().getId())), + ChatManager.spies(server)); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!isMember(sender) || !ChatManager.onServer(sender, server)) { + sender.system("CHAT_FIGHT_NO_TEAM"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.fight(receiver, server, sender, teamName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_TEAM", ChatFormat.highlightMentions(message, "§f", receiver)); + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(server.getServerInfo().getName(), null, sender, "+" + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java new file mode 100644 index 000000000..67730da70 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/GlobalChannel.java @@ -0,0 +1,74 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.velocitycore.discord.DiscordBot; +import net.kyori.adventure.text.Component; + +import java.util.stream.Stream; + +public class GlobalChannel extends ChatChannel { + + @Override + public String persistId() { + return "global"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_GLOBAL"); + } + + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_GLOBAL"); + } + + @Override + protected String suggestion(Chatter receiver) { + return "/global "; + } + + @Override + public Stream readers() { + return Stream.concat(Stream.of(Chatter.console()), Chatter.allPlayers().filter(ChatManager::readsGlobal).map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId()))); + } + + @Override + public boolean canWrite(Chatter sender) { + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (mirror) { + DiscordBot.withBot(bot -> bot.getIngameChat().sendMessage(format(bot.getIngameChat(), sender, message))); + } + return true; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java new file mode 100644 index 000000000..e4d6c1314 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/LocalChannel.java @@ -0,0 +1,141 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.network.packets.server.ChatRecordPacket; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.network.NetworkSender; +import lombok.Getter; +import net.kyori.adventure.text.Component; + +import java.util.stream.Stream; + +/** + * The chat of all players on one server. On arenas this is the fight chat. + */ +public class LocalChannel extends ChatChannel { + + @Getter + private final RegisteredServer server; + + LocalChannel(RegisteredServer server) { + this.server = server; + } + + public boolean isArena() { + return ChatManager.isArena(server); + } + + @Override + public String persistId() { + return "local"; + } + + @Override + public Message tag(Chatter receiver) { + if (isArena()) { + return new Message("CHAT_CHANNEL_ARENA"); + } + return new Message("CHAT_CHANNEL_LOCAL"); + } + + @Override + public Message hover(Chatter receiver) { + return new Message("CHAT_HOVER_LOCAL", tag(receiver), server.getServerInfo().getName()); + } + + @Override + protected Message icon(Chatter receiver) { + if (!isArena()) return new Message("CHAT_ICON_LOCAL"); + + // Moderators reading an arena they are not on need to know which arena it is + if (ChatManager.onServer(receiver, server)) { + return new Message("CHAT_ICON_FIGHT"); + } + return new Message("CHAT_ICON_FIGHT_FOREIGN", server.getServerInfo().getName()); + } + + @Override + protected String suggestion(Chatter receiver) { + if (isArena() && !ChatManager.onServer(receiver, server)) { + return "/chat " + server.getServerInfo().getName() + " msg "; + } + return "/local "; + } + + @Override + public Stream readers() { + Stream players = server.getPlayersConnected().stream().map(Chatter::of).filter(chatter -> ChatManager.reads(chatter, persistId())).map(Chatter.class::cast); + if (isArena()) { + players = Stream.concat(players, ChatManager.spies(server)); + } + return players; + } + + @Override + public boolean canWrite(Chatter sender) { + // Moderators can write into fight chats they are not on (/chat msg) + if (!ChatManager.onServer(sender, server) && !(isArena() && sender.user().hasPerm(UserPerm.MODERATION))) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + if (!isArena()) { + return ChatFormat.player("CHAT_GLOBAL", receiver, sender, null, message); + } + + String highlighted = ChatFormat.highlightMentions(message, "§7", receiver); + FightInfoPacket info = ChatManager.getFightInfo(server); + int id = sender.user().getId(); + if (info != null && info.getBluePlayers().contains(id)) { + return ChatFormat.fight(receiver, server, sender, info.getBlueName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted); + } else if (info != null && info.getRedPlayers().contains(id)) { + return ChatFormat.fight(receiver, server, sender, info.getRedName(), "CHAT_FIGHT_PARTICIPANT_SENDER", "CHAT_FIGHT_PARTICIPANT", highlighted); + } else if (ChatManager.isReferee(server, id)) { + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_REFEREE_SENDER", "CHAT_FIGHT_REFEREE", highlighted); + } + return ChatFormat.fight(receiver, server, sender, null, "CHAT_FIGHT_SPECTATOR_SENDER", "CHAT_FIGHT_SPECTATOR", highlighted); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (isArena()) { + // Arenas never display chat from Velocity, they only record it for replays + NetworkSender.send(server, new ChatRecordPacket(sender.user().getId(), message)); + } + return false; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createChat(server.getServerInfo().getName(), null, sender, message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java new file mode 100644 index 000000000..1e23d787f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/ServerTeamChannel.java @@ -0,0 +1,87 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.discord.DiscordBot; +import net.kyori.adventure.text.Component; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class ServerTeamChannel extends ChatChannel { + + @Override + public String persistId() { + return "serverteam"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_SERVERTEAM"); + } + + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_SERVERTEAM"); + } + + @Override + protected String suggestion(Chatter receiver) { + return "/stc "; + } + + @Override + public Stream readers() { + return Arrays.stream(Chatter.serverteam().getChatters()).filter(chatter -> ChatManager.reads(chatter, persistId())); + } + + @Override + public boolean canWrite(Chatter sender) { + if (!sender.user().hasPerm(UserPerm.TEAM)) { + sender.system("CHAT_NO_WRITE"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_SERVERTEAM", receiver, sender, null, message); + } + + @Override + protected boolean delivered(Chatter sender, String message, boolean mirror) { + if (mirror) { + DiscordBot.withBot(bot -> bot.getServerTeamChat().sendMessage(ChatFormat.player("CHAT_GLOBAL", bot.getServerTeamChat(), sender, null, message))); + } + return true; + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/stc " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java new file mode 100644 index 000000000..4603c3d54 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/chat/TeamChannel.java @@ -0,0 +1,90 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.chat; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.AuditLog; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.commands.StreamingCommand; +import lombok.Getter; +import net.kyori.adventure.text.Component; + +import java.util.stream.Stream; + +public class TeamChannel extends ChatChannel { + + @Getter + private final int team; + + TeamChannel(int team) { + this.team = team; + } + + @Override + public String persistId() { + return "team"; + } + + @Override + public Message tag(Chatter receiver) { + return new Message("CHAT_CHANNEL_TEAM"); + } + + @Override + protected Message icon(Chatter receiver) { + return new Message("CHAT_ICON_TEAM"); + } + + @Override + protected String suggestion(Chatter receiver) { + return "/tc "; + } + + @Override + public Stream readers() { + return Chatter.allPlayers() + .map(Chatter::of) + .filter(chatter -> chatter.user().getTeam() == team) + .filter(StreamingCommand::isNotStreaming) + .filter(chatter -> ChatManager.reads(chatter, persistId())) + .map(Chatter.class::cast); + } + + @Override + public boolean canWrite(Chatter sender) { + if (sender.user().getTeam() != team) { + sender.system("TC_NO_TEAM"); + return false; + } + return true; + } + + @Override + protected Component format(Chatter receiver, Chatter sender, String message) { + return ChatFormat.player("CHAT_TEAM", receiver, sender, null, message); + } + + @Override + public void audit(SteamwarUser sender, String message) { + AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/tc " + message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java index ea8467898..552691d2f 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java @@ -23,6 +23,7 @@ import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.discord.DiscordBot; @Linked @@ -40,7 +41,7 @@ public class AlertCommand extends SWCommand { } String s = String.join(" ", message); - Chatter.broadcast().system("ALERT", s.replace('&', '§')); + ChatManager.getAlert().send(sender, s); if ("-discord".equals(sendToDiscord)) { DiscordBot.withBot(bot -> bot.getAnnouncementChannel().send(s)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java new file mode 100644 index 000000000..106042f4a --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChatCommand.java @@ -0,0 +1,123 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.commands; + +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.command.TypeValidator; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Linked +public class ChatCommand extends SWCommand { + + public ChatCommand() { + super("chat"); + } + + @Register(description = "CHAT_USAGE") + public void current(PlayerChatter sender) { + ChatManager.notifyDefault(sender, ChatManager.getDefault(sender)); + } + + @Register + public void switchTo(PlayerChatter sender, ChatChannel channel) { + ChatManager.switchTo(sender, channel); + } + + @Register + public void joinOrLeave(PlayerChatter sender, ChatChannel channel, @StaticValue(value = {"leave", "join"}, allowISE = true) boolean join) { + if (join) { + ChatManager.join(sender, channel); + } else { + ChatManager.leave(sender, channel); + } + } + + @Register(value = "arenas", description = "CHAT_ARENAS_USAGE") + public void arenas(@Validator("moderator") PlayerChatter sender, @StaticValue(value = {"leave", "join"}, allowISE = true) boolean join) { + ChatManager.setReadAllArenas(sender, join); + } + + @Register(description = "CHAT_MSG_USAGE") + public void msg(@Validator("moderator") PlayerChatter sender, ChatChannel channel, @StaticValue("msg") String msg, @ErrorMessage(value = "CHAT_MSG_USAGE", allowEAs = false) String... message) { + channel.send(sender, String.join(" ", message)); + } + + @Validator(value = "moderator", local = true) + public TypeValidator moderatorValidator() { + return (sender, value, messageSender) -> !messageSender.send(!value.user().hasPerm(UserPerm.MODERATION), "CHAT_NO_WRITE"); + } + + @ClassMapper(value = ChatChannel.class, local = true) + public TypeMapper channelTypeMapper() { + return new TypeMapper<>() { + @Override + public ChatChannel map(Chatter sender, PreviousArguments previousArguments, String s) { + return switch (s.toLowerCase()) { + case "global" -> ChatManager.getGlobal(); + case "local" -> sender.getPlayer() == null ? null : ChatManager.currentLocal(sender.getPlayer()); + case "team", "tc" -> sender.user().getTeam() != 0 ? ChatManager.team(sender.user().getTeam()) : null; + case "serverteam", "stc" -> sender.user().hasPerm(UserPerm.TEAM) ? ChatManager.getServerTeam() : null; + default -> arena(sender, s); + }; + } + + @Override + public boolean validate(Chatter sender, ChatChannel value, MessageSender messageSender) { + return !messageSender.send(value == null, "CHAT_CHANNEL_UNKNOWN"); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + List channels = new ArrayList<>(List.of("global", "local")); + if (sender.user().getTeam() != 0) channels.add("team"); + if (sender.user().hasPerm(UserPerm.TEAM)) channels.add("serverteam"); + if (sender.user().hasPerm(UserPerm.MODERATION)) { + ChatManager.fightServers().map(server -> server.getServerInfo().getName()).forEach(channels::add); + } + return channels; + } + }; + } + + /** + * Moderators can address the fight chat of every fight server by its server name. + */ + private static ChatChannel arena(Chatter sender, String name) { + if (!sender.user().hasPerm(UserPerm.MODERATION)) return null; + + return ChatManager.fightServers() + .filter(server -> server.getServerInfo().getName().equalsIgnoreCase(name)) + .findFirst() + .map(ChatManager::local) + .orElse(null); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java new file mode 100644 index 000000000..96f7eb2b5 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GlobalCommand.java @@ -0,0 +1,43 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +package de.steamwar.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.linkage.Linked; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.velocitycore.chat.ChatManager; + +@Linked +public class GlobalCommand extends SWCommand { + + public GlobalCommand() { + super("global"); + } + + @Register(description = "GLOBAL_USAGE") + public void genericCommand(Chatter sender, String... message) { + ChatManager.sendOrSwitch(sender, ChatManager.getGlobal(), message, "GLOBAL_USAGE"); + if (message.length > 0 && sender instanceof PlayerChatter player) { + ChatManager.readGlobalAfterWriting(player); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java index 40f48fbf6..b52a014f3 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java @@ -22,7 +22,8 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.PlayerChatter; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class LocalCommand extends SWCommand { @@ -31,8 +32,11 @@ public class LocalCommand extends SWCommand { super("local", "bc", "bauchat"); } - @Register - public void genericCommand(PlayerChatter player, String... message) { - ChatListener.localChat(player, String.join(" ", message)); + @Register(description = "CHAT_BC_USAGE") + public void genericCommand(PlayerChatter sender, String... message) { + ChatChannel channel = ChatManager.currentLocal(sender.getPlayer()); + if (channel == null) return; + + ChatManager.sendOrSwitch(sender, channel, message, "CHAT_BC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java index 622aa2dc5..4db12170e 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java @@ -22,12 +22,9 @@ package de.steamwar.velocitycore.commands; import com.velocitypowered.api.proxy.Player; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; -import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; import de.steamwar.messages.PlayerChatter; -import de.steamwar.sql.IgnoreSystem; import de.steamwar.velocitycore.VelocityCore; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatManager; import static de.steamwar.persistent.Storage.lastChats; @@ -49,15 +46,7 @@ public class MsgCommand extends SWCommand { return; } - if (IgnoreSystem.isIgnored(target.getUniqueId(), sender.user().getUUID())) { - sender.system("MSG_IGNORED"); - return; - } - - Chatter receiver = Chatter.of(target); - ChatListener.sendChat(sender, new ChatterGroup(sender, receiver), "CHAT_MSG", receiver, String.join(" ", args)); - lastChats.put(sender.getPlayer(), target); - lastChats.put(target, sender.getPlayer()); + ChatManager.direct(sender.user().getUUID(), target.getUniqueId()).send(sender, String.join(" ", args)); } public static void remove(Player player) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java index 0b04183e0..024c118da 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java @@ -23,7 +23,7 @@ import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.sql.UserPerm; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class ServerTeamchatCommand extends SWCommand { @@ -33,7 +33,7 @@ public class ServerTeamchatCommand extends SWCommand { } @Register(description = "STC_USAGE") - public void genericCommand(Chatter sender, @ErrorMessage(value = "STC_USAGE", allowEAs = false) String... message) { - ChatListener.sendChat(sender, Chatter.serverteam(), "CHAT_SERVERTEAM", null, String.join(" ", message)); + public void genericCommand(Chatter sender, String... message) { + ChatManager.sendOrSwitch(sender, ChatManager.getServerTeam(), message, "STC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java index c73b256c6..c3108a550 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java @@ -67,7 +67,7 @@ public class TeamCommand extends SWCommand { helpMessages(sender, "TEAM_HELP_CHAT", "TEAM_HELP_EVENT", "TEAM_HELP_LEAVE"); if (user.isLeader()) { - helpMessages(sender, "TEAM_HELP_INVITE", "TEAM_HELP_REMOVE", "TEAM_HELP_KUERZEL", "TEAM_HELP_NAME", "TEAM_HELP_COLOR", "TEAM_HELP_LEADER", "TEAM_HELP_STEP_BACK", "TEAM_HELP_SERVER"); + helpMessages(sender, "TEAM_HELP_INVITE", "TEAM_HELP_REMOVE", "TEAM_HELP_KUERZEL", "TEAM_HELP_NAME", "TEAM_HELP_COLOR", "TEAM_HELP_LEADER", "TEAM_HELP_STEP_BACK"); } } } @@ -287,6 +287,10 @@ public class TeamCommand extends SWCommand { @Register(value = "info", description = "TEAM_INFO_USAGE") public void info(Chatter sender, @ErrorMessage("UNKNOWN_TEAM") Team team) { + if (team.getMembers().isEmpty()) { + sender.system("UNKNOWN_TEAM"); + return; + } List users = team.getMembers().stream().map(SteamwarUser::byId).toList(); sender.system("TEAM_INFO_TEAM", team.getTeamName(), team.getTeamColor(), team.getTeamKuerzel()); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java index e9b1d4d26..39de54578 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java @@ -22,9 +22,7 @@ package de.steamwar.velocitycore.commands; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; -import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatManager; @Linked public class TeamchatCommand extends SWCommand { @@ -34,14 +32,13 @@ public class TeamchatCommand extends SWCommand { } @Register(description = "TC_USAGE") - public void genericCommand(Chatter sender, @ErrorMessage(value = "TC_USAGE", allowEAs = false) String... args) { - SteamwarUser user = sender.user(); - - if (user.getTeam() == 0) { + public void genericCommand(Chatter sender, String... args) { + int team = sender.user().getTeam(); + if (team == 0) { sender.system("TC_NO_TEAM"); return; } - ChatListener.sendChat(sender, new ChatterGroup(Chatter.allStream().filter(p -> p.user().getTeam() == user.getTeam()).filter(StreamingCommand::isNotStreaming)), "CHAT_TEAM", null, String.join(" ", args)); + ChatManager.sendOrSwitch(sender, ChatManager.team(team), args, "TC_USAGE"); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java index 7e7dba1ed..4b557bb7b 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java @@ -21,6 +21,7 @@ package de.steamwar.velocitycore.discord; import de.steamwar.command.SWCommand; import de.steamwar.messages.Chatter; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.sql.Event; import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.discord.channels.*; @@ -181,9 +182,9 @@ public class DiscordBot { // There is a hard limit of 30 messages per minute to send as a webhook, thus with 8 webhooks we can send // 240 messages per minute. Which means 4 every second. I looked at the WGS fights and there were around // ~70 in a short burst and then rather long no new message. - ingameChat = new DiscordChatRoom(config.channel("ingame"), "CHAT_DISCORD_GLOBAL", Chatter::globalChat, 8); + ingameChat = new DiscordChatRoom(config.channel("ingame"), ChatManager.getGlobal(), 8); // 60 messages per minute should be enough for the server team! - serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), "CHAT_DISCORD_SERVERTEAM", Chatter::serverteam, 2); + serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), ChatManager.getServerTeam(), 2); VelocityCore.schedule(() -> { try { diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java index ef02ce11d..6c69a5164 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java @@ -20,22 +20,17 @@ package de.steamwar.velocitycore.discord.channels; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; import de.steamwar.sql.Punishment; import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.velocitycore.chat.ChatChannel; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import java.util.function.Supplier; - public class DiscordChatRoom extends DiscordChannel { - private final String format; - private final Supplier target; + private final ChatChannel target; - public DiscordChatRoom(String channel, String format, Supplier target, int maxNumberOfWebhooks) { + public DiscordChatRoom(String channel, ChatChannel target, int maxNumberOfWebhooks) { super(channel, maxNumberOfWebhooks); - this.format = format; this.target = target; } @@ -47,6 +42,6 @@ public class DiscordChatRoom extends DiscordChannel { return; } - ChatListener.sendChat(Chatter.of(user.getUUID(), true), target.get(), format, null, event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' ')); + target.send(Chatter.of(user.getUUID(), true), event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' '), false); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java index ad522fee7..1da67ed66 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java @@ -27,30 +27,18 @@ import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.ServerInfo; import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; -import de.steamwar.messages.ChatterGroup; -import de.steamwar.messages.Message; -import de.steamwar.messages.PlayerChatter; -import de.steamwar.network.packets.server.PingPacket; -import de.steamwar.persistent.Subserver; import de.steamwar.sql.*; import de.steamwar.velocitycore.ArenaMode; import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.commands.PunishmentCommand; -import de.steamwar.velocitycore.discord.DiscordBot; -import de.steamwar.velocitycore.network.NetworkSender; -import de.steamwar.velocitycore.util.SteamwarPrefix; -import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; @Linked public class ChatListener extends BasicListener { @@ -138,21 +126,7 @@ public class ChatListener extends BasicListener { return; } - Subserver subserver = Subserver.getSubserver(player); - String serverName = AuditLog.SERVER_NAME_VELOCITY; - if (Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().orElseThrow().getServerInfo()) { - serverName = subserver.getServer().getName(); - localChat(Chatter.of(player), message); - } else if (message.startsWith("+")) { - serverName = player.getCurrentServer() - .map(ServerConnection::getServerInfo) - .map(ServerInfo::getName) - .orElse(serverName); - localChat(Chatter.of(player), message.substring(1)); - } else { - sendChat(Chatter.of(player), Chatter.globalChat(), "CHAT_GLOBAL", null, message); - } - AuditLog.createChat(serverName, null, user, message); + ChatManager.chat(Chatter.of(player), message); } private static boolean isMistypedCommand(Player player, String message) { @@ -172,115 +146,6 @@ public class ChatListener extends BasicListener { return isCommand; } - public static void sendChat(Chatter sender, ChatterGroup receivers, String format, Chatter msgReceiver, String message) { - SteamwarUser user = sender.user(); - final String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; - if (chatFilter(sender, coloredMessage)) return; - - boolean useSwPrefix = useSwPrefix(user); - - boolean noReceiver = true; - for (Chatter player : receivers.getChatters()) { - if (player.chatShown()) { - chatToReciever(player, msgReceiver, user, format, coloredMessage, useSwPrefix); - if (sender.user().getId() != player.user().getId()) noReceiver = false; - } - - } - - if (format.equals("CHAT_GLOBAL")) { - DiscordBot.withBot(bot -> chatToReciever(bot.getIngameChat(), msgReceiver, user, format, coloredMessage, useSwPrefix)); - } else if (format.equals("CHAT_SERVERTEAM")) { - DiscordBot.withBot(bot -> chatToReciever(bot.getServerTeamChat(), msgReceiver, user, "CHAT_GLOBAL", coloredMessage, useSwPrefix)); - } else if (noReceiver) { - sender.system("CHAT_NO_RECEIVER"); - } - } - - public static void localChat(PlayerChatter sender, String message) { - if (message.isEmpty()) { - sender.system("CHAT_BC_USAGE"); - return; - } - - if (ChatListener.filteredCommand(sender, message)) return; - - if (!message.startsWith("/") && chatFilter(sender, message)) return; - - sender.getPlayer().spoofChatInput(message); - } - - private static boolean chatFilter(Chatter sender, String message) { - if (!sender.chatShown()) { - sender.system("CHAT_RECEIVE"); - return true; - } - - if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { - sender.system("CHAT_EMPTY"); - return true; - } - - SteamwarUser user = sender.user(); - if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) { - sender.system("CHAT_NO_LINKS"); - return true; - } - - if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true; - - if (message.contains("LIXFEL")) { - specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); - } - if (message.contains("YOYONOW")) { - specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); - } - if (message.contains("CHAOSCAOT")) { - specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); - } - - return false; - } - - private static void chatToReciever(Chatter receiver, Chatter msgReceiver, SteamwarUser sender, String format, String message, boolean useSwPrefix) { - UserPerm.Prefix prefix = sender.prefix(); - - String teamPrefix = ""; - if (sender.hasPerm(UserPerm.TEAM) && useSwPrefix) { - teamPrefix = "§eS§8W "; - } else if (sender.getTeam() != 0) { - Team team = Team.byId(sender.getTeam()); - teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " "; - } - - String chatColorCode = sender.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; - receiver.prefixless(format, - sender, - msgReceiver == null ? receiver : msgReceiver, - highlightMentions(message, chatColorCode, receiver), - teamPrefix, - "", - prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(), - prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", - chatColorCode); - } - - private static boolean useSwPrefix(SteamwarUser user) { - if (!user.hasPerm(UserPerm.TEAM)) { - return false; - } - - UserPerm.Prefix prefix = user.prefix(); - - boolean hasTeamPrefix = user.getTeam() != 0; - boolean hasSwPrefix = prefix != UserPerm.emptyPrefix; - - if (!hasSwPrefix) return false; - if (!hasTeamPrefix) return true; - - return SteamwarPrefix.usesSWPrefix(user); - } - private static boolean filteredCommand(Chatter sender, String message) { String command = message.split(" ", 2)[0]; if (command.startsWith("/") && command.contains(":")) { @@ -290,27 +155,6 @@ public class ChatListener extends BasicListener { return false; } - private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { - sender.system("CHAT_LIXFEL_ACTION_BAR"); - for (int i = 0; i < delay.length; i++) { - int finalI = i; - VelocityCore.schedule(() -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); - } - } - - private static String highlightMentions(String message, String returnColor, Chatter player) { - if (!message.contains("@")) return message; - - String mark = "@" + player.user().getUserName(); - return Arrays.stream(message.split(" ")).map(cur -> { - if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) { - NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId())); - return "§e" + cur + returnColor; - } - return cur; - }).collect(Collectors.joining(" ")); - } - @Subscribe public void onTabCompleteResponseEvent(TabCompleteEvent e) { List suggestions = e.getSuggestions(); diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java index 704acfec4..b7ad0d196 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java @@ -24,6 +24,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.linkage.Linked; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.velocitycore.chat.ChatManager; import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.network.ServerMetaInfo; import de.steamwar.velocitycore.tablist.TablistManager; @@ -50,7 +51,8 @@ public class FightInfoHandler extends PacketHandler { FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName()); - TablistManager.newFightInfo(server, packet); + FightInfoPacket previous = TablistManager.newFightInfo(server, packet); + ChatManager.updateFightInfo(server, previous, packet); lobbys.removeIf(s -> s.getPlayersConnected().isEmpty()); lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket)); diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java index 7d2fc7ae7..9631720d0 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java @@ -45,9 +45,17 @@ public class TablistManager extends BasicListener implements Disable { private static final Map fightInfos = new HashMap<>(); - public static synchronized void newFightInfo(RegisteredServer info, FightInfoPacket packet) { - fightInfos.put(info, packet); + /** + * @return the previous fight info of the server, null if there was none + */ + public static synchronized FightInfoPacket newFightInfo(RegisteredServer info, FightInfoPacket packet) { + FightInfoPacket previous = fightInfos.put(info, packet); fightInfos.keySet().removeIf(serverInfo -> serverInfo.getPlayersConnected().isEmpty()); + return previous; + } + + public static synchronized FightInfoPacket getFightInfo(RegisteredServer server) { + return fightInfos.get(server); } private final Map tablists = new HashMap<>();