Compare commits

..
Author SHA1 Message Date
Chaoscaot a8204a181b Add API V2
Signed-off-by: Chaoscaot <max@maxsp.de>
2026-04-17 18:25:42 +02:00
33 changed files with 981 additions and 479 deletions
@@ -1,63 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.dev;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.command.SWCommand;
import de.steamwar.linkage.Linked;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
@Linked
public class CreateKitCommand extends SWCommand {
public CreateKitCommand() {
super("createkit");
if (!BauSystem.DEV_SERVER) unregister();
}
@Register
public void onCommand(Player player, String name) {
YamlConfiguration yaml = new YamlConfiguration();
yaml.set("Items", player.getInventory().getContents());
yaml.set("Armor", player.getInventory().getArmorContents());
yaml.set("Effects", player.getActivePotionEffects());
yaml.set("LeaderAllowed", true);
yaml.set("MemberAllowed", true);
yaml.set("EnterStage", 0);
yaml.set("TNT", true);
YamlConfiguration kits = new YamlConfiguration();
kits.set("Kits." + name, yaml);
try {
kits.save(new File("new.kits.yaml"));
player.sendMessage("Kit created!");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -135,7 +135,7 @@ public abstract class ViewFlag {
}
Location secoundLocation;
if (Math.abs(previousVelocity.getX()) >= Math.abs(previousVelocity.getZ())) {
if (previousVelocity.getX() >= previousVelocity.getZ()) {
secoundLocation = previous.getLocation().clone().add(delta.getX(), delta.getY(), 0);
} else {
secoundLocation = previous.getLocation().clone().add(0, delta.getY(), delta.getZ());
@@ -52,10 +52,6 @@ public final class GameModeConfig<M, W> {
private static final Map<String, GameModeConfig<?, String>> byGameName;
private static final Map<SchematicType, GameModeConfig<?, String>> bySchematicType;
public static <M> Collection<GameModeConfig<M, String>> getAll() {
return (Collection) byFileName.values();
}
public static <M> GameModeConfig<M, String> getByFileName(File file) {
return (GameModeConfig<M, String>) byFileName.get(file.getName());
}
@@ -91,24 +87,8 @@ public final class GameModeConfig<M, W> {
byFileName = new HashMap<>();
byGameName = new HashMap<>();
bySchematicType = new HashMap<>();
SchematicType.values();
DEFAULTS = SQLWrapper.impl.loadGameModeConfig(null);
init();
}
public static void init() {
byFileName.clear();
byGameName.clear();
bySchematicType.clear();
File folder = SQLWrapper.impl.getSchemTypesFolder();
if (!folder.exists()) return;
if (!folder.isDirectory()) return;
for (File file : Objects.requireNonNull(folder.listFiles())) {
if (!file.getName().endsWith(".yml")) continue;
if (file.getName().endsWith(".kits.yml")) continue;
SQLWrapper.impl.loadGameModeConfig(file);
}
byFileName.values().forEach(gameModeConfig -> {
List<SchematicType> subTypes = Collections.unmodifiableList(gameModeConfig.Schematic.SubTypesStrings.stream()
@@ -687,16 +667,13 @@ public final class GameModeConfig<M, W> {
*/
public final Map<Set<M>, Integer> Limited;
/** */
public final List<AllowedItemsConfig<M>> AllowedItems;
private SchematicConfig(YMLWrapper<M, ?> loader) {
loaded = loader.canLoad();
Size = new SizeConfig(loader.with("Size"));
Inset = new InsetConfig(loader.with("Inset"));
Type = null;
Type = loader.getSchematicType("Type", "Normal");
SubTypesStrings = loader.getStringList("SubTypes");
SubTypes = new ArrayList<>();
SubTypes = loader.getSchematicTypeList("SubTypes");
Shortcut = loader.getString("Shortcut", "");
Material = loader.getMaterial("Material", "STONE_BUTTON");
ManualCheck = loader.getBoolean("ManualCheck", true);
@@ -709,7 +686,6 @@ public final class GameModeConfig<M, W> {
MaxDispenserItems = loader.getInt("MaxDispenserItems", 128);
MaxBlastResistance = loader.getDouble("MaxBlastResistance", Double.MAX_VALUE);
MaxDesignBlastResistance = loader.getDouble("MaxDesignBlastResistance", MaxBlastResistance);
AllowedItems = loader.withEvery("AllowedItems").stream().map(AllowedItemsConfig::new).collect(Collectors.toList());
Map<Set<M>, Integer> Limited = new HashMap<>();
for (Map<?, ?> entry : loader.getMapList("Limited")) {
@@ -794,32 +770,6 @@ public final class GameModeConfig<M, W> {
bottom = loader.getInt("bottom", 0);
}
}
public static final class AllowedItemsConfig<M> {
/**
* The materials of the items that may be included in inventories
*/
public final List<M> AllowedMaterials;
/**
* The materials of inventory blocks that may contain the listed items
*/
public final List<M> AllowedIn;
/**
* The maximum amount of items of the listed items contained per inventory
*/
public final Integer MaxPerInventory;
/**
* The maximum amount of items of the listed items contained in total within all inventories
*/
public final Integer TotalMax;
private AllowedItemsConfig(YMLWrapper<M, ?> loader) {
AllowedMaterials = loader.getMaterialList("AllowedMaterials");
AllowedIn = loader.getMaterialList("AllowedIn");
MaxPerInventory = loader.getInt("MaxPerInventory", Integer.MAX_VALUE);
TotalMax = loader.getInt("TotalMax", Integer.MAX_VALUE);
}
}
}
@ToString
@@ -36,5 +36,8 @@ public interface SQLWrapper<M> {
return Collections.emptyList();
}
default void processSchematicType(GameModeConfig<?, String> gameModeConfig) {
}
void additionalExceptionMetadata(StringBuilder builder);
}
@@ -19,7 +19,11 @@
package de.steamwar.sql
import java.io.File
import java.util.*
import java.util.Locale
import java.util.Locale.getDefault
import java.util.stream.Collectors
data class SchematicType(
val name: String,
@@ -43,65 +47,58 @@ data class SchematicType(
@JvmField
val Normal = SchematicType("Normal", "", Type.NORMAL, null, "STONE_BUTTON", false)
private val types: MutableList<SchematicType> = mutableListOf()
private val fromDB: MutableMap<String, SchematicType> = mutableMapOf()
private val types: List<SchematicType>
private val fromDB: Map<String, SchematicType>?
init {
GameModeConfig.init()
init()
}
val tmpTypes = mutableListOf<SchematicType>()
val tmpFromDB = mutableMapOf<String, SchematicType>()
@JvmStatic
fun init() {
types.clear()
fromDB.clear()
tmpTypes.add(Normal)
tmpFromDB[Normal.toDB()] = Normal
types.add(Normal)
fromDB[Normal.toDB()] = Normal
for (gameModeConfig in GameModeConfig.getAll<Any>()) {
val type = gameModeConfig.Schematic.Type
?: continue
if (fromDB.containsKey(type.toDB())) continue
types.add(type)
fromDB[type.toDB()] = type
if (gameModeConfig.CheckQuestions.isNotEmpty() && type.checkType != null) {
types.add(type.checkType)
fromDB[type.checkType.toDB()] = type.checkType
val folder = SQLWrapper.impl.schemTypesFolder
if (folder.exists()) {
for (configFile in Arrays.stream<File?>(folder.listFiles { _, name ->
name.endsWith(
".yml"
) && !name.endsWith(".kits.yml")
}).sorted().collect(Collectors.toList())) {
val gameModeConfig = SQLWrapper.impl.loadGameModeConfig(configFile)
if (gameModeConfig.Schematic.Type == null) continue
if (tmpFromDB.containsKey(gameModeConfig.Schematic.Type.toDB())) continue
val current = gameModeConfig.Schematic.Type
if (gameModeConfig.CheckQuestions.isNotEmpty()) {
val checkType = current.checkType
tmpTypes.add(checkType!!)
tmpFromDB[checkType.toDB()] = checkType
}
tmpTypes.add(current)
tmpFromDB[current.toDB()] = current
SQLWrapper.impl.processSchematicType(gameModeConfig)
}
}
types = tmpTypes.toList()
fromDB = tmpFromDB.toMap()
}
@JvmStatic
fun values() =
types
fun values() = types
@JvmStatic
fun fromDB(value: String) =
fromDB[value.lowercase()]
fun fromDB(value: String) = fromDB?.let { it[value.lowercase()] }
}
fun name() =
name
fun name() = name
fun toDB() = name.lowercase()
fun toDB() =
name.lowercase()
fun check() = type == Type.CHECK_TYPE
fun fightType() = type == Type.FIGHT_TYPE
fun writeable() = type == Type.NORMAL
fun check() =
type == Type.CHECK_TYPE
fun fightType() =
type == Type.FIGHT_TYPE
fun writeable() =
type == Type.NORMAL
fun checkType() =
if (manualCheck) checkType else this
fun isAssignable() =
type == Type.NORMAL || (type == Type.FIGHT_TYPE && checkType != null) || !manualCheck
fun checkType() = if (manualCheck) checkType else this
fun isAssignable() = type == Type.NORMAL || (type == Type.FIGHT_TYPE && checkType != null) || !manualCheck
enum class Type {
NORMAL,
@@ -198,7 +198,7 @@ class SteamwarUser(id: EntityID<Int>): IntEntity(id) {
var discordId by SteamwarUserTable.discordId
private val punishments by lazy { Punishment.getPunishmentsOfPlayer(id.value) }
val punishments by lazy { Punishment.getPunishmentsOfPlayer(id.value) }
private val perms by lazy { UserPerm.getPerms(id.value) }
private val prefix by lazy { perms.firstOrNull { UserPerm.prefixes.containsKey(it) }?.let { UserPerm.prefixes[it]} ?: UserPerm.emptyPrefix }
@@ -66,6 +66,7 @@ class Team(id: EntityID<Int>) : IntEntity(id) {
var deleted by TeamTable.deleted
private set
val members by lazy { useDb { SteamwarUserTable.select(SteamwarUserTable.id).where { SteamwarUserTable.team eq teamId }.map { it[SteamwarUserTable.id].value } } }
val membersUser by lazy { useDb { SteamwarUser.find { SteamwarUserTable.team eq teamId }.toList() } }
fun size() = useDb { SteamwarUser.find { SteamwarUserTable.team eq teamId }.count().toInt() }
fun disband(user: SteamwarUser) = useDb {
@@ -79,7 +79,7 @@ class TeamTeilnahme(id: EntityID<CompositeID>) : CompositeEntity(id) {
@JvmStatic
fun getEvents(teamId: Int) = useDb {
find { TeamTeilnahmeTable.teamId eq teamId }.map { Event.byId(it.eventId.value) }.toSet()
find { TeamTeilnahmeTable.teamId eq teamId }.mapNotNull { Event.byId(it.eventId.value) }.toSet()
}
@JvmStatic
@@ -76,20 +76,6 @@ final class YMLWrapper<M, W> {
return new YMLWrapper<>(false, Collections.emptyMap(), materialMapper, winconditionMapper);
}
public List<YMLWrapper<M, W>> withEvery(String path) {
if (document.containsKey(path)) {
Object value = document.get(path);
if(value instanceof List<?>) {
List<Object> list = (List<Object>) value;
return list.stream().map(o -> new YMLWrapper<>(true, Collections.singletonMap("value", o), materialMapper, winconditionMapper)).collect(Collectors.toList());
}
}
return Collections.emptyList();
}
public <T> T get(String path, T defaultValue, Function<Object, T> mapper) {
Object value = this.document.get(path);
if (value == null) return defaultValue;
@@ -42,5 +42,4 @@ dependencies {
compileOnly(libs.fastutil)
compileOnly(libs.authlib)
compileOnly(project(":FightSystem:FightSystem_14"))
}
@@ -1,51 +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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.listener;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependentTask;
import net.minecraft.world.entity.projectile.windcharge.WindCharge;
import org.bukkit.Location;
public class WindchargeStopper21 implements WindchargeStopper.IWindchargeStopper {
public WindchargeStopper21() {
new StateDependentTask(true, FightState.Running, this::run, 1, 1);
}
private static final int middleLine = Config.SpecSpawn.getBlockZ();
private static final Class<?> windChargeClass = WindCharge.class;
private void run() {
Recording.iterateOverEntities(windChargeClass::isInstance, entity -> {
Location location = entity.getLocation();
Location prevLocation = location.clone().subtract(entity.getVelocity());
boolean passedMiddle = location.getBlockZ() > middleLine && prevLocation.getBlockZ() > middleLine ||
location.getBlockZ() < middleLine && prevLocation.getBlockZ() < middleLine;
if(!passedMiddle) {
entity.remove();
}
});
}
}
@@ -1,33 +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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.utils;
import org.bukkit.inventory.ItemStack;
public class FlatteningWrapper21 extends FlatteningWrapper14 {
@Override
public boolean hasAttributeModifier(ItemStack stack) {
if (!stack.getDataTypes().isEmpty()) {
return true;
}
return super.hasAttributeModifier(stack);
}
}
@@ -1,23 +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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.listener;
public class WindchargeStopper8 implements WindchargeStopper.IWindchargeStopper {
}
@@ -1,35 +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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.listener;
import de.steamwar.core.VersionDependent;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.linkage.Linked;
@Linked
public class WindchargeStopper {
static {
VersionDependent.getVersionImpl(FightSystem.getPlugin());
}
public interface IWindchargeStopper {
}
}
@@ -25,7 +25,6 @@ import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.world.block.BaseBlock;
import de.steamwar.core.Core;
import de.steamwar.schematicsystem.autocheck.AutoChecker.InventoryScanResult;
import de.steamwar.sql.GameModeConfig;
import de.steamwar.sql.SchematicType;
import org.bukkit.Material;
@@ -51,8 +50,7 @@ public class AutoChecker15 implements AutoChecker.IAutoChecker {
result.getBlockCounts().merge(material, 1, Integer::sum);
if(AutoCheckerItems.impl.getInventoryMaterials().contains(material)) {
InventoryScanResult inventoryResult = checkInventory(result, block, material, new BlockPos(x, y, z));
result.getInventoryScans().put(new BlockPos(x, y, z), inventoryResult);
checkInventory(result, block, material, new BlockPos(x, y, z));
}
if(x == min.getBlockX() || x == max.getBlockX() || y == max.getBlockY() || z == min.getBlockZ() || z == max.getBlockZ()) {
@@ -64,24 +62,41 @@ public class AutoChecker15 implements AutoChecker.IAutoChecker {
return result;
}
private InventoryScanResult checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos) {
private static final Map<Material, Set<Material>> itemsInInv = new EnumMap<>(Material.class);
static {
itemsInInv.put(Material.BUCKET, EnumSet.of(Material.DISPENSER));
itemsInInv.put(Material.TNT, EnumSet.of(
Material.CHEST, Material.BARREL, Material.SHULKER_BOX, Material.BLACK_SHULKER_BOX,
Material.BLUE_SHULKER_BOX, Material.BROWN_SHULKER_BOX, Material.CYAN_SHULKER_BOX,
Material.GRAY_SHULKER_BOX, Material.GREEN_SHULKER_BOX, Material.LIGHT_BLUE_SHULKER_BOX,
Material.LIGHT_GRAY_SHULKER_BOX, Material.LIME_SHULKER_BOX, Material.MAGENTA_SHULKER_BOX,
Material.ORANGE_SHULKER_BOX, Material.PINK_SHULKER_BOX, Material.PURPLE_SHULKER_BOX,
Material.RED_SHULKER_BOX, Material.WHITE_SHULKER_BOX, Material.YELLOW_SHULKER_BOX
));
itemsInInv.put(Material.FIRE_CHARGE, EnumSet.of(Material.DISPENSER));
itemsInInv.put(Material.ARROW, EnumSet.of(Material.DISPENSER));
AutoCheckerItems.impl.getAllowedMaterialsInInventory().forEach(material -> itemsInInv.put(material, AutoCheckerItems.impl.getInventoryMaterials()));
}
private void checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos) {
CompoundTag nbt = block.getNbtData();
if(nbt == null) {
result.getDefunctNbt().add(pos);
return null;
return;
}
if(material == Material.JUKEBOX && nbt.getValue().containsKey("RecordItem")){
result.getRecords().add(pos);
return null;
return;
}
List<CompoundTag> items = nbt.getList("Items", CompoundTag.class);
if(items.isEmpty())
return null; //Leeres Inventar
return; //Leeres Inventar
Map<Material, Integer> inventoryItemCounts = new HashMap<>();
int counter = 0;
for(CompoundTag item : items){
if(!item.containsKey("id")){
result.getDefunctNbt().add(pos);
@@ -92,18 +107,16 @@ public class AutoChecker15 implements AutoChecker.IAutoChecker {
if(itemType == null) //Leere Slots
continue;
if (!itemsInInv.getOrDefault(itemType, EnumSet.noneOf(Material.class)).contains(material)) {
result.getForbiddenItems().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType);
} else if(material == Material.DISPENSER && (itemType == Material.ARROW || itemType == Material.FIRE_CHARGE)) {
counter += Core.getVersion() >= 21 ? item.getInt("count") : item.getByte("Count");
}
if (item.containsKey("tag")) {
result.getForbiddenNbt().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType);
}
else {
int count = Core.getVersion() >= 21 ? item.getInt("count") : item.getByte("Count");
inventoryItemCounts.merge(itemType, count, Integer::sum);
}
}
return new InventoryScanResult(material, inventoryItemCounts);
result.getDispenserItems().put(pos, counter);
}
@Override
@@ -24,13 +24,11 @@ import de.steamwar.core.VersionDependent;
import de.steamwar.sql.GameModeConfig;
import de.steamwar.schematicsystem.SchematicSystem;
import de.steamwar.sql.SchematicType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.bukkit.Material;
import java.util.*;
import java.util.stream.Collectors;
public class AutoChecker {
@@ -49,14 +47,6 @@ public class AutoChecker {
AutoCheckerResult sizeCheck(Clipboard clipboard, GameModeConfig<Material, String> type);
}
@AllArgsConstructor
@Getter
@ToString
public static class InventoryScanResult {
private final Material inventoryMaterial;
private final Map<Material, Integer> itemCounts;
}
@Getter
@ToString
public static class BlockScanResult {
@@ -64,13 +54,8 @@ public class AutoChecker {
private final List<BlockPos> defunctNbt = new ArrayList<>();
private final List<BlockPos> records = new ArrayList<>();
private final Map<Material, List<BlockPos>> designBlocks = new EnumMap<>(Material.class);
private final Map<BlockPos, InventoryScanResult> inventoryScans = new HashMap<>();
private final Map<BlockPos, Integer> dispenserItems = new HashMap<>();
private final Map<BlockPos, Set<Material>> forbiddenItems = new HashMap<>();
private final Map<BlockPos, Set<Material>> forbiddenNbt = new HashMap<>();
public Map<Material, Integer> getItemCounts() {
return inventoryScans.values().stream()
.flatMap(i -> i.getItemCounts().entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::sum));
}
}
}
@@ -62,12 +62,10 @@ public class AutoCheckerResult {
!type.isAfterDeadline();
}
public boolean doInventoriesOnlyContainAllowedItems() {
public boolean isDispenserItemsOK() {
return blockScanResult.getDispenserItems().values().stream().allMatch(i -> i <= type.Schematic.MaxDispenserItems);
}
public boolean hasWarnings() {
return blockScanResult.getDefunctNbt().isEmpty();
}
@@ -22,6 +22,7 @@ package de.steamwar.sql;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import de.steamwar.velocitycore.VelocityCore;
import de.steamwar.velocitycore.commands.CheckCommand;
import java.io.File;
@@ -37,6 +38,15 @@ public class SQLWrapperImpl implements SQLWrapper<String> {
return new GameModeConfig<>(file, GameModeConfig.ToString, GameModeConfig.ToString, GameModeConfig.ToInternalName, true);
}
@Override
public void processSchematicType(GameModeConfig<?, String> gameModeConfig) {
SchematicType type = gameModeConfig.Schematic.Type;
if (type.checkType() != null) {
CheckCommand.setCheckQuestions(type.checkType(), gameModeConfig.CheckQuestions);
CheckCommand.addFightType(type.checkType(), type);
}
}
@Override
public void additionalExceptionMetadata(StringBuilder builder) {
builder.append("\nServers: ");
@@ -25,10 +25,7 @@ import lombok.Getter;
import lombok.experimental.UtilityClass;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.*;
@UtilityClass
public class ArenaMode {
@@ -53,12 +50,12 @@ public class ArenaMode {
if(!folder.exists())
return;
GameModeConfig.init();
SchematicType.init();
for (GameModeConfig<String, String> gameModeConfig : GameModeConfig.<String>getAll()) {
for(File file : Arrays.stream(folder.listFiles((file, name) -> name.endsWith(".yml") && !name.endsWith(".kits.yml") && !name.equals("config.yml"))).sorted().toList()) {
GameModeConfig<String, String> gameModeConfig = new GameModeConfig<>(file, GameModeConfig.ToString, GameModeConfig.ToString, GameModeConfig.ToInternalName, false);
if (!gameModeConfig.Server.loaded) continue;
allModes.add(gameModeConfig);
byInternal.put(gameModeConfig.configFile.getName().replace(".yml", ""), gameModeConfig);
byInternal.put(file.getName().replace(".yml", ""), gameModeConfig);
for (String name : gameModeConfig.Server.ChatNames) {
byChat.put(name.toLowerCase(), gameModeConfig);
}
@@ -40,7 +40,6 @@ import net.kyori.adventure.text.format.NamedTextColor;
import java.awt.*;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -49,9 +48,20 @@ import java.util.logging.Level;
@Linked
public class CheckCommand extends SWCommand {
private static final Map<SchematicType, SchematicType> fightTypes = new HashMap<>();
private static final Map<SchematicType, List<String>> checkQuestions = new HashMap<>();
private static final Map<UUID, CheckSession> currentCheckers = new HashMap<>();
private static final Map<Integer, CheckSession> currentSchems = new HashMap<>();
public static void setCheckQuestions(SchematicType checkType, List<String> checkQuestions) {
CheckCommand.checkQuestions.put(checkType, checkQuestions);
}
public static void addFightType(SchematicType checkType, SchematicType fightType) {
fightTypes.put(checkType, fightType);
}
public static boolean isChecking(Player player){
return currentCheckers.containsKey(player.getUniqueId());
}
@@ -137,7 +147,7 @@ public class CheckCommand extends SWCommand {
if(!schem.getSchemtype().check()){
VelocityCore.getLogger().log(Level.SEVERE, () -> sender.user().getUserName() + " tried to check an uncheckable schematic!");
return;
}else if(schem.getOwner() == sender.user().getId() && !sender.user().hasPerm(UserPerm.ADMINISTRATION)) {
}else if(schem.getOwner() == sender.user().getId()) {
sender.system("CHECK_SCHEMATIC_OWN");
return;
}
@@ -238,9 +248,9 @@ public class CheckCommand extends SWCommand {
this.checker = checker;
this.schematic = schematic;
this.startTime = Timestamp.from(Instant.now());
this.checkList = GameModeConfig.getBySchematicType(schematic.getSchemtype()).CheckQuestions.listIterator();
this.checkList = checkQuestions.get(schematic.getSchemtype()).listIterator();
GameModeConfig<String, String> mode = GameModeConfig.getBySchematicType(schematic.getSchemtype());
GameModeConfig<String, String> mode = ArenaMode.getBySchemType(fightTypes.get(schematic.getSchemtype()));
new ServerStarter().test(mode, mode.getRandomMap(), checker.getPlayer()).check(schematic.getId()).callback(subserver -> {
currentCheckers.put(checker.user().getUUID(), this);
currentSchems.put(schematic.getId(), this);
@@ -294,50 +304,7 @@ public class CheckCommand extends SWCommand {
}
private void accept(){
// TODO: This Code is only for the WGS and because YoyoNow is not available this can be removed or changed after the WGS!
if (schematic.getSchemtype().toDB().equals("cwargearseason26")) {
int userId = schematic.getOwner();
SteamwarUser user = SteamwarUser.byId(userId);
int teamId = user.getTeam();
SchematicNode teamFolder = SchematicNode.getSchematicNodeInNode(172325)
.stream()
.filter(schematicNode -> schematicNode.getName().startsWith(teamId + "_"))
.findFirst()
.orElse(null);
if (teamFolder == null) {
internalAccept();
return;
}
// Copy Schem into team folder of -1 user
String name = DateTimeFormatter.ofPattern("yyyy.MM.dd_HH:mm:ss").format(schematic.getLastUpdate().toLocalDateTime());
NodeData data = NodeData.getLatest(schematic);
SchematicNode node = SchematicNode.createSchematic(-1, name, teamFolder.getNodeId());
NodeData.saveFromStream(node, data.schemData(false), data.getNodeFormat());
// Accept the team folder schematic and set other to Normal
node.setSchemtype(GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type);
// Conclude by setting send in schematic to normal and broadcast
concludeCheckSession("freigegeben", SchematicType.Normal, () -> {
Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID());
owner.withPlayerOrOffline(
player -> owner.system("CHECK_ACCEPTED", schematic.getSchemtype().name(), schematic.getName()),
() -> DiscordAlert.send(owner, Color.GREEN, new Message("DC_TITLE_SCHEMINFO"), new Message("DC_SCHEM_ACCEPT", schematic.getName()), true)
);
notifyTeam(new Message("CHECK_ACCEPTED_TEAM", schematic.getName(), owner.user().getUserName()));
return owner.getPlayer() != null;
});
return;
}
internalAccept();
}
private void internalAccept() {
concludeCheckSession("freigegeben", GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type, () -> {
concludeCheckSession("freigegeben", fightTypes.get(schematic.getSchemtype()), () -> {
Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID());
owner.withPlayerOrOffline(
player -> owner.system("CHECK_ACCEPTED", schematic.getSchemtype().name(), schematic.getName()),
@@ -158,6 +158,7 @@ public class Tablist extends ChannelInboundHandlerAdapter {
public void disable() {
sendTabPacket(new ArrayList<>(directTabItems.values()), null);
directTabItems.clear();
sendTabPacket(current, null);
current.clear();
@@ -55,7 +55,7 @@ public class BauLock {
break;
case SUPERVISOR:
BauweltMember member = BauweltMember.getBauMember(owner.getId(), target.getId());
locked = member == null || !member.isSupervisor();
locked = !member.isSupervisor();
break;
case SERVERTEAM:
locked = !target.hasPerm(UserPerm.TEAM);
+51 -45
View File
@@ -47,61 +47,67 @@ data class UsernamePassword(val name: String, val password: String, val keepLogg
fun Route.configureAuth() {
route("/auth") {
val client = HttpClient(Java) {
install(ContentNegotiation) {
json()
}
}
post {
val request = call.receive<UsernamePassword>()
SteamwarUser.clear()
val user = SteamwarUser.get(request.name)
val valid = user?.verifyPassword(request.password) ?: false
if (!valid) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid"))
return@post
}
call.sessions.set(SWUserSession(user.getId()))
call.respond(ResponseUser(user))
}
delete {
call.sessions.clear<SWUserSession>()
call.respond(HttpStatusCode.NoContent)
}
route("/discord") {
post {
val token = call.receiveText()
configureDiscordAuth()
configurePasswordAuth()
}
}
val res = client.get("https://discord.com/api/v10/oauth2/@me") {
headers {
append("Authorization", "Bearer $token")
}
}
val resJson = Json.parseToJsonElement(res.bodyAsText()).jsonObject
val discordId = resJson["user"]?.jsonObject["id"]?.jsonPrimitive?.content
fun Route.configurePasswordAuth() {
post {
val request = call.receive<UsernamePassword>()
if (discordId == null) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid Discord token", "invalid"))
return@post
}
SteamwarUser.clear()
val user = SteamwarUser.get(request.name)
val valid = user?.verifyPassword(request.password) ?: false
SteamwarUser.clear()
val user = SteamwarUser.get(discordId.toLong())
if (!valid) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid username or password", "invalid"))
return@post
}
if (user == null) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Discord account not linked", "not_linked"))
return@post
}
call.sessions.set(SWUserSession(user.getId()))
call.respond(ResponseUser(user))
}
}
call.sessions.set(SWUserSession(user.getId()))
call.respond(ResponseUser(user))
}
fun Route.configureDiscordAuth() {
val client = HttpClient(Java) {
install(ContentNegotiation) {
json()
}
}
}
post("/discord") {
val token = call.receiveText()
val res = client.get("https://discord.com/api/v10/oauth2/@me") {
headers {
append("Authorization", "Bearer $token")
}
}
val resJson = Json.parseToJsonElement(res.bodyAsText()).jsonObject
val discordId = resJson["user"]?.jsonObject["id"]?.jsonPrimitive?.content
if (discordId == null) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Invalid Discord token", "invalid"))
return@post
}
SteamwarUser.clear()
val user = SteamwarUser.get(discordId.toLong())
if (user == null) {
call.respond(HttpStatusCode.Forbidden, ResponseError("Discord account not linked", "not_linked"))
return@post
}
call.sessions.set(SWUserSession(user.getId()))
call.respond(ResponseUser(user))
}
}
@@ -75,7 +75,7 @@ data class ResponseUser(
@Serializable
data class ResponseUserList(val entries: List<ResponseUser>, val rows: Long)
private fun Query.addUserFilter(
fun Query.addUserFilter(
name: String? = null,
uuid: UUID? = null,
team: Set<Int>? = null,
@@ -27,6 +27,7 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.v1.core.ResultRow
import java.sql.Timestamp
import java.time.Instant
@@ -60,6 +61,8 @@ data class ResponseEventFight(
@Serializable
data class ResponseTeam(val id: Int, val name: String, val kuerzel: String, val color: String) {
constructor(team: Team) : this(team.teamId, team.teamName, team.teamKuerzel, team.teamColor)
constructor(row: ResultRow) : this(row[TeamTable.id].value, row[TeamTable.name], row[TeamTable.kuerzel], row[TeamTable.color])
}
@Serializable
@@ -19,6 +19,12 @@
package de.steamwar.routes
import de.steamwar.routes.v2.configureAuthV2
import de.steamwar.routes.v2.configureGameModeRoutes
import de.steamwar.routes.v2.configureSchematicsV2Route
import de.steamwar.routes.v2.configureSteamWarRoute
import de.steamwar.routes.v2.configureTeamRoutes
import de.steamwar.routes.v2.configureUsersRouteV2
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.routing.*
@@ -34,6 +40,17 @@ fun Application.configureRoutes() {
configureSchematic()
configureAuth()
configureAuditLog()
route("/v2") {
configureAuditLog()
configurePage()
configureEventsRoute()
configureAuthV2()
configureTeamRoutes()
configureSteamWarRoute()
configureUsersRouteV2()
configureSchematicsV2Route()
configureGameModeRoutes()
}
}
}
}
@@ -43,8 +43,31 @@ import java.util.*
import java.util.zip.GZIPInputStream
@Serializable
data class ResponseSchematic(val name: String, val id: Int, val type: String?, val owner: Int, val item: String, val lastUpdate: Long, val rank: Int, val replaceColor: Boolean, val allowReplay: Boolean) {
constructor(node: SchematicNode) : this(node.name, node.getId(), node.schemtype?.name(), node.owner, node.item, node.lastUpdate.time, node.rank, node.replaceColor(), node.allowReplay())
data class ResponseSchematic(
val name: String,
val id: Int,
val type: String?,
val owner: Int,
val item: String,
val lastUpdate: Long,
val replaceColor: Boolean,
val allowReplay: Boolean,
val members: List<ResponseUser>
) {
constructor(node: SchematicNode) : this(
node.name,
node.getId(),
node.schemtype.name(),
node.owner,
node.item,
node.lastUpdate.time,
node.replaceColor(),
node.allowReplay(),
node.members.map {
ResponseUser(
SteamwarUser.byId(it.member)!!
)
})
}
@Serializable
@@ -72,9 +95,12 @@ fun Route.configureSchematic() {
val node = call.receiveSchematic() ?: return@get
val user = call.principal<SWAuthPrincipal>()?.user
if(user != null && !node.accessibleByUser(user)) {
if (user != null && !node.accessibleByUser(user)) {
call.respond(HttpStatusCode.Forbidden)
SWException.log("User ${user.userName} tried to download schematic ${node.name} without permission", user.id.toString())
SWException.log(
"User ${user.userName} tried to download schematic ${node.name} without permission",
user.id.toString()
)
return@get
}
@@ -83,8 +109,15 @@ fun Route.configureSchematic() {
return@get
}
call.response.header("Content-Disposition", "attachment; filename=\"${node.name}${data.nodeFormat.fileEnding}\"")
call.respondBytes(data.schemData(false).readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK)
call.response.header(
"Content-Disposition",
"attachment; filename=\"${node.name}${data.nodeFormat.fileEnding}\""
)
call.respondBytes(
data.schemData(false).readAllBytes(),
contentType = ContentType.Application.OctetStream,
status = HttpStatusCode.OK
)
}
get("/info") {
val node = call.receiveSchematic() ?: return@get
@@ -100,18 +133,22 @@ fun Route.configureSchematic() {
val schemName = file.name.substringBeforeLast(".")
if (SchematicNode.invalidSchemName(arrayOf(schemName))) {
call.respond(HttpStatusCode.BadRequest, ResponseError(
error = "INVALID_NAME"
))
call.respond(
HttpStatusCode.BadRequest, ResponseError(
error = "INVALID_NAME"
)
)
return@post
}
val schemType = file.name.substringAfterLast(".")
if (schemType != "schem" && schemType != "schematic") {
call.respond(HttpStatusCode.BadRequest, ResponseError(
error = "INVALID_SUFFIX"
))
call.respond(
HttpStatusCode.BadRequest, ResponseError(
error = "INVALID_SUFFIX"
)
)
return@post
}
@@ -146,22 +183,27 @@ fun Route.configureSchematic() {
.value
if (fawe.equals("2.12.3-SNAPSHOT")) {
SWException.log("Schematic with Bugged Version Uploaded", """
SWException.log(
"Schematic with Bugged Version Uploaded", """
Schematic=$schemName
User=${user.userName}
Id=${user.id}
""".trimIndent())
""".trimIndent()
)
}
} catch (_: Exception) {}
} catch (_: Exception) {
}
}
NodeData.saveFromStream(node, content.inputStream(), version)
call.respond(ResponseSchematic(node))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ResponseError(
error = e.message ?: "GENERIC", code = "UPLOAD_ERROR"
))
call.respond(
HttpStatusCode.BadRequest, ResponseError(
error = e.message ?: "GENERIC", code = "UPLOAD_ERROR"
)
)
}
}
}
@@ -178,7 +220,7 @@ suspend fun ApplicationCall.receiveSchematic(fieldName: String = "code", delete:
return null
}
if(dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) {
if (dl.timestamp.toInstant().plus(Duration.of(5, ChronoUnit.MINUTES)).isBefore(Instant.now())) {
respond(HttpStatusCode.Gone)
return null
}
@@ -0,0 +1,46 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.routes.v2
import de.steamwar.plugins.SWUserSession
import de.steamwar.routes.configureDiscordAuth
import de.steamwar.routes.configurePasswordAuth
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.delete
import io.ktor.server.routing.route
import io.ktor.server.sessions.clear
import io.ktor.server.sessions.sessions
fun Route.configureAuthV2() {
route("/auth") {
delete {
call.sessions.clear<SWUserSession>()
call.respond(HttpStatusCode.NoContent)
}
configureDiscordAuth()
route("/legacy") {
configurePasswordAuth()
}
}
}
@@ -0,0 +1,55 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.routes.v2
import de.steamwar.ResponseError
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import org.bspfsystems.yamlconfiguration.file.YamlConfiguration
import java.io.File
import kotlin.io.nameWithoutExtension
fun Route.configureGameModeRoutes() {
route("/gamemodes") {
get {
call.respond(
File("/configs/GameModes/").listFiles()!!
.filter { it.name.endsWith(".yml") && !it.name.endsWith(".kits.yml") }
.map { it.nameWithoutExtension })
}
get("/{gamemode}/maps") {
val gamemode = call.parameters["gamemode"]
if (gamemode == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid gamemode"))
return@get
}
val file = File("/configs/GameModes/$gamemode.yml")
if (!file.exists()) {
call.respond(HttpStatusCode.NotFound, ResponseError("Gamemode not found"))
return@get
}
call.respond(YamlConfiguration.loadConfiguration(file).getStringList("Server.Maps"))
}
}
}
@@ -0,0 +1,210 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.routes.v2
import com.sun.tools.jdeprscan.Main.call
import de.steamwar.ResponseError
import de.steamwar.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.routes.ResponseSchematic
import de.steamwar.routes.ResponseSchematicType
import de.steamwar.routes.UploadSchematic
import de.steamwar.routes.nbt
import de.steamwar.routes.receiveSchematic
import de.steamwar.sql.NodeData
import de.steamwar.sql.NodeData.SchematicFormat
import de.steamwar.sql.NodeDownload
import de.steamwar.sql.SWException
import de.steamwar.sql.SchematicNode
import de.steamwar.sql.SchematicType
import dev.dewy.nbt.tags.collection.CompoundTag
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.auth.authentication
import io.ktor.server.auth.principal
import io.ktor.server.request.receive
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import java.io.BufferedInputStream
import java.io.DataInputStream
import java.util.Base64
import java.util.zip.GZIPInputStream
data class ListedSchematicNode(val name: String, val id: Int, val type: String)
fun Route.configureSchematicsV2Route() {
route("/schematics") {
get("/types") {
call.respond(SchematicType.values().filter { !it.check() }
.map { ResponseSchematicType(it.name(), it.toDB()) })
}
get("/list/{path...}") {
val path = call.parameters.getAll("path")?.joinToString("/") ?: "/"
val user = call.authentication.principal<SWAuthPrincipal>()
if (user == null) {
call.respond(HttpStatusCode.Unauthorized)
return@get
}
val node = SchematicNode.getNodeFromPath(user.user, path)
call.respond(SchematicNode.list(user.user, node?.id?.value).map { ListedSchematicNode(it.name, it.id.value, it.schemtype.toDB()) })
}
get("/download/{code}") {
val node = call.receiveSchematic() ?: return@get
val user = call.principal<SWAuthPrincipal>()?.user
if(user != null && !node.accessibleByUser(user)) {
call.respond(HttpStatusCode.Forbidden)
SWException.log("User ${user.userName} tried to download schematic ${node.name} without permission", user.id.toString())
return@get
}
val data = NodeData.getLatest(node) ?: run {
call.respond(HttpStatusCode.InternalServerError)
return@get
}
call.response.header("Content-Disposition", "attachment; filename=\"${node.name}${data.nodeFormat.fileEnding}\"")
call.respondBytes(data.schemData(false).readAllBytes(), contentType = ContentType.Application.OctetStream, status = HttpStatusCode.OK)
}
post("/upload") {
val user = call.principal<SWAuthPrincipal>()?.user
if (user == null) {
call.respond(HttpStatusCode.Unauthorized)
return@post
}
val file = call.receive<UploadSchematic>()
val schemName = file.name.substringBeforeLast(".")
if (SchematicNode.invalidSchemName(arrayOf(schemName))) {
call.respond(HttpStatusCode.BadRequest, ResponseError(
error = "INVALID_NAME"
))
return@post
}
val schemType = file.name.substringAfterLast(".")
if (schemType != "schem" && schemType != "schematic") {
call.respond(HttpStatusCode.BadRequest, ResponseError(
error = "INVALID_SUFFIX"
))
return@post
}
var node = SchematicNode.getSchematicNode(user.getId(), schemName, null as Int?)
if (node == null) {
node = SchematicNode.createSchematic(user.getId(), schemName, null)
}
try {
val content = Base64.getDecoder().decode(file.content)
var schem = nbt.fromStream(DataInputStream(BufferedInputStream(GZIPInputStream(content.inputStream()))))
if (schem.size() == 1) schem = schem.first() as CompoundTag
val version = schem.let {
if (it.contains("Materials"))
return@let SchematicFormat.MCEDIT
else if (it.contains("Blocks"))
return@let SchematicFormat.SPONGE_V3
else
return@let SchematicFormat.SPONGE_V2
}
if (version == SchematicFormat.SPONGE_V3) {
try {
val fawe = schem.getCompound("Metadata")
.getCompound("WorldEdit")
.getString("Version")
.value
if (fawe.equals("2.12.3-SNAPSHOT")) {
SWException.log("Schematic with Bugged Version Uploaded", """
Schematic=$schemName
User=${user.userName}
Id=${user.id}
""".trimIndent())
}
} catch (_: Exception) {}
}
NodeData.saveFromStream(node, content.inputStream(), version)
call.respond(ResponseSchematic(node))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ResponseError(
error = e.message ?: "GENERIC", code = "UPLOAD_ERROR"
))
}
}
route("/{id}") {
install(SWPermissionCheck) {
mustAuth = true
}
get {
val node = call.receiveSchem() ?: return@get
call.respond(ResponseSchematic(node))
}
post("/download") {
val node = call.receiveSchem() ?: return@post
call.respond(HttpStatusCode.OK, NodeDownload.getLink(node))
}
}
}
}
suspend fun ApplicationCall.receiveSchem(): SchematicNode? {
val schemId = parameters["id"]?.toIntOrNull()
if (schemId == null) {
respond(HttpStatusCode.BadRequest)
return null
}
val schem = SchematicNode.getSchematicNode(schemId)
if (schem == null) {
respond(HttpStatusCode.NotFound)
return null
}
if (!(principal<SWAuthPrincipal>()?.user?.let { schem.accessibleByUser(it) } ?: false)) {
respond(HttpStatusCode.Forbidden)
return null
}
return schem
}
@@ -0,0 +1,60 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.routes.v2
import de.steamwar.ResponseError
import de.steamwar.routes.ResponseUser
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.UserPerm
import de.steamwar.util.fetchData
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import java.net.InetSocketAddress
fun Route.configureSteamWarRoute() {
route("/steamwar") {
get {
try {
val server = fetchData(InetSocketAddress("steamwar.de", 25565), 100)
call.respond(server)
} catch (e: Exception) {
e.printStackTrace()
call.respond(HttpStatusCode.InternalServerError, ResponseError(e.message ?: "Unknown error"))
return@get
}
}
get("/team") {
call.respond(
listOf(
UserPerm.PREFIX_ADMIN,
UserPerm.PREFIX_DEVELOPER,
UserPerm.PREFIX_MODERATOR,
UserPerm.PREFIX_SUPPORTER,
UserPerm.PREFIX_BUILDER
).associateWith { SteamwarUser.getUsersWithPerm(it) }.mapKeys { UserPerm.prefixes[it.key]!!.chatPrefix }
.mapValues { it.value.map { ResponseUser(it) } })
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.routes.v2
import de.steamwar.routes.ResponseEvent
import de.steamwar.routes.ResponseTeam
import de.steamwar.routes.ResponseUser
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.Team
import de.steamwar.sql.TeamTable
import de.steamwar.sql.TeamTeilnahme
import de.steamwar.sql.internal.useDb
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.jdbc.Query
import org.jetbrains.exposed.v1.jdbc.andWhere
import org.jetbrains.exposed.v1.jdbc.selectAll
fun Query.addTeamFilter(teamName: String?, teamKuerzel: String?): Query {
teamName?.let { andWhere { TeamTable.name like "%$it%" } }
teamKuerzel?.let { andWhere { TeamTable.kuerzel like "%$it%" } }
return this
}
fun Route.configureTeamRoutes() {
route("/teams") {
get {
val teamName = call.request.queryParameters["name"]
val teamKuerzel = call.request.queryParameters["kuerzel"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
call.respond(useDb {
TeamTable.selectAll().addTeamFilter(teamName, teamKuerzel).limit(limit).offset((page * limit).toLong())
.map { ResponseTeam(it) }
})
}
get("/{team}") {
@Serializable
data class TeamResponse(
val team: ResponseTeam,
val members: List<ResponseUser>,
val leaders: List<ResponseUser>,
val events: List<ResponseEvent>
)
val team = call.parameters["team"]?.toIntOrNull()?.let { Team.byId(it) }
if (team == null) {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.respond(useDb {
TeamResponse(
ResponseTeam(team),
team.membersUser.map { ResponseUser(it) },
team.membersUser.filter { it.leader }.map { ResponseUser(it) },
TeamTeilnahme.getEvents(team.teamId).map { ResponseEvent(it) })
})
}
}
}
@@ -0,0 +1,274 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.routes.v2
import de.steamwar.ResponseError
import de.steamwar.data.getCachedSkin
import de.steamwar.plugins.SWAuthPrincipal
import de.steamwar.plugins.SWPermissionCheck
import de.steamwar.routes.ResponseTeam
import de.steamwar.routes.ResponseUser
import de.steamwar.routes.ResponseUserList
import de.steamwar.routes.UserStats
import de.steamwar.routes.addUserFilter
import de.steamwar.routes.catchException
import de.steamwar.sql.Punishment
import de.steamwar.sql.SteamwarUser
import de.steamwar.sql.SteamwarUserTable
import de.steamwar.sql.Team
import de.steamwar.sql.UserPerm
import de.steamwar.sql.internal.useDb
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.auth.principal
import io.ktor.server.request.receive
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondFile
import io.ktor.server.routing.Route
import io.ktor.server.routing.delete
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.put
import io.ktor.server.routing.route
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.v1.jdbc.selectAll
import java.sql.Timestamp
import java.time.Instant
import java.util.UUID
@Serializable
data class ResponsePunishment(
val id: Int,
val type: Punishment.PunishmentType,
val reason: String,
val issuer: ResponseUser,
val startTime: Long,
val endTime: Long,
val perma: Boolean,
val active: Boolean
) {
constructor(punishment: Punishment) : this(
punishment.id.value,
punishment.type,
punishment.reason,
ResponseUser(SteamwarUser.byId(punishment.punisher)!!),
punishment.startTime.toInstant().toEpochMilli(),
punishment.endTime.toInstant().toEpochMilli(),
punishment.perma,
punishment.isCurrent()
)
}
@Serializable
data class CreatePunishment(
val type: Punishment.PunishmentType,
val reason: String,
val perma: Boolean,
val endTime: Long
)
fun Route.configureUsersRouteV2() {
get("/users/{user}/skin") {
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
return@get
}
val skin = getCachedSkin(user.uuid.toString())
call.response.header("X-Cache", if (skin.second) "HIT" else "MISS")
call.response.header("Cache-Control", "public, max-age=604800")
call.respondFile(skin.first)
}
get("/users/{user}/stats") {
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
return@get
}
val authUser = call.principal<SWAuthPrincipal>()
if (authUser == null || !(authUser.user.id == user.id || authUser.user.hasPerm(UserPerm.MODERATION))) {
call.respond(HttpStatusCode.Forbidden)
return@get
}
call.respond(UserStats(user))
}
route("/users") {
install(SWPermissionCheck) {
permission = UserPerm.MODERATION
}
get {
val name = call.request.queryParameters["name"]
val uuid = call.request.queryParameters["uuid"]?.let { catchException { UUID.fromString(it) } }
val team = call.request.queryParameters.getAll("team")?.map { it.toInt() }?.toSet()
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val includePerms = call.request.queryParameters["includePerms"]?.toBoolean() ?: false
val includeId = call.request.queryParameters["includeId"]?.toBoolean() ?: false
call.respond(
useDb {
ResponseUserList(
SteamwarUserTable.selectAll().addUserFilter(name, uuid, team).limit(limit)
.offset((page * limit).toLong())
.map { ResponseUser(it, includeId, includePerms) },
SteamwarUserTable.selectAll().addUserFilter(name, uuid, team).count()
)
}
)
}
route("/{user}") {
get {
@Serializable
data class UserResponse(val user: ResponseUser, val team: ResponseTeam)
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.NotFound)
return@get
}
call.respond(
useDb {
UserResponse(ResponseUser(user), ResponseTeam(Team.byId(user.team)))
}
)
}
put("/prefix/{prefix}") {
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
return@put
}
val prefix = call.parameters["prefix"]?.let { name -> UserPerm.entries.find { it.name == name } }
if (prefix == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid prefix"))
return@put
}
user.perms().filter { it.name.startsWith("PREFIX_") }.forEach {
UserPerm.removePerm(user, it)
}
if (prefix != UserPerm.PREFIX_NONE) {
UserPerm.addPerm(user, prefix)
}
call.respond(HttpStatusCode.Accepted)
}
route("/perms") {
get {
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
return@get
}
call.respond(user.perms())
}
route("/{perm}") {
put {
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
return@put
}
val perm = call.parameters["perm"]?.let { name -> UserPerm.entries.find { it.name == name } }
if (perm == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid perm"))
return@put
}
UserPerm.addPerm(user, perm)
call.respond(HttpStatusCode.Accepted)
}
delete {
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
return@delete
}
val perm = call.parameters["perm"]?.let { name -> UserPerm.entries.find { it.name == name } }
if (perm == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid perm"))
return@delete
}
UserPerm.removePerm(user, perm)
call.respond(HttpStatusCode.Accepted)
}
}
}
}
route("/punishments") {
get {
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
return@get
}
val punishments = user.punishments.toList()
call.respond(punishments.map { ResponsePunishment(it.second) })
}
post {
val user = call.receiveUser()
if (user == null) {
call.respond(HttpStatusCode.BadRequest, ResponseError("Invalid user"))
return@post
}
val punishment = call.receive<CreatePunishment>()
val punisher = call.principal<SWAuthPrincipal>()?.user ?: return@post
user.punish(punishment.type, Timestamp.from(Instant.ofEpochMilli(punishment.endTime)), punishment.reason, punisher.getId(), punishment.perma)
call.respond(HttpStatusCode.Accepted)
}
}
}
}
fun ApplicationCall.receiveUser(): SteamwarUser? {
val userString = parameters["user"] ?: return null
if (userString == "me") {
return principal<SWAuthPrincipal>()?.user
}
userString.toIntOrNull()?.let { return SteamwarUser.byId(it) }
userString.let { catchException { UUID.fromString(it) } }?.let { return SteamwarUser.get(it) }
return null
}