diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java
index e1f7b11ab..5a8cac696 100644
--- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java
+++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java
@@ -32,6 +32,7 @@ import de.steamwar.bausystem.features.tracer.TraceManager;
import de.steamwar.bausystem.features.tracer.TraceRecorder;
import de.steamwar.bausystem.features.world.BauScoreboard;
import de.steamwar.bausystem.linkage.BauGuiItem;
+import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.region.RegionSystem;
import de.steamwar.bausystem.utils.ScoreboardElement;
import de.steamwar.bausystem.utils.TickManager;
@@ -40,8 +41,9 @@ import de.steamwar.command.AbstractValidator;
import de.steamwar.command.SWCommandUtils;
import de.steamwar.core.CRIUSleepEvent;
import de.steamwar.core.Core;
-import de.steamwar.core.WorldEditRendererCUIEditor;
import de.steamwar.core.WorldIdentifier;
+import de.steamwar.core.worldedit.SteamEdit;
+import de.steamwar.core.worldedit.WorldEditRendererCUIEditor;
import de.steamwar.linkage.AbstractLinker;
import de.steamwar.linkage.SpigotLinker;
import de.steamwar.message.Message;
@@ -49,6 +51,7 @@ import de.steamwar.providers.BauServerInfo;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
+import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -129,7 +132,19 @@ public class BauSystem extends JavaPlugin implements Listener {
TraceManager.instance.init();
TraceRecorder.instance.init();
- new WorldEditRendererCUIEditor();
+ SteamEdit.init();
+ SteamEdit.ADJACENT_FILTERS = location -> {
+ Region region = Region.getRegion(location);
+ Region.Area area;
+ if (region.getBuildArea().inRegion(location, true)) {
+ area = region.getBuildArea();
+ } else if (region.getTestblockArea().inRegion(location, true)) {
+ area = region.getTestblockArea();
+ } else {
+ return SteamEdit.AdjacentFilter.ALWAYS;
+ }
+ return (x, y, z) -> area.inRegion(new Location(null, x, y, z), true);
+ };
Bukkit.getWorlds().get(0).setGameRule(GameRule.SEND_COMMAND_FEEDBACK, false);
diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/AntiPlayerCollision.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/AntiPlayerCollision.java
new file mode 100644
index 000000000..119dd65db
--- /dev/null
+++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/AntiPlayerCollision.java
@@ -0,0 +1,41 @@
+/*
+ * 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.world;
+
+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.PlayerJoinEvent;
+import org.bukkit.scoreboard.Scoreboard;
+import org.bukkit.scoreboard.Team;
+
+@Linked
+public class AntiPlayerCollision implements Listener {
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ Scoreboard board = event.getPlayer().getScoreboard();
+ Team team = board.getTeam("players");
+ if (team == null) team = board.registerNewTeam("players");
+ team.setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.NEVER);
+ team.addPlayer(event.getPlayer());
+ }
+}
diff --git a/CommonCore/SQL/build.gradle.kts b/CommonCore/SQL/build.gradle.kts
index 095287594..8706ecdfa 100644
--- a/CommonCore/SQL/build.gradle.kts
+++ b/CommonCore/SQL/build.gradle.kts
@@ -19,6 +19,7 @@
plugins {
steamwar.kotlin
+ kotlin("plugin.lombok")
}
kotlin {
@@ -40,4 +41,8 @@ dependencies {
compileOnlyApi(libs.exposedDao)
compileOnlyApi(libs.exposedJdbc)
compileOnlyApi(libs.exposedTime)
+ testImplementation(kotlin("test"))
}
+repositories {
+ mavenCentral()
+}
\ No newline at end of file
diff --git a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java
index 87c6bdb12..17bfccb5a 100644
--- a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java
+++ b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java
@@ -688,13 +688,6 @@ public final class GameModeConfig {
*/
public final int MaxBlocks;
- /**
- * Maximal amount of items per dispenser
- *
- * @implSpec {@code 128} by default
- */
- public final int MaxDispenserItems;
-
/**
* Maximal blast resistance for the blocks
*
@@ -710,10 +703,12 @@ public final class GameModeConfig {
public final double MaxDesignBlastResistance;
/**
- * List of limited material (combinations)
- * List contains tags Amount (integer) and Materials (List of material names in Spigot 1.12 AND Spigot 1.15 format)
+ * List of limited block materials
+ *
+ * @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.
*/
- public final Map, Integer> Limited;
+ public final List> Limited;
private SchematicConfig(YMLWrapper loader) {
loaded = loader.canLoad();
@@ -731,32 +726,140 @@ public final class GameModeConfig {
IgnorePublicOnly = loader.getBoolean("IgnorePublicOnly", false);
UnlimitedPrepare = loader.getBoolean("UnlimitedPrepare", false);
MaxBlocks = loader.getInt("MaxBlocks", 0);
- MaxDispenserItems = loader.getInt("MaxDispenserItems", 128);
MaxBlastResistance = loader.getDouble("MaxBlastResistance", Double.MAX_VALUE);
MaxDesignBlastResistance = loader.getDouble("MaxDesignBlastResistance", MaxBlastResistance);
- Map, Integer> Limited = new HashMap<>();
- for (Map, ?> entry : loader.getMapList("Limited")) {
- int amount = (Integer) entry.get("Amount");
- Set materials = new HashSet<>((List) entry.get("Materials"));
- if (amount == 0) {
- materials.forEach(material -> {
- Limited.put(Collections.singleton(loader.materialMapper.apply(material.toUpperCase())), 0);
- });
- } else {
- Limited.put(Collections.unmodifiableSet(materials.stream().map(String::toUpperCase).map(loader.materialMapper).collect(Collectors.toSet())), amount);
- }
+ List> limited = new ArrayList<>();
+ Set blocks = new HashSet<>();
+ for (YMLWrapper limitedLoader : loader.withAsList("Limited")) {
+ BlockRule blockRule = new BlockRule<>(limitedLoader);
+ blocks.addAll(blockRule.Materials);
+ limited.add(blockRule);
}
- SQLWrapper.impl.getMaterialWithGreaterBlastResistance(MaxBlastResistance).forEach(material -> {
- if (Limited.entrySet().stream().anyMatch(entry -> entry.getKey().contains(material))) return;
- Limited.put(Collections.singleton((M) material), 0);
- });
- this.Limited = Collections.unmodifiableMap(Limited);
+ for (M material : (List) SQLWrapper.impl.getMaterialWithGreaterBlastResistance(MaxBlastResistance)) {
+ if (material == null || !blocks.add(material)) continue;
+ limited.add(new BlockRule<>(material, 0));
+ }
+ for (M material : (Set) SQLWrapper.impl.getInventoryMaterials()) {
+ if (material == null || !blocks.add(material)) continue;
+ limited.add(new BlockRule<>(material, Integer.MAX_VALUE));
+ }
+ this.Limited = Collections.unmodifiableList(limited);
this.ReplacementsWithoutBlockUpdates = loader.getMap("ReplacementsWithoutBlockUpdates", loader.materialMapper, loader.materialMapper);
this.ReplacementsWithBlockUpdates = loader.getMap("ReplacementsWithBlockUpdates", loader.materialMapper, loader.materialMapper);
}
+ public int getMaxCount(M material) {
+ return Limited.stream()
+ .filter(rule -> rule.Materials.contains(material))
+ .mapToInt(rule -> rule.Amount)
+ .max()
+ .orElse(Integer.MAX_VALUE);
+ }
+
+ public boolean isInventory(M material) {
+ return Limited.stream()
+ .filter(rule -> rule.Materials.contains(material))
+ .anyMatch(rule -> rule.Content.loaded);
+ }
+
+ public Integer getItemAmount(M material, M item) {
+ return Limited.stream()
+ .filter(rule -> rule.Materials.contains(material))
+ .filter(rule -> rule.Content.Items.contains(item))
+ .mapToInt(rule -> rule.Content.Amount)
+ .max()
+ .orElse(0);
+ }
+
+ @ToString
+ public static final class BlockRule {
+
+ /**
+ * The block materials this rule applies to
+ */
+ public final Set Materials;
+
+ /**
+ * Maximal amount of Blocks allowed in the schematic
+ *
+ * @implSpec {@code 0} by default
+ */
+ public final int Amount;
+
+ /**
+ * The item content filter for Blocks
+ *
+ * @implSpec no Items with {@code Amount 0} (denies any item) by default
+ */
+ public final Content Content;
+
+ /**
+ * Resolves one {@code Materials} entry to the concrete materials it denotes: {@code storage}
+ * for {@link SQLWrapper#getStorageMaterials()}, {@code inventory} for
+ * {@link SQLWrapper#getInventoryMaterials()}, or the material itself otherwise.
+ */
+ private Set expandInventoryGroup(YMLWrapper loader, String name) {
+ switch (name.toUpperCase()) {
+ case "STORAGE":
+ return (Set) SQLWrapper.impl.getStorageMaterials();
+ case "INVENTORY":
+ return (Set) SQLWrapper.impl.getInventoryMaterials();
+ default:
+ M material = loader.materialMapper.apply(name.toUpperCase());
+ return material != null ? Collections.singleton(material) : Collections.emptySet();
+ }
+ }
+
+ private BlockRule(YMLWrapper loader) {
+ Set blocks = loader.getStringList("Materials")
+ .stream()
+ .flatMap(value -> expandInventoryGroup(loader, value).stream())
+ .collect(Collectors.toSet());
+ Materials = Collections.unmodifiableSet(blocks);
+ Amount = loader.getInt("Amount", 0);
+ Content = new Content<>(loader.with("Content"));
+ }
+
+ private BlockRule(M block, int amount) {
+ Materials = Collections.singleton(block);
+ Amount = amount;
+ Content = new Content<>();
+ }
+
+ @ToString
+ public static final class Content {
+
+ public final boolean loaded;
+
+ /**
+ * Items allowed by this rule
+ */
+ public final Set Items;
+
+ /**
+ * Maximal amount of an allowed item per single block instance
+ *
+ * @implSpec {@code 0} by default
+ */
+ public final int Amount;
+
+ private Content(YMLWrapper loader) {
+ loaded = loader.canLoad();
+ List items = loader.getMaterialList("Items");
+ Items = Collections.unmodifiableSet(new HashSet<>(items));
+ Amount = loader.getInt("Amount", 0);
+ }
+
+ private Content() {
+ loaded = false;
+ Items = new HashSet<>();
+ Amount = 0;
+ }
+ }
+ }
+
@ToString
public static final class SizeConfig {
diff --git a/CommonCore/SQL/src/de/steamwar/sql/Punishment.kt b/CommonCore/SQL/src/de/steamwar/sql/Punishment.kt
index 8f5f3ed4e..3635ea251 100644
--- a/CommonCore/SQL/src/de/steamwar/sql/Punishment.kt
+++ b/CommonCore/SQL/src/de/steamwar/sql/Punishment.kt
@@ -183,14 +183,6 @@ class Punishment(id: EntityID) : IntEntity(id) {
"UNNOFIGHTSERVER",
UserPerm.MODERATION
),
- NoTeamServer(
- "NOTEAMSERVER_TEAM",
- "NOTEAMSERVER_PERMA",
- "NOTEAMSERVER_UNTIL",
- "UNNOTEAMSERVER_ERROR",
- "UNNOTEAMSERVER",
- UserPerm.MODERATION
- ),
Note("NOTE_TEAM", null, null, null, null, UserPerm.PUNISHMENTS, true);
fun isMulti() =
diff --git a/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java b/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java
index f049f6808..565687fe7 100644
--- a/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java
+++ b/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java
@@ -24,6 +24,7 @@ import de.steamwar.ImplementationProvider;
import java.io.File;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
public interface SQLWrapper {
SQLWrapper> impl = ImplementationProvider.getImpl("de.steamwar.sql.SQLWrapperImpl");
@@ -36,5 +37,21 @@ public interface SQLWrapper {
return Collections.emptyList();
}
+ /**
+ * Storage-type inventory materials (chests, barrels, shulker boxes of any color) - the
+ * {@code storage} group usable in {@code GameModeConfig}'s {@code Limited}.
+ */
+ default Set getStorageMaterials() {
+ return Collections.emptySet();
+ }
+
+ /**
+ * Every inventory material - the {@code all} group usable in {@code GameModeConfig}'s
+ * {@code Limited}. Expected to be a superset of {@link #getStorageMaterials()}.
+ */
+ default Set getInventoryMaterials() {
+ return Collections.emptySet();
+ }
+
void additionalExceptionMetadata(StringBuilder builder);
}
diff --git a/CommonCore/SQL/src/de/steamwar/sql/YMLWrapper.java b/CommonCore/SQL/src/de/steamwar/sql/YMLWrapper.java
index 567524ff2..87df1352f 100644
--- a/CommonCore/SQL/src/de/steamwar/sql/YMLWrapper.java
+++ b/CommonCore/SQL/src/de/steamwar/sql/YMLWrapper.java
@@ -76,6 +76,25 @@ final class YMLWrapper {
return new YMLWrapper<>(false, Collections.emptyMap(), materialMapper, winconditionMapper);
}
+ public List> withAsList(String path) {
+ if (document.containsKey(path)) {
+ Object value = document.get(path);
+ if (value instanceof List) {
+ List> list = (List>) value;
+ List