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
Chaoscaot 2208dcc0fb Fix Set Parent
Signed-off-by: Chaoscaot <max@maxsp.de>
2026-04-15 18:20:07 +02:00
26 changed files with 907 additions and 1247 deletions
@@ -94,7 +94,7 @@ class NodeMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
{ Optional.ofNullable(it?.value) })
private set
fun setParentId(id: Int?) {
fun setParentId(id: Int?) = useDb {
parent = Optional.ofNullable(id)
}
@@ -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
@@ -23,12 +23,14 @@ import com.sk89q.worldedit.extent.clipboard.Clipboard;
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.fight.JoinRequest;
import de.steamwar.fightsystem.listener.Chat;
import de.steamwar.fightsystem.record.GlobalRecorder;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.OneShotStateDependent;
import de.steamwar.fightsystem.utils.Region;
import de.steamwar.sql.SchematicNode;
import de.steamwar.sql.SteamwarUser;
import lombok.Getter;
@@ -52,6 +54,7 @@ import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Villager;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;
import java.util.*;
import java.util.logging.Level;
@@ -84,8 +87,6 @@ public abstract class AI {
private final BukkitTask task;
private final Queue<Action> queue = new ArrayDeque<>();
protected final NavMesh navMesh;
protected AI(FightTeam team, SteamwarUser user) {
this.team = team;
@@ -97,8 +98,6 @@ public abstract class AI {
ais.put(entity.getUniqueId(), this);
team.addMember(entity, user);
navMesh = new NavMesh(team);
if(FightState.Schem.contains(FightState.getFightState()))
Bukkit.getScheduler().runTask(FightSystem.getPlugin(), () -> schematic(team.getClipboard()));
}
@@ -135,61 +134,58 @@ public abstract class AI {
Chat.broadcastChat("PARTICIPANT_CHAT", team.getColoredName(), entity.getName(), message);
}
public LocalCoordinate getPosition() {
return WorldCoordinate.from(entity.getLocation()).toLocal(team);
public Vector getPosition() {
Location location = entity.getLocation();
Region extend = team.getExtendRegion();
if(Fight.getUnrotated() == team)
return new Vector(
location.getX() - extend.getMinX(),
location.getY() - team.getSchemRegion().getMinY(),
location.getZ() - extend.getMinZ()
);
else
return new Vector(
extend.getMaxX() - location.getX(),
location.getY() - team.getSchemRegion().getMinY(),
extend.getMaxZ() - location.getZ()
);
}
public Material getBlock(WorldCoordinate pos) {
return getBlock(pos.toLocal(team));
}
public Material getBlock(LocalCoordinate pos) {
public Material getBlock(Vector pos) {
queue.add(new Action(1));
return pos.toWorld(team).toLocation(Config.world).getBlock().getType();
return translate(pos).getBlock().getType();
}
public BlockData getBlockData(WorldCoordinate pos) {
return getBlockData(pos.toLocal(team));
}
public BlockData getBlockData(LocalCoordinate pos) {
public BlockData getBlockData(Vector pos) {
queue.add(new Action(1));
return pos.toWorld(team).toLocation(Config.world).getBlock().getBlockData();
return translate(pos).getBlock().getBlockData();
}
public void setTNT(WorldCoordinate pos) {
setTNT(pos.toLocal(team));
}
public void setTNT(LocalCoordinate pos) {
public void setTNT(Vector pos) {
queue.add(new Action(1) {
@Override
public void run() {
if(FightState.getFightState() != FightState.RUNNING)
return;
Location location = pos.toWorld(team).toLocation(Config.world);
Location location = translate(pos);
if(interactionDistanceViolation(location)) {
chat("InteractionDistanceViolation: setTNT");
return;
}
Block block = location.getBlock();
if(block.getType() == Material.AIR || block.getType() == Material.WATER || block.getType() == Material.LAVA || block.getType() == Material.TNT)
if(block.getType() == Material.AIR)
block.setType(Material.TNT);
}
});
}
public void interact(WorldCoordinate pos) {
interact(pos.toLocal(team));
}
public void interact(LocalCoordinate pos) {
public void interact(Vector pos) {
queue.add(new Action(1) {
@Override
public void run() {
Location location = pos.toWorld(team).toLocation(Config.world);
Location location = translate(pos);
if(interactionDistanceViolation(location)) {
chat("InteractionDistanceViolation: interact");
return;
@@ -200,15 +196,11 @@ public abstract class AI {
});
}
public void interact(WorldCoordinate pos, int n) {
interact(pos.toLocal(team), n);
}
public void interact(LocalCoordinate pos, int n) {
public void interact(Vector pos, int n) {
queue.add(new Action(1) {
@Override
public void run() {
Location location = pos.toWorld(team).toLocation(Config.world);
Location location = translate(pos);
if (interactionDistanceViolation(location))
return;
Block block = location.getBlock();
@@ -225,75 +217,39 @@ public abstract class AI {
});
}
public MoveResult checkMove(WorldCoordinate target) {
Location location = entity.getLocation();
if(!entity.isOnGround() && location.getBlock().getType() != Material.LADDER) {
return MoveResult.FALLING;
}
if(Math.abs(location.getX() - target.getX()) > 1.5 || Math.abs(location.getY() - target.getY()) > 1.5 || Math.abs(location.getZ() - target.getZ()) > 1.5) {
return MoveResult.OVERDISTANCE;
}
if(!team.getFightPlayer(entity).canEntern() && !team.getExtendRegion().inRegion(target.toLocation(null)))
return MoveResult.OUT_OF_BORDER;
return MoveResult.OK;
}
public enum MoveResult {
FALLING,
OVERDISTANCE,
OUT_OF_BORDER,
OK,
}
public void move(WorldCoordinate pos) {
public void move(Vector pos) {
queue.add(new Action(MOVEMENT_DELAY) {
@Override
public void run() {
MoveResult moveResult = checkMove(pos);
switch (moveResult) {
case FALLING:
FightSystem.getPlugin().getLogger().log(Level.INFO, "Entity falling");
return;
case OVERDISTANCE:
FightSystem.getPlugin().getLogger().log(Level.INFO, () -> entity.getName() + ": Overdistance movement " + entity.getLocation().toVector() + " " + pos);
return;
case OUT_OF_BORDER:
return;
default:
break;
Location location = entity.getLocation();
if(!entity.isOnGround() && location.getBlock().getType() != Material.LADDER) {
FightSystem.getPlugin().getLogger().log(Level.INFO, "Entity falling");
return;
}
Location currentLocation = entity.getLocation();
Location target = pos.toLocation(Config.world);
if (currentLocation.getBlockX() != target.getBlockX() || currentLocation.getBlockY() != target.getBlockY() || currentLocation.getBlockZ() != target.getBlockZ()) {
target.setDirection(target.toVector().subtract(currentLocation.toVector()));
}
if (currentLocation.getBlockX() == target.getBlockX() && currentLocation.getBlockZ() == target.getBlockZ()) {
target.setYaw(currentLocation.getYaw());
Location target = translate(pos);
if(Math.abs(location.getX() - target.getX()) > 1.0 || Math.abs(location.getY() - target.getY()) > 1.5 || Math.abs(location.getZ() - target.getZ()) > 1.0) {
FightSystem.getPlugin().getLogger().log(Level.INFO, () -> entity.getName() + ": Overdistance movement " + location.toVector() + " " + target.toVector());
return;
}
if(!team.getFightPlayer(entity).canEntern() && !team.getExtendRegion().inRegion(target))
return;
if(!entity.teleport(target, PlayerTeleportEvent.TeleportCause.PLUGIN))
FightSystem.getPlugin().getLogger().log(Level.INFO, "Entity not teleported: " + entity.isValid());
GlobalRecorder.getInstance().entityMoves(entity);
navMesh.update(getPosition().toWorld(team));
}
});
}
public void move(LocalCoordinate pos) {
move(pos.toWorld(team));
}
private boolean interactionDistanceViolation(Location location) {
return location.distance(entity.getEyeLocation()) > INTERACTION_RANGE;
}
private void interact(Block block) {
BlockData data = block.getBlockData(); //TODO only 1.14+ compatible at the moment
System.out.println(block + " " + data);
if (data instanceof NoteBlock) {
NoteBlock noteBlock = (NoteBlock) data;
Note note = noteBlock.getNote();
@@ -324,7 +280,6 @@ public abstract class AI {
}
powerable.setPowered(!isPowered);
System.out.println(powerable);
}
block.setBlockData(data);
if(data instanceof Switch) {
@@ -364,6 +319,24 @@ public abstract class AI {
queue.poll().run();
}
public Location translate(Vector pos) {
Region extend = team.getExtendRegion();
if(Fight.getUnrotated() == team)
return new Location(
Config.world,
pos.getX() + extend.getMinX(),
pos.getY() + team.getSchemRegion().getMinY(),
pos.getZ() + extend.getMinZ()
);
else
return new Location(
Config.world,
extend.getMaxX() - pos.getX(),
pos.getY() + team.getSchemRegion().getMinY(),
extend.getMaxZ() - pos.getZ()
);
}
private static class Action {
private int delay;
public Action(int delay) {
@@ -20,10 +20,8 @@
package de.steamwar.fightsystem.ai;
import de.steamwar.Reflection;
import de.steamwar.core.Core;
import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.ai.yoyonow.YoyoNowAI;
import de.steamwar.fightsystem.fight.FightTeam;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -37,8 +35,7 @@ import java.util.stream.Collectors;
@AllArgsConstructor
public class AIManager {
private static final List<AIManager> AIs = Arrays.asList(
new AIManager(DummyAI.class, Material.STONE, () -> ArenaMode.Test.contains(Config.mode)),
new AIManager(YoyoNowAI.class, Material.SLIME_BLOCK, () -> Config.GameModeConfig.Schematic.Type.toDB().equals("miniwargear") && Core.getVersion() >= 14)
new AIManager(DummyAI.class, Material.STONE, () -> ArenaMode.Test.contains(Config.mode))
);
public static List<AIManager> availableAIs() {
@@ -1,180 +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.fightsystem.ai;
import de.steamwar.fightsystem.ai.schematic.Cannon;
import java.util.ArrayList;
import java.util.List;
public interface Action {
Result step(AI ai);
enum Result {
ONGOING,
FINISHED,
FAILED,
;
}
class MultiAction implements Action {
protected final List<Action> actions = new ArrayList<>();
@Override
public Result step(AI ai) {
if (!actions.isEmpty()) {
Action.Result result = actions.get(0).step(ai);
if (result == Action.Result.FAILED) {
ai.chat(this + " + " + actions.get(0) + " failed");
actions.clear();
return Result.FAILED;
}
if (result == Action.Result.FINISHED) {
ai.chat(this + " + " + actions.get(0) + " finished");
actions.remove(0);
}
}
return actions.isEmpty() ? Result.FINISHED : Result.ONGOING;
}
}
class WaitAction implements Action {
private int delay;
public WaitAction(int delay) {
this.delay = delay;
}
@Override
public Result step(AI ai) {
if (--delay > 0) {
return Result.ONGOING;
} else {
return Result.FINISHED;
}
}
}
class MoveAction implements Action {
private final WorldCoordinate destination;
private List<WorldCoordinate> path;
private int overDistanceCounter = 0;
public MoveAction(AI ai, LocalCoordinate destination) {
this(ai, destination.toWorld(ai.team));
}
public MoveAction(AI ai, WorldCoordinate destination) {
this.destination = destination;
this.path = ai.navMesh.pathToNearest(ai.getPosition().toWorld(ai.team), destination);
}
@Override
public Result step(AI ai) {
if (this.path.isEmpty()) return Result.FAILED;
WorldCoordinate coordinate = path.get(0);
if (!ai.navMesh.isWalkable(coordinate)) {
path = ai.navMesh.pathToNearest(ai.getPosition().toWorld(ai.team), destination);
if (path.isEmpty()) return Result.FAILED;
coordinate = path.get(0);
}
AI.MoveResult moveResult = ai.checkMove(coordinate);
if (moveResult == AI.MoveResult.OVERDISTANCE) {
if (overDistanceCounter == 5) {
path = ai.navMesh.pathToNearest(ai.getPosition().toWorld(ai.team), destination);
return path.isEmpty() ? Result.FAILED : Result.ONGOING;
}
if (overDistanceCounter++ > 5) {
return Result.FAILED;
}
return Result.ONGOING;
}
overDistanceCounter = 0;
if (moveResult == AI.MoveResult.FALLING) {
return Result.ONGOING;
}
if (moveResult == AI.MoveResult.OK) {
path.remove(0);
ai.move(coordinate);
}
return path.isEmpty() ? Result.FINISHED : Result.ONGOING;
}
}
class InteractAction implements Action {
private final LocalCoordinate destination;
public InteractAction(LocalCoordinate destination) {
this.destination = destination;
}
@Override
public Result step(AI ai) {
ai.interact(destination);
return Result.FINISHED;
}
}
class ReadyAction implements Action {
@Override
public Result step(AI ai) {
ai.setReady();
return Result.FINISHED;
}
}
class PlaceTNTAction implements Action {
private final LocalCoordinate destination;
public PlaceTNTAction(LocalCoordinate destination) {
this.destination = destination;
}
@Override
public Result step(AI ai) {
ai.setTNT(destination);
return Result.FINISHED;
}
}
class LoadCannon extends MultiAction {
public LoadCannon(AI ai, Cannon cannon) {
for (LocalCoordinate tntPosition : cannon.getTntPositions()) {
actions.add(new MoveAction(ai, tntPosition.toWorld(ai.team)));
actions.add(new PlaceTNTAction(tntPosition));
}
LocalCoordinate trigger = cannon.getTriggerPositions().iterator().next();
actions.add(new MoveAction(ai, trigger.toWorld(ai.team)));
actions.add(new InteractAction(trigger));
}
}
}
@@ -1,90 +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.fightsystem.ai;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.utils.Region;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.util.NumberConversions;
@Getter
@AllArgsConstructor
public class LocalCoordinate {
private double x;
private double y;
private double z;
public int getBlockX() {
return NumberConversions.floor(x);
}
public int getBlockY() {
return NumberConversions.floor(y);
}
public int getBlockZ() {
return NumberConversions.floor(z);
}
public double distanceSquared(LocalCoordinate coordinate) {
return NumberConversions.square(x - coordinate.x) + NumberConversions.square(y - coordinate.y) + NumberConversions.square(z - coordinate.z);
}
public WorldCoordinate toWorld(FightTeam team) {
// TODO: Fix the of by one blue/red
// Team Red is not correct!
// NightTown is team blue unrotated!
Region extend = team.getExtendRegion();
if(Fight.getUnrotated() == team) {
return new WorldCoordinate(
x + extend.getMinX(),
y + team.getSchemRegion().getMinY(),
z + extend.getMinZ()
);
} else {
return new WorldCoordinate(
extend.getMaxX() - x - 1,
y + team.getSchemRegion().getMinY(),
extend.getMaxZ() - z - 1
);
}
}
public LocalCoordinate add(LocalCoordinate localCoordinate) {
this.x += localCoordinate.x;
this.y += localCoordinate.y;
this.z += localCoordinate.z;
return this;
}
public LocalCoordinate add(double x, double y, double z) {
this.x += x;
this.y += y;
this.z += z;
return this;
}
@Override
public String toString() {
return "Local{" + x + "," + y + "," + z + "}";
}
}
@@ -1,402 +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.fightsystem.ai;
import de.steamwar.entity.RBlockDisplay;
import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.OneShotStateDependent;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.NumberConversions;
import org.bukkit.util.Transformation;
import org.bukkit.util.VoxelShape;
import org.joml.AxisAngle4f;
import org.joml.Vector3f;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class NavMesh {
private static final double PLAYER_FALL_DISTANCE = 3;
private static final double PLAYER_JUMP_HEIGHT = 1.25;
private static final double PLAYER_HEIGHT = 1.8;
private static final Cuboid PLAYER_SHADOW = new Cuboid(0.2, 0, 0.2, 0.6, 1, 0.6);
private static final Set<Pos> RELATIVE_BLOCKS = new HashSet<>();
protected static final BlockData PATH_BLOCK = Material.LIME_STAINED_GLASS.createBlockData();
static {
for (int y = (int) -Math.ceil(PLAYER_FALL_DISTANCE); y <= Math.ceil(PLAYER_JUMP_HEIGHT); y++) {
RELATIVE_BLOCKS.add(new Pos(-1, y, 0));
RELATIVE_BLOCKS.add(new Pos(1, y, 0));
RELATIVE_BLOCKS.add(new Pos(0, y, -1));
RELATIVE_BLOCKS.add(new Pos(0, y, 1));
}
RELATIVE_BLOCKS.add(new Pos(0, 1, 0));
RELATIVE_BLOCKS.add(new Pos(0, -1, 0));
}
private FightTeam fightTeam;
private REntityServer server = new REntityServer();
private Map<Pos, Pos> walkable = new HashMap<>();
private Map<Pos, Set<Pos>> connections = new HashMap<>(); // Reverse connections!
@Getter
private boolean ready = false;
public NavMesh(FightTeam fightTeam) {
this.fightTeam = fightTeam;
new OneShotStateDependent(ArenaMode.All, FightState.PostSchemSetup, () -> {
Bukkit.getOnlinePlayers().forEach(server::addPlayer);
Bukkit.getScheduler().runTaskLater(FightSystem.getPlugin(), () -> {
long time = System.currentTimeMillis();
fightTeam.getExtendRegion().forEach((x, y, z) -> {
if (y < fightTeam.getSchemRegion().getMinY()) return;
calculatePosition(new Pos(x, y, z), walkable);
});
walkable.values().forEach(this::checkNeighbouring);
System.out.println("NavMesh initialized in " + (System.currentTimeMillis() - time) + " ms");
ready = true;
}, 20);
});
new OneShotStateDependent(ArenaMode.All, FightState.Spectate, () -> {
ready = false;
});
}
private void calculatePosition(Pos pos, Map<Pos, Pos> walkable) {
Block block = Config.world.getBlockAt(pos.x, pos.y, pos.z);
// Air is not walkable
if (block.isPassable()) return;
// Ignore Ladders from floor calculation!
if (block.getType() != Material.LADDER) {
// Floor Position
pos.floor = playerCollisionMax(block.getCollisionShape());
// If no walkable block remove
if (pos.floor == 0) return;
} else {
pos.floor = 0.5;
}
// Ceiling Position
pos.ceiling = fightTeam.getExtendRegion().getMaxY() - pos.y;
for (int y = pos.y + 1; y <= fightTeam.getExtendRegion().getMaxY() + 3; y++) {
block = Config.world.getBlockAt(pos.x, y, pos.z);
if (block.isPassable()) continue;
double min = playerCollisionMin(block.getCollisionShape());
if (min >= 1.0) continue;
pos.ceiling = y + min - pos.y;
break;
}
// If player cannot fit remove
if (pos.ceiling - pos.floor < PLAYER_HEIGHT) return;
walkable.put(pos, pos);
}
@RequiredArgsConstructor
private static class Pos {
private final int x;
private final int y;
private final int z;
private double floor = 0;
private double ceiling = 0;
@Override
public String toString() {
return x + "," + y + "(" + floor + ".." + ceiling + ")" + "," + z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Pos)) return false;
Pos pos = (Pos) o;
return x == pos.x && y == pos.y && z == pos.z;
}
@Override
public int hashCode() {
return Objects.hash(x, y, z);
}
public Pos add(Pos pos) {
return new Pos(x + pos.x, y + pos.y, z + pos.z);
}
public WorldCoordinate toWorld() {
return new WorldCoordinate(x, y + floor, z);
}
public double floorPosition() {
return y + floor;
}
public double ceilingPosition() {
return y + ceiling;
}
}
private double playerCollisionMax(VoxelShape voxelShape) {
List<Double> yList = voxelShape.getBoundingBoxes().stream()
.flatMap(boundingBox -> Stream.of(boundingBox.getMinY(), boundingBox.getMaxY()))
.sorted()
.collect(Collectors.toList());
double collisionY = 0;
for (double y : yList) {
if (voxelShape.getBoundingBoxes()
.stream()
.noneMatch(boundingBox -> collides(y, boundingBox))) {
collisionY = y;
break;
}
}
return collisionY;
}
private double playerCollisionMin(VoxelShape voxelShape) {
List<Double> yList = voxelShape.getBoundingBoxes().stream()
.flatMap(boundingBox -> Stream.of(boundingBox.getMinY(), boundingBox.getMaxY()))
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
double collisionY = 1.5;
for (double y : yList) {
if (voxelShape.getBoundingBoxes()
.stream()
.noneMatch(boundingBox -> collides(y - 1, boundingBox))) {
collisionY = y;
break;
}
}
return collisionY;
}
private boolean collides(double y, BoundingBox boundingBox) {
Cuboid second = new Cuboid(boundingBox.getMinX(), boundingBox.getMinY(), boundingBox.getMinZ(), boundingBox.getWidthX(), boundingBox.getHeight(), boundingBox.getWidthZ());
return PLAYER_SHADOW.setY(y).intersects(second);
}
@AllArgsConstructor
private static class Cuboid {
private double x;
private double y;
private double z;
private double dx;
private double dy;
private double dz;
public boolean intersects(Cuboid cuboid) {
double minx = x - cuboid.dx;
double miny = y - cuboid.dy;
double minz = z - cuboid.dz;
double maxx = minx + dx + cuboid.dx;
double maxy = miny + dy + cuboid.dy;
double maxz = minz + dz + cuboid.dz;
return maxx > cuboid.x && maxy > cuboid.y && maxz > cuboid.z && minx < cuboid.x && miny < cuboid.y && minz < cuboid.z;
}
public Cuboid setY(double y) {
return new Cuboid(x, y, z, dx, dy, dz);
}
}
public void update(WorldCoordinate coordinate) {
final int px = coordinate.getBlockX();
final int py = coordinate.getBlockY();
final int pz = coordinate.getBlockZ();
for (int x = -2; x <= 2; x++) {
for (int z = -2; z <= 2; z++) {
for (int y = fightTeam.getSchemRegion().getMinY(); y <= py + 2; y++) {
Pos current = new Pos(px + x, y, pz + z);
walkable.remove(current);
calculatePosition(current, walkable);
}
}
}
walkable.values().forEach(this::checkNeighbouring);
}
private void checkNeighbouring(Pos pos) {
for (Set<Pos> value : connections.values()) {
value.remove(pos);
}
for (Pos relative : RELATIVE_BLOCKS) {
Pos other = walkable.get(pos.add(relative));
if (other == null) continue;
Block thisBlock = Config.world.getBlockAt(pos.x, pos.y, pos.z);
Block otherBlock = Config.world.getBlockAt(other.x, other.y, other.z);
if (thisBlock.getType() != Material.LADDER && otherBlock.getType() == Material.LADDER) {
if ((relative.x != 0 || relative.z != 0) && pos.y != other.y) continue;
} else {
// Check if jumpable vertical distance
if (other.floorPosition() - pos.floorPosition() > PLAYER_JUMP_HEIGHT) continue;
// Check if Ceiling is high enough for player walking down!
if (other.ceilingPosition() < pos.floorPosition() + PLAYER_HEIGHT) continue;
}
connections.computeIfAbsent(other, __ -> new LinkedHashSet<>()).add(pos);
}
}
private Pos toPos(WorldCoordinate coordinate) {
Pos pos = new Pos(coordinate.getBlockX(), coordinate.getBlockY(), coordinate.getBlockZ());
if (walkable.containsKey(pos)) return walkable.get(pos);
pos = new Pos(pos.x, pos.y - 1, pos.z);
if (walkable.containsKey(pos)) return walkable.get(pos);
return null;
}
public boolean isWalkable(WorldCoordinate coordinate) {
Pos pos = new Pos(coordinate.getBlockX(), NumberConversions.floor(coordinate.getY() - 0.0625), coordinate.getBlockZ());
return walkable.containsKey(pos);
}
public List<WorldCoordinate> walkable() {
return walkable.values().stream().map(Pos::toWorld).collect(Collectors.toList());
}
public List<WorldCoordinate> pathToNearest(WorldCoordinate fromCoordinate, WorldCoordinate toCoordinate) {
Pos to = toPos(toCoordinate);
if (walkable.containsKey(to)) return path(toPos(fromCoordinate), to);
Pos closestTo = walkable.values()
.stream()
.filter(pos -> {
double distance = pos.toWorld().distanceSquared(toCoordinate);
return distance < AI.INTERACTION_RANGE * AI.INTERACTION_RANGE && distance > 1.5 * 1.5;
})
.min(Comparator.comparing(pos -> pos.toWorld().distanceSquared(toCoordinate)))
.orElse(null);
if (closestTo == null) {
return Collections.emptyList();
}
return path(toPos(fromCoordinate), closestTo);
}
public List<WorldCoordinate> path(WorldCoordinate fromCoordinate, WorldCoordinate toCoordinate) {
Pos from = toPos(fromCoordinate);
Pos to = toPos(toCoordinate);
return path(from, to);
}
private List<WorldCoordinate> path(Pos from, Pos to) {
server.getEntities().forEach(REntity::die);
if (from == null || to == null) {
return Collections.emptyList();
}
if (from.equals(to)) {
return Collections.emptyList();
}
List<Pos> checking = new ArrayList<>(Arrays.asList(to));
Map<Pos, Pos> route = new HashMap<>();
while (!checking.isEmpty()) {
Set<Pos> toCheck = new HashSet<>();
for (Pos pos : checking) {
boolean foundFrom = false;
Set<Pos> successors = connections.get(pos);
if (successors == null) {
return Collections.emptyList();
}
for (Pos p : successors) {
if (route.containsKey(p)) continue;
route.put(p, pos);
toCheck.add(p);
foundFrom = p.equals(from);
if (foundFrom) break;
}
if (foundFrom) {
List<Pos> path = new ArrayList<>();
path.add(from);
while (path.get(path.size() - 1) != to) {
path.add(route.get(path.get(path.size() - 1)));
}
for (int i = 0; i < path.size(); i++) {
path.set(i, walkable.get(path.get(i)));
}
int i = 0;
while (i < path.size() - 1) {
Pos current = path.get(i);
Pos next = path.get(i + 1);
if (current.floorPosition() > next.floorPosition() + 1) {
Pos between = new Pos(next.x, current.y, next.z);
between.floor = current.floor;
path.add(i + 1, between);
i++;
}
i++;
}
List<WorldCoordinate> coordinates = path.stream().skip(1).map(Pos::toWorld).collect(Collectors.toList());
coordinates.forEach(coordinate -> {
RBlockDisplay block = new RBlockDisplay(server, coordinate.toLocation(Config.world));
block.setBlock(PATH_BLOCK);
block.setTransform(new Transformation(new Vector3f(0, 0, 0), new AxisAngle4f(0, 0, 0, 0), new Vector3f(1, 0.001F, 1), new AxisAngle4f(0, 0, 0, 0)));
});
coordinates.forEach(vector -> vector.add(0.5, 0, 0.5));
return coordinates;
}
}
checking.clear();
checking.addAll(toCheck);
}
return Collections.emptyList();
}
}
@@ -1,107 +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.fightsystem.ai;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.utils.Region;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.util.NumberConversions;
import org.bukkit.util.Vector;
@Getter
@AllArgsConstructor
public class WorldCoordinate {
private double x;
private double y;
private double z;
public static WorldCoordinate from(Location location) {
return new WorldCoordinate(location.getX(), location.getY(), location.getZ());
}
public static WorldCoordinate from(Vector vector) {
return new WorldCoordinate(vector.getX(), vector.getY(), vector.getZ());
}
public int getBlockX() {
return NumberConversions.floor(x);
}
public int getBlockY() {
return NumberConversions.floor(y);
}
public int getBlockZ() {
return NumberConversions.floor(z);
}
public double distanceSquared(WorldCoordinate coordinate) {
return NumberConversions.square(x - coordinate.x) + NumberConversions.square(y - coordinate.y) + NumberConversions.square(z - coordinate.z);
}
public LocalCoordinate toLocal(FightTeam team) {
// TODO: Fix the of by one blue/red
Region extend = team.getExtendRegion();
if (Fight.getUnrotated() == team) {
return new LocalCoordinate(
x - extend.getMinX(),
y - team.getSchemRegion().getMinY(),
z - extend.getMinZ()
);
} else {
return new LocalCoordinate(
extend.getMaxX() - x + 1,
y - team.getSchemRegion().getMinY(),
extend.getMaxZ() - z + 1
);
}
}
public WorldCoordinate add(WorldCoordinate worldCoordinate) {
this.x += worldCoordinate.x;
this.y += worldCoordinate.y;
this.z += worldCoordinate.z;
return this;
}
public WorldCoordinate add(double x, double y, double z) {
this.x += x;
this.y += y;
this.z += z;
return this;
}
public Vector toVector() {
return new Vector(x, y, z);
}
public Location toLocation(World world) {
return new Location(world, x, y, z);
}
@Override
public String toString() {
return "World{" + x + "," + y + "," + z + "}";
}
}
@@ -1,45 +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.fightsystem.ai.schematic;
import de.steamwar.fightsystem.ai.LocalCoordinate;
import lombok.Getter;
import java.util.HashSet;
import java.util.Set;
@Getter
public class Bridge {
private final Set<LocalCoordinate> shieldActivators = new HashSet<>();
private LocalCoordinate automaticActivator = null;
private int automaticActivatorTime = 0;
public Bridge addShieldActivator(LocalCoordinate shieldActivator) {
this.shieldActivators.add(shieldActivator);
return this;
}
public Bridge addAutomaticActivator(LocalCoordinate automaticActivator, int automaticActivatorTime) {
this.automaticActivator = automaticActivator;
this.automaticActivatorTime = automaticActivatorTime;
return this;
}
}
@@ -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.fightsystem.ai.schematic;
import de.steamwar.fightsystem.ai.LocalCoordinate;
import lombok.Getter;
import org.bukkit.util.Consumer;
import java.util.ArrayList;
import java.util.List;
@Getter
public class WarMachine {
private int schematicId = 0;
private Bridge bridge = new Bridge();
private List<Cannon> cannons = new ArrayList<>();
private List<LocalCoordinate> tntChests = new ArrayList<>();
public WarMachine setSchematicId(int schematicId) {
this.schematicId = schematicId;
return this;
}
public WarMachine editBridge(Consumer<Bridge> initializer) {
initializer.accept(bridge);
return this;
}
public WarMachine addCannon(Consumer<Cannon> initializer) {
Cannon cannon = new Cannon();
initializer.accept(cannon);
cannons.add(cannon);
return this;
}
public WarMachine addTnTChest(LocalCoordinate chest) {
tntChests.add(chest);
return this;
}
public WarMachine finish(List<WarMachine> list) {
list.add(this);
return this;
}
}
@@ -1,71 +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.fightsystem.ai.schematic.impl;
import de.steamwar.fightsystem.ai.LocalCoordinate;
import de.steamwar.fightsystem.ai.schematic.WarMachine;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MiniWarGear20 {
private static final Random RANDOM = new Random();
private static final List<WarMachine> MiniWarGear20 = new ArrayList<>();
public static WarMachine select() {
return MiniWarGear20.get(RANDOM.nextInt(MiniWarGear20.size()));
}
public static WarMachine select(int schematicId) {
return MiniWarGear20.stream()
.filter(warMachine -> warMachine.getSchematicId() == schematicId)
.findFirst()
.orElse(null);
}
public static final WarMachine DPR_PV1_Reaper = new WarMachine()
.setSchematicId(135745)
.editBridge(bridge -> {
bridge.addShieldActivator(new LocalCoordinate(22, 15, 18));
bridge.addShieldActivator(new LocalCoordinate(31, 3, 12));
bridge.addShieldActivator(new LocalCoordinate(19, 3, 12));
})
.addCannon(cannon -> {
cannon.addTnt(new LocalCoordinate(29, 1, 21));
cannon.addTnt(new LocalCoordinate(29, 1, 22));
cannon.addTnt(new LocalCoordinate(29, 1, 23));
cannon.addTnt(new LocalCoordinate(30, 1, 22));
cannon.addTnt(new LocalCoordinate(30, 1, 23));
cannon.addTnt(new LocalCoordinate(29, 2, 24));
cannon.addTnt(new LocalCoordinate(30, 2, 24));
cannon.addTnt(new LocalCoordinate(29, 2, 25));
cannon.addTnt(new LocalCoordinate(30, 2, 25));
cannon.addTnt(new LocalCoordinate(29, 3, 24));
cannon.addTnt(new LocalCoordinate(30, 3, 24));
cannon.addTnt(new LocalCoordinate(29, 3, 25));
cannon.addTnt(new LocalCoordinate(30, 3, 25));
cannon.addTrigger(new LocalCoordinate(30, 3, 19));
cannon.addTrigger(new LocalCoordinate(31, 3, 18));
cannon.addTrigger(new LocalCoordinate(31, 3, 20));
})
.finish(MiniWarGear20);
}
@@ -1,98 +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.fightsystem.ai.yoyonow;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import de.steamwar.fightsystem.ai.AI;
import de.steamwar.fightsystem.ai.Action;
import de.steamwar.fightsystem.ai.schematic.Cannon;
import de.steamwar.fightsystem.ai.schematic.WarMachine;
import de.steamwar.fightsystem.ai.schematic.impl.MiniWarGear20;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.sql.SchematicNode;
import de.steamwar.sql.SteamwarUser;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class YoyoNowAI extends AI {
private WarMachine selectedSchematic;
protected YoyoNowAI(FightTeam team) {
super(team, SteamwarUser.get("YoyoNow.AI"));
getEntity().setGlowing(true);
}
@Override
public SchematicNode chooseSchematic() {
selectedSchematic = MiniWarGear20.select();
return SchematicNode.getSchematicNode(selectedSchematic.getSchematicId());
}
@Override
public void schematic(Clipboard clipboard) {
selectedSchematic = MiniWarGear20.select(team.getSchematic());
if (selectedSchematic == null) stop();
}
private final Random random = new Random();
private List<Action> actions = new ArrayList<>();
private boolean setupDone = false;
@Override
protected void plan() {
if (!navMesh.isReady()) return;
if (!actions.isEmpty()) {
Action.Result result = actions.get(0).step(this);
if (result == Action.Result.FAILED) {
chat(actions.get(0) + " failed");
actions.clear();
return;
}
if (result == Action.Result.FINISHED) {
chat(actions.get(0) + " finished");
actions.remove(0);
}
return;
}
if (!setupDone && FightState.getFightState() == FightState.POST_SCHEM_SETUP) {
chat("Setup actions to perform!");
selectedSchematic.getBridge().getShieldActivators().forEach(localCoordinate -> {
actions.add(new Action.MoveAction(this, localCoordinate.toWorld(getTeam())));
actions.add(new Action.InteractAction(localCoordinate));
});
actions.add(new Action.ReadyAction());
chat("Actions: " + actions.size());
setupDone = true;
}
if (FightState.getFightState() == FightState.RUNNING) {
Cannon cannon = selectedSchematic.getCannons().get(random.nextInt(selectedSchematic.getCannons().size()));
actions.add(new Action.LoadCannon(this, cannon));
}
}
}
+1 -11
View File
@@ -91,14 +91,4 @@ tasks.register<FightServer>("QuickGear20") {
template = "QuickGear20"
worldName = "arenas/WarGearPark"
config = "QuickGear20.yml"
}
tasks.register<FightServer>("MiniWarGear20") {
group = "run"
description = "Run a MiniWarGear 1.20 Fight Server"
dependsOn(":SpigotCore:shadowJar")
dependsOn(":FightSystem:shadowJar")
template = "MiniWarGear20"
worldName = "arenas/NightTown"
config = "MiniWarGear20.yml"
}
}
+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
}
@@ -17,29 +17,30 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.ai.schematic;
package de.steamwar.routes.v2
import de.steamwar.fightsystem.ai.LocalCoordinate;
import lombok.Getter;
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
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
fun Route.configureAuthV2() {
route("/auth") {
delete {
call.sessions.clear<SWUserSession>()
call.respond(HttpStatusCode.NoContent)
}
@Getter
public class Cannon {
private final List<LocalCoordinate> tntPositions = new ArrayList<>();
private final Set<LocalCoordinate> triggerPositions = new HashSet<>();
public Cannon addTnt(LocalCoordinate tnt) {
tntPositions.add(tnt);
return this;
configureDiscordAuth()
route("/legacy") {
configurePasswordAuth()
}
}
public Cannon addTrigger(LocalCoordinate trigger) {
triggerPositions.add(trigger);
return this;
}
}
}
@@ -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
}