currently refactoring a lot

This commit is contained in:
kento2 2026-07-04 12:25:54 +02:00
parent 7fe7409a15
commit a32805a623
46 changed files with 571 additions and 642 deletions

View file

@ -22,12 +22,7 @@ dependencies {
implementation(project(":core-bukkit-api"))
// http://forge.kentoj.de/scrow/-/packages/maven/de.kentoj.scrow:kencommandapi-core
implementation("de.kentoj.scrow:kencommandapi-core:0.21")
// TODO remove, we use postgres now
// https://www.mongodb.com/docs/languages/java/reactive-streams-driver/current/get-started/download-and-install/
implementation(platform("org.mongodb:mongodb-driver-bom:5.6.1"))
implementation("org.mongodb:mongodb-driver-reactivestreams")
implementation("de.kentoj.scrow:kencommandapi-core:0.22")
}
tasks {

View file

@ -1,13 +1,13 @@
package de.kentoj.scrow.bukkit;
import de.kentoj.scrow.bukkit.crown.CrownCommand;
import de.kentoj.scrow.bukkit.economy.CoinsCommand;
import de.kentoj.scrow.bukkit.economy.command.CoinsCommand;
import de.kentoj.scrow.bukkit.economy.command.CoinsSetLiteral;
import de.kentoj.scrow.bukkit.friends.command.FriendCommand;
import de.kentoj.scrow.bukkit.region.RegionPlayerTracker;
import de.kentoj.scrow.bukkit.region.RegionTask;
import de.kentoj.scrow.bukkit.server.LobbyCommand;
import de.kentoj.scrow.bukkit.server.PlayCommand;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.IOException;
@ -26,8 +26,8 @@ public class CoreImplPlugin extends JavaPlugin {
ScrowAPISurface.initScrowAPI(this, ENABLE_DATABASE);
{
ScrowAPI.getCommandApi().register(new FriendCommand().getRootNode());
ScrowAPI.getCommandApi().register(new CoinsCommand().getRootNode());
ScrowAPI.getCommandApi().register(new FriendCommand().getRootNode());
ScrowAPI.getCommandApi().register(new CrownCommand().getRootNode());
ScrowAPI.getCommandApi().register(new PlayCommand().getRootNode());
ScrowAPI.getCommandApi().register(new LobbyCommand().getRootNode());

View file

@ -27,9 +27,7 @@ import java.io.IOException;
public class ScrowAPISurface {
public static void initScrowAPI(Plugin plugin, boolean enableDatabase) {
ScrowAPI.setMinecraftScheduler(Schedulers.fromExecutor(cmd -> {
Bukkit.getScheduler().runTask(plugin, cmd);
}));
ScrowAPI.setCorePlugin(plugin);
try {
String natsHost = EnvUtils.envOrThrow("HOST_NATS");
@ -44,23 +42,23 @@ public class ScrowAPISurface {
if (enableDatabase) {
ConnectionFactory factory = ConnectionFactories.get(EnvUtils.envOrThrow("HOST_POSTGRES"));
ScrowAPI.setDbConFactory(new DatabaseConnectionFactoryImpl(factory));
ScrowAPI.setJdbi(new DatabaseConnectionFactoryImpl(factory));
}
ScrowAPI.setRegionManager(new RegionManagerImpl());
ScrowAPI.setCommandApi(new CommandAPI(plugin));
ScrowAPI.setInstanceManager(new InstanceManagerImpl(ScrowAPI.getNats()));
ScrowAPI.setEconomyService(ScrowAPI.getDbConFactory() != null ?
new PostgresEconomyService(ScrowAPI.getDbConFactory()) : new InMemoryEconomyService());
ScrowAPI.setFriendRequestService(ScrowAPI.getDbConFactory() != null ?
new PostgresFriendRequestService(ScrowAPI.getDbConFactory()) : new InMemoryFriendRequestService());
ScrowAPI.setFriendshipService(ScrowAPI.getDbConFactory() != null ?
new PostgresFriendshipService(ScrowAPI.getDbConFactory()) : new InMemoryFriendshipService());
ScrowAPI.setEconomyService(ScrowAPI.getJdbi() != null ?
new PostgresEconomyService(ScrowAPI.getJdbi()) : new InMemoryEconomyService());
ScrowAPI.setFriendRequestService(ScrowAPI.getJdbi() != null ?
new PostgresFriendRequestService(ScrowAPI.getJdbi()) : new InMemoryFriendRequestService());
ScrowAPI.setFriendshipService(ScrowAPI.getJdbi() != null ?
new PostgresFriendshipService(ScrowAPI.getJdbi()) : new InMemoryFriendshipService());
}
public static void destroyScrowAPI() {
try {
ScrowAPI.getDbConFactory().close();
ScrowAPI.getJdbi().close();
} catch (Exception e) {
throw new RuntimeException(e);
}

View file

@ -28,7 +28,7 @@ public class CrownDeployServerLiteral implements CommandExecutor<CommandSender>
}
@Override
public CompletableFuture<Result<Void,String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object,String>> execute(CommandContext<CommandSender> ctx) {
var template = ctx.getArg(templateArg);
return ScrowAPI.getInstanceManager().deployInstance(template)
.map(server -> {

View file

@ -38,7 +38,7 @@ public class CrownDestroyServerLiteral implements CommandExecutor<CommandSender>
}
@Override
public CompletableFuture<Result<Void,String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object,String>> execute(CommandContext<CommandSender> ctx) {
var handle = ctx.getArg(handleArg);
return ScrowAPI.getInstanceManager().destroyInstance(handle)
.map(__ -> {

View file

@ -25,7 +25,7 @@ public class CrownListServersLiteral implements CommandExecutor<CommandSender> {
}
@Override
public CompletableFuture<Result<Void,String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object,String>> execute(CommandContext<CommandSender> ctx) {
return ScrowAPI.getInstanceManager().getInstances()
.collectList()
.map(list -> style.list(Component.text("Running instances:"),

View file

@ -1,78 +0,0 @@
package de.kentoj.scrow.bukkit.economy;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.argument.CommandArgument;
import de.kentoj.kencommandapi.api.argument.types.IntegerArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.api.Results;
import de.kentoj.kencommandapi.type.OfflinePlayerArgumentType;
import de.kentoj.scrow.bukkit.ScrowAPI;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import reactor.core.scheduler.Schedulers;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
public class CoinsCommand {
@Getter
private final CommandNode<CommandSender> rootNode;
private final CommandArgument<CommandSender, OfflinePlayer> playerArg;
private final CommandArgument<CommandSender, Integer> amountArg;
public CoinsCommand() {
this.rootNode = CommandNode.node("coins", "eco");
this.playerArg = CommandArgument.arg("player", OfflinePlayerArgumentType.getInstance());
this.amountArg = CommandArgument.arg("amount", new IntegerArgumentType<>());
{
CommandNode<CommandSender> setLiteral = CommandNode.node("set");
rootNode.addLiteral(setLiteral);
setLiteral.addArgument(playerArg);
setLiteral.addArgument(amountArg);
setLiteral.setExecutor(this::setCoins);
}
{
CommandNode<CommandSender> getLiteral = CommandNode.node("get");
rootNode.addLiteral(getLiteral);
getLiteral.addArgument(playerArg);
getLiteral.setExecutor(this::getCoins);
}
}
private CompletableFuture<Result<Void,String>> getCoins(CommandContext<CommandSender> ctx) {
var player = ctx.getArg(playerArg);
return ScrowAPI.getEconomyService()
.getCoins(player.getUniqueId())
.subscribeOn(Schedulers.boundedElastic())
.publishOn(ScrowAPI.getMinecraftScheduler())
.map(amount -> {
if (player == ctx.getSender()) {
ctx.getSender().sendMessage("You have " + amount + "$");
} else {
ctx.getSender().sendMessage(player.getName() + " has " + amount + "$");
}
return Result.success();
}).toFuture();
}
private CompletableFuture<Result<Void,String>> setCoins(CommandContext<CommandSender> ctx) {
var player = ctx.getArg(playerArg);
var amount = ctx.getArg(amountArg);
return ScrowAPI.getEconomyService()
.setCoins(player.getUniqueId(), amount)
.subscribeOn(Schedulers.boundedElastic())
.publishOn(ScrowAPI.getMinecraftScheduler())
.map(__ -> {
ctx.getSender().sendMessage(player.getName() + " now has " + amount + "$");
return Results.success(null);
}).toFuture();
}
}

View file

@ -1,9 +1,7 @@
package de.kentoj.scrow.bukkit.economy;
import com.google.common.base.Preconditions;
import de.kentoj.scrow.bukkit.EconomyService;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
import de.kentoj.scrow.bukkit.misc.EconomyService;
import java.util.HashMap;
import java.util.Map;
@ -14,34 +12,29 @@ public class InMemoryEconomyService implements EconomyService {
private final Map<UUID, Integer> coins = new HashMap<>();
@Override
public Mono<@NotNull Integer> getCoins(UUID playerId) {
return Mono.fromSupplier(() -> coins.getOrDefault(playerId, 0));
public int getCoins(UUID playerId) {
return coins.getOrDefault(playerId, 0);
}
@Override
public Mono<@NotNull Void> setCoins(UUID playerId, int amount) {
public void setCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.fromSupplier(() -> coins.put(playerId, amount)).then();
coins.put(playerId, amount);
}
@Override
public Mono<@NotNull Void> depositCoins(UUID playerId, int amount) {
public void depositCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.fromRunnable(() -> {
var cur = coins.getOrDefault(playerId, amount);
coins.put(playerId, cur + amount);
});
var cur = coins.getOrDefault(playerId, 0);
coins.put(playerId, cur + amount);
}
@Override
public Mono<@NotNull Boolean> tryWithdrawCoins(UUID playerId, int amount) {
public boolean tryWithdrawCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.fromSupplier(() -> {
var cur = coins.getOrDefault(playerId, 0);
if (cur < amount) return false;
coins.put(playerId, cur - amount);
return true;
});
var cur = coins.getOrDefault(playerId, 0);
if (cur < amount) return false;
coins.put(playerId, cur - amount);
return true;
}
}

View file

@ -1,74 +0,0 @@
package de.kentoj.scrow.bukkit.economy;
import com.google.common.base.Preconditions;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.Updates;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import de.kentoj.scrow.bukkit.EconomyService;
import org.bson.Document;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
import java.util.UUID;
// TODO log
public class MongoEconomyService implements EconomyService {
private static final String COLLECTION_NAME = "economy";
private final @NotNull MongoDatabase database;
public MongoEconomyService(MongoDatabase database) {
this.database = database;
Mono.from(database.createCollection(COLLECTION_NAME)).block();
}
@Override
public Mono<@NotNull Integer> getCoins(UUID playerId) {
return Mono.from(
getCollection().find(Filters.eq("_id", playerId)).first()
).map(result -> result.getInteger("coins")).defaultIfEmpty(0);
}
@Override
public Mono<@NotNull Void> setCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.from(
getCollection().updateOne(
Filters.eq("_id", playerId),
Updates.set("coins", amount),
new UpdateOptions().upsert(true)
)
).then();
}
@Override
public Mono<@NotNull Void> depositCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.from(getCollection().updateOne(
Filters.eq("_id", playerId),
Updates.inc("coins", amount),
new UpdateOptions().upsert(true)
)).then();
}
@Override
public Mono<@NotNull Boolean> tryWithdrawCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.from(getCollection().updateOne(
Filters.and(
Filters.eq("_id", playerId),
Filters.gte("coins", amount)
),
Updates.inc("coins", -1 * amount)
)).map(result -> result.getModifiedCount() > 0);
}
private MongoCollection<Document> getCollection() {
return database.getCollection(COLLECTION_NAME);
}
}

View file

@ -1,46 +1,46 @@
package de.kentoj.scrow.bukkit.economy;
import com.google.common.base.Preconditions;
import de.kentoj.scrow.bukkit.EconomyService;
import de.kentoj.scrowlib.connection.DatabaseConnectionFactory;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.misc.EconomyService;
import org.jdbi.v3.core.Jdbi;
import java.util.UUID;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
// TODO log operations to separate table
public class PostgresEconomyService implements EconomyService {
private final DatabaseConnectionFactory conFactory;
private final Jdbi jdbi;
public PostgresEconomyService(DatabaseConnectionFactory conFactory) {
this.conFactory = conFactory;
conFactory.create().sql("""
public PostgresEconomyService() {
checkNotNull(ScrowAPI.getJdbi());
this.jdbi = ScrowAPI.getJdbi();
jdbi.withHandle(h -> h.execute("""
CREATE TABLE IF NOT EXISTS economy (
playerId UUID NOT NULL,
balance INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(playerId)
)
""").then().block();
"""));
}
@Override
public Mono<@NotNull Integer> getCoins(UUID playerId) {
return conFactory.create()
.sql("""
SELECT balance
FROM economy
WHERE playerId = :playerId
""")
public int getCoins(UUID playerId) {
return jdbi.withHandle(h -> h
.createQuery("SELECT balance FROM economy WHERE playerId = :playerId")
.bind("playerId", playerId)
.map(row -> row.get("balance", Integer.class))
.one();
.map(row -> row.getColumn("balance", Integer.class))
.findOne()
.orElse(0));
}
@Override
public Mono<@NotNull Void> setCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return conFactory.create().sql("""
public void setCoins(UUID playerId, int amount) {
checkArgument(amount >= 0, "amount cannot be negative");
jdbi.withHandle(h -> h
.createUpdate("""
INSERT INTO economy (playerId, balance)
VALUES (:playerId, :amount)
ON CONFLICT (playerId)
@ -50,13 +50,14 @@ public class PostgresEconomyService implements EconomyService {
""")
.bind("playerId", playerId)
.bind("amount", amount)
.then();
.execute());
}
@Override
public Mono<@NotNull Void> depositCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return conFactory.create().sql("""
public void depositCoins(UUID playerId, int amount) {
checkArgument(amount >= 0, "amount cannot be negative");
jdbi.withHandle(h -> h.
createUpdate("""
INSERT INTO economy (playerId, balance)
VALUES (:playerId, :amount)
ON CONFLICT (playerId)
@ -66,21 +67,20 @@ public class PostgresEconomyService implements EconomyService {
""")
.bind("playerId", playerId)
.bind("amount", amount)
.then();
.execute());
}
@Override
public Mono<@NotNull Boolean> tryWithdrawCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return conFactory.create().sql("""
public boolean tryWithdrawCoins(UUID playerId, int amount) {
checkArgument(amount >= 0, "amount cannot be negative");
return jdbi.withHandle(h -> h
.createUpdate("""
UPDATE economy
SET balance = balance - :amount
WHERE playerId = :playerId
""")
.bind("playerId", playerId)
.bind("amount", amount)
.fetch()
.rowsUpdated()
.map(updated -> updated > 0);
.execute() > 0);
}
}

View file

@ -0,0 +1,56 @@
package de.kentoj.scrow.bukkit.economy.command;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.node.RootCommandNode;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.ScrowAPISurface;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@NoArgsConstructor(access = AccessLevel.NONE)
public class CoinsCommand implements CommandExecutor<CommandSender> {
@Getter
private final RootCommandNode<CommandSender> rootNode;
private final MessageStyle style;
public CoinsCommand() {
style = new ScrowMessageStyle("Coins", NamedTextColor.GOLD);
rootNode = CommandNode.rootNode(style, "coins", "bal", "balance");
rootNode.setPermission("command.coins.get.self");
var setLiteral = new CoinsSetLiteral(style).getLiteral();
setLiteral.setPermission("command.coins.set");
rootNode.addLiteral(setLiteral);
var getLiteral = new CoinsGetLiteral(style).getLiteral();
getLiteral.setPermission("command.coins.get.others");
rootNode.addLiteral(getLiteral);
}
@Override
public CompletableFuture<Result<Object, String>> execute(CommandContext<CommandSender> ctx) {
var player = ((Player) ctx.getSender());
return Tasks.supplyAsync(() ->
ScrowAPI.getEconomyService().getCoins(player.getUniqueId()))
.<Result<Object, String>>mapSync(amount -> {
player.sendMessage(style.ok("You have " + amount + "$"));
return Results.success(Optional.empty());
}).asFuture();
}
}

View file

@ -0,0 +1,50 @@
package de.kentoj.scrow.bukkit.economy.command;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgument;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.kencommandapi.type.OfflinePlayerArgumentType;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import lombok.Getter;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class CoinsGetLiteral implements CommandExecutor<CommandSender> {
@Getter
private final CommandNode<CommandSender> literal;
private final MessageStyle style;
private final CommandArgument<CommandSender, OfflinePlayer> playerArg;
public CoinsGetLiteral(MessageStyle style) {
this.style = style;
playerArg = CommandArgument.arg("player", OfflinePlayerArgumentType.getInstance());
playerArg.setDefaultValueProvider(sender -> (OfflinePlayer) sender);
literal = CommandNode.node("set");
literal.addArgument(playerArg);
literal.setExecutor(this);
}
@Override
public CompletableFuture<Result<Object, String>> execute(CommandContext<CommandSender> ctx) {
var player = ctx.getArg(playerArg);
return Tasks.supplyAsync(() ->
ScrowAPI.getEconomyService().getCoins(player.getUniqueId()))
.<Result<Object, String>>mapSync(amount -> {
ctx.getSender().sendMessage(style.ok(player.getName() + " has " + amount + "$"));
return Results.success(Optional.empty());
}).asFuture();
}
}

View file

@ -0,0 +1,53 @@
package de.kentoj.scrow.bukkit.economy.command;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgument;
import de.kentoj.kencommandapi.api.argument.types.IntegerArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.kencommandapi.type.OfflinePlayerArgumentType;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import lombok.Getter;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class CoinsSetLiteral implements CommandExecutor<CommandSender> {
@Getter
private final CommandNode<CommandSender> literal;
private final MessageStyle style;
private final CommandArgument<CommandSender, OfflinePlayer> playerArg;
private final CommandArgument<CommandSender, Integer> amountArg;
public CoinsSetLiteral(MessageStyle style) {
this.style = style;
playerArg = CommandArgument.arg("player", OfflinePlayerArgumentType.getInstance());
amountArg = CommandArgument.arg("amount", new IntegerArgumentType<>());
literal = CommandNode.node("set");
literal.addArgument(playerArg);
literal.addArgument(amountArg);
literal.setExecutor(this);
}
@Override
public CompletableFuture<Result<Object, String>> execute(CommandContext<CommandSender> ctx) {
var player = ctx.getArg(playerArg);
var amount = ctx.getArg(amountArg);
return Tasks.runAsync(() ->
ScrowAPI.getEconomyService().setCoins(player.getUniqueId(), amount))
.<Result<Object, String>>supplySync(() -> {
ctx.getSender().sendMessage(style.ok(player.getName() + " now has " + amount + "$"));
return Results.success(Optional.empty());
}).asFuture();
}
}

View file

@ -36,7 +36,7 @@ public class FriendAddLiteral implements CommandExecutor<CommandSender> {
}
@Override
public CompletableFuture<Result<Void,String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object,String>> execute(CommandContext<CommandSender> ctx) {
var self = (Player) ctx.getSender();
var other = ctx.getArg(playerArg);
return findAction(self, other)

View file

@ -3,20 +3,16 @@ package de.kentoj.scrow.bukkit.friends.command;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.node.RootCommandNode;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
@NoArgsConstructor(access = AccessLevel.NONE)
public class FriendCommand {
@Getter
private final RootCommandNode<CommandSender> rootNode;
public FriendCommand() {
var style = new ScrowMessageStyle(Component.text("Friends")
.color(NamedTextColor.GREEN));
this.rootNode = CommandNode.rootNode(style, "f", "friend");
public static RootCommandNode<CommandSender> create() {
var style = new ScrowMessageStyle("Friends", NamedTextColor.GREEN);
var rootNode = CommandNode.rootNode(style, "f", "friend");
rootNode.addLiteral(new FriendListLiteral(style).getRootNode());
rootNode.addLiteral(new FriendAddLiteral(style).getRootNode());
rootNode.addLiteral(new FriendRemoveLiteral(style).getRootNode());

View file

@ -1,12 +1,15 @@
package de.kentoj.scrow.bukkit.friends.command;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.api.Results;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.scrow.bukkit.ScrowAPI;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
@ -18,34 +21,42 @@ public class FriendListLiteral implements CommandExecutor<CommandSender> {
@Getter
private final CommandNode<CommandSender> rootNode;
private final MessageStyle style;
public FriendListLiteral() {
public FriendListLiteral(MessageStyle style) {
this.style = style;
this.rootNode = CommandNode.node("list");
rootNode.setExecutor(this);
}
@Override
public CompletableFuture<Result<Void,String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object, String>> execute(CommandContext<CommandSender> ctx) {
var sender = (Player) ctx.getSender();
var ownUid = sender.getUniqueId();
return ScrowAPI.getFriendshipService().getFriendships(ownUid)
.map(friendship -> Bukkit.getOfflinePlayer(friendship.getUuids().getOther(ownUid)))
.sort(this::compareByOnlineStatus)
.collectList()
.map(list -> {
if (list.isEmpty()) return Result.error("You have no friends. Use /friend add <player>");
return CompletableFuture.supplyAsync(() ->
ScrowAPI.getFriendshipService().getFriendships(ownUid))
.thenApply(friendships -> ScrowAPI.getScheduler().sync(() -> {
var list = friendships.stream()
.map(friendship -> Bukkit.getOfflinePlayer(friendship.getUuids().getOther(ownUid)))
.sorted(this::compareByOnlineStatus)
.toList();
if (list.isEmpty()) return Results.failure("You have no friends. Use /friend add <player>");
var msg = "§3§lFriends§r§7 ☆ You have §3" + list.size() + "§7 friend";
if (list.size() > 1) msg += "s";
msg += ":§r\n";
var message = "§7You have §3" + list.size() + "§7" + ("§" +
.append(Component.text(list.size() == 1 ? " friend:" : " friends:"));
for (var friend : list) {
var status = friend.isOnline() ? "(§aonline§7)" : "(offline)";
msg += "§c≫ §7" + friend.getName() + " " + status + "\n";
message = message.appendNewline();
var status = Component.text("(").append(
friend.isOnline()
? Component.text("online").color(NamedTextColor.GRAY)
: Component.text("offline").color(NamedTextColor.RED))
.append(Component.text(")"));
message = message.append(Component.text("").color(NamedTextColor.AQUA));
}
sender.sendMessage(msg);
return Result.success();
}).toFuture();
});
}
private int compareByOnlineStatus(OfflinePlayer p1, OfflinePlayer p2) {

View file

@ -1,18 +1,20 @@
package de.kentoj.scrow.bukkit.friends.command;
import de.kentoj.kencommandapi.api.node.CommandNode;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgument;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.invocation.CommandExecutor;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.api.Results;
import de.kentoj.kencommandapi.api.node.CommandNode;
import de.kentoj.kencommandapi.type.OfflinePlayerArgumentType;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import lombok.Getter;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class FriendRemoveLiteral implements CommandExecutor<CommandSender> {
@ -20,6 +22,8 @@ public class FriendRemoveLiteral implements CommandExecutor<CommandSender> {
private final CommandNode<CommandSender> rootNode;
private final CommandArgument<CommandSender, OfflinePlayer> playerArg;
private final Scheduler scheduler = ScrowAPI.getScheduler();
public FriendRemoveLiteral() {
this.playerArg = CommandArgument.arg("player", OfflinePlayerArgumentType.getInstance());
this.rootNode = CommandNode.node("remove");
@ -28,15 +32,16 @@ public class FriendRemoveLiteral implements CommandExecutor<CommandSender> {
}
@Override
public CompletableFuture<Result<Void,String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object, String>> execute(CommandContext<CommandSender> ctx) {
var friend = ctx.getArg(playerArg);
var player = (Player) ctx.getSender();
return ScrowAPI.getFriendshipService().deleteFriendship(friend.getUniqueId(), player.getUniqueId())
.map(wasAdded -> wasAdded
? Result.success() : Result.error("You are not friends with " + friend.getName())
).doOnNext(success -> {
if (success.isSuccess())
ctx.getSender().sendMessage("You are no longer friends with " + friend.getName());
}).toFuture();
return Tasks.supplyAsync(() ->
ScrowAPI.getFriendshipService().deleteFriendship(friend.getUniqueId(), player.getUniqueId()))
.<Result<Object, String>>mapSync(deleted -> {
if (!deleted)
return Results.failure("You are not friends with " + friend.getName());
ctx.getSender().sendMessage("You have ended the friendship with " + friend.getName());
return Results.success(Optional.empty());
}).asFuture();
}
}

View file

@ -24,7 +24,7 @@ public class FriendRequestsLiteral implements CommandExecutor<CommandSender> {
}
@Override
public CompletableFuture<Result<Void,String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object,String>> execute(CommandContext<CommandSender> ctx) {
var sender = (Player) ctx.getSender();
return ScrowAPI.getFriendRequestService().getFriendRequestsTo(sender.getUniqueId())
.collectList()

View file

@ -1,13 +1,13 @@
package de.kentoj.scrow.bukkit.friends.friendship;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.scrow.bukkit.friends.Friendship;
import de.kentoj.scrow.bukkit.friends.FriendshipService;
import de.kentoj.scrow.bukkit.friends.OrderedUUIDPair;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -16,24 +16,27 @@ public class InMemoryFriendshipService implements FriendshipService {
private final Map<OrderedUUIDPair, Friendship> friendships = new HashMap<>();
@Override
public Mono<@NotNull Friendship> getFriendship(UUID uuid1, UUID uuid2) {
return Mono.justOrEmpty(friendships.get(new OrderedUUIDPair(uuid1, uuid2)));
public Result<Friendship, String> getFriendship(UUID uuid1, UUID uuid2) {
return Results.success(friendships.get(new OrderedUUIDPair(uuid1, uuid2)));
}
@Override
public Flux<@NotNull Friendship> getFriendships(UUID playerId) {
return Flux.fromStream(friendships.keySet().stream()
.filter(pair -> pair.getFirst().equals(playerId) || pair.getSecond().equals(playerId))
.map(friendships::get));
public Result<List<Friendship>, String> getFriendships(UUID playerId) {
return Results.success(friendships.entrySet().stream()
.filter(ent ->
ent.getKey().getFirst().equals(playerId) || ent.getKey().getSecond().equals(playerId)
)
.map(Map.Entry::getValue)
.toList());
}
@Override
public Mono<@NotNull Boolean> saveFriendship(Friendship friendship) {
return Mono.just(friendships.putIfAbsent(friendship.getUuids(), friendship) == null);
public Result<Boolean, String> saveFriendship(Friendship friendship) {
return Results.success(friendships.putIfAbsent(friendship.getUuids(), friendship) == null);
}
@Override
public Mono<@NotNull Boolean> deleteFriendship(UUID uuid1, UUID uuid2) {
return Mono.just(friendships.remove(new OrderedUUIDPair(uuid1, uuid2)) != null);
public Result<Boolean, String> deleteFriendship(UUID uuid1, UUID uuid2) {
return Results.success(friendships.remove(new OrderedUUIDPair(uuid1, uuid2)) != null);
}
}

View file

@ -1,103 +0,0 @@
package de.kentoj.scrow.bukkit.friends.friendship;
import com.mongodb.ErrorCategory;
import com.mongodb.MongoWriteException;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import de.kentoj.scrow.bukkit.friends.Friendship;
import de.kentoj.scrow.bukkit.friends.FriendshipService;
import de.kentoj.scrow.bukkit.friends.OrderedUUIDPair;
import org.bson.Document;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.sql.Date;
import java.util.UUID;
public class MongoFriendshipService implements FriendshipService {
private static final String COLLECTION_NAME = "friendships";
private final MongoDatabase database;
public MongoFriendshipService(MongoDatabase database) {
this.database = database;
Mono.when(
database.createCollection(COLLECTION_NAME),
collection().createIndex(
Indexes.compoundIndex(
Indexes.ascending("first"),
Indexes.ascending("second")
),
new IndexOptions().unique(true))
).block();
}
@Override
public Mono<@NotNull Friendship> getFriendship(UUID uuid1, UUID uuid2) {
var uuids = new OrderedUUIDPair(uuid1, uuid2);
return Mono.from(collection()
.find(Filters.and(
Filters.eq("first", uuids.getFirst()),
Filters.eq("second", uuids.getSecond())
)))
.map(this::fromDocument);
}
@Override
public Flux<@NotNull Friendship> getFriendships(UUID playerId) {
return Flux.from(collection()
.find(Filters.or(
Filters.eq("first", playerId),
Filters.eq("second", playerId)
)))
.map(this::fromDocument);
}
@Override
public Mono<@NotNull Boolean> saveFriendship(Friendship friendship) {
return Mono.from(collection()
.insertOne(toDocument(friendship)))
.thenReturn(true)
.onErrorResume(MongoWriteException.class, e ->
e.getError().getCategory() == ErrorCategory.DUPLICATE_KEY
? Mono.just(Boolean.FALSE)
: Mono.error(e)
);
}
@Override
public Mono<@NotNull Boolean> deleteFriendship(UUID uuid1, UUID uuid2) {
var uuids = new OrderedUUIDPair(uuid1, uuid2);
return Mono.from(collection()
.deleteOne(Filters.and(
Filters.eq("first", uuids.getFirst()),
Filters.eq("second", uuids.getSecond())
)))
.map(res -> res.getDeletedCount() > 0);
}
private MongoCollection<Document> collection() {
return database.getCollection(COLLECTION_NAME);
}
private Friendship fromDocument(Document doc) {
return new FriendshipImpl(
doc.get("first", UUID.class),
doc.get("second", UUID.class),
doc.getDate("createdAt").toInstant()
);
}
private Document toDocument(Friendship friendship) {
return new Document()
.append("first", friendship.getUuids().getFirst())
.append("second", friendship.getUuids().getSecond())
.append("createdAt", Date.from(friendship.getCreatedAt()));
}
}

View file

@ -30,7 +30,7 @@ public class PostgresFriendshipService implements FriendshipService {
}
@Override
public Mono<@NotNull Friendship> getFriendship(UUID uuid1, UUID uuid2) {
public Result<Friendship, String> getFriendship(UUID uuid1, UUID uuid2) {
var uuids = new OrderedUUIDPair(uuid1, uuid2);
return dbConFactory.create().sql("""
@ -49,7 +49,7 @@ public class PostgresFriendshipService implements FriendshipService {
}
@Override
public Flux<@NotNull Friendship> getFriendships(UUID playerId) {
public Result<List<Friendship>, String> getFriendships(UUID playerId) {
return dbConFactory.create().sql("""
SELECT first_uuid, second_uuid, created_at
FROM friendships
@ -65,7 +65,7 @@ public class PostgresFriendshipService implements FriendshipService {
}
@Override
public Mono<@NotNull Boolean> saveFriendship(Friendship friendship) {
public Result<Boolean, String> saveFriendship(Friendship friendship) {
return dbConFactory.create().sql("""
INSERT INTO friendships (first_uuid, second_uuid, created_at)
VALUES (:first, :second, :createdAt)
@ -80,7 +80,7 @@ public class PostgresFriendshipService implements FriendshipService {
}
@Override
public Mono<@NotNull Boolean> deleteFriendship(UUID uuid1, UUID uuid2) {
public Result<Boolean, String> deleteFriendship(UUID uuid1, UUID uuid2) {
var uuids = new OrderedUUIDPair(uuid1, uuid2);
return dbConFactory.create().sql("""

View file

@ -2,11 +2,9 @@ package de.kentoj.scrow.bukkit.friends.request;
import de.kentoj.scrow.bukkit.friends.FriendRequest;
import de.kentoj.scrow.bukkit.friends.FriendRequestService;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@ -15,34 +13,34 @@ public class InMemoryFriendRequestService implements FriendRequestService {
private final Set<FriendRequest> requests = new HashSet<>();
@Override
public Flux<@NotNull FriendRequest> getFriendRequestsTo(UUID playerId) {
return Flux.fromStream(requests.stream()
.filter(req -> req.getTo().equals(playerId)));
public List<FriendRequest> getFriendRequestsTo(UUID playerId) {
return requests.stream()
.filter(req -> req.getTo().equals(playerId))
.toList();
}
@Override
public Flux<@NotNull FriendRequest> getFriendRequestsFrom(UUID playerId) {
return Flux.fromStream(requests.stream()
.filter(req -> req.getFrom().equals(playerId)));
public List<FriendRequest> getFriendRequestsFrom(UUID playerId) {
return requests.stream()
.filter(req -> req.getFrom().equals(playerId))
.toList();
}
@Override
public Mono<@NotNull FriendRequest> getFriendRequest(UUID from, UUID to) {
return Mono.justOrEmpty(requests.stream()
.filter(req -> req.getFrom().equals(from) && req.getTo().equals(to))
.findFirst()
.orElse(null));
public FriendRequest getFriendRequest(UUID from, UUID to) {
return requests.stream()
.filter(req -> req.getFrom().equals(from) && req.getTo().equals(to))
.findFirst()
.orElse(null);
}
@Override
public Mono<@NotNull Boolean> saveFriendRequest(FriendRequest friendRequest) {
return getFriendRequest(friendRequest.getFrom(), friendRequest.getTo())
.map(__ -> false)
.switchIfEmpty(Mono.fromRunnable(() -> requests.add(friendRequest)));
public boolean saveFriendRequest(FriendRequest friendRequest) {
return requests.add(friendRequest);
}
@Override
public Mono<@NotNull Boolean> deleteFriendRequest(UUID from, UUID to) {
return Mono.just(requests.removeIf(req -> req.getFrom().equals(from) && req.getTo().equals(to)));
public boolean deleteFriendRequest(UUID from, UUID to) {
return requests.removeIf(req -> req.getFrom().equals(from) && req.getTo().equals(to));
}
}

View file

@ -1,107 +0,0 @@
package de.kentoj.scrow.bukkit.friends.request;
import com.mongodb.ErrorCategory;
import com.mongodb.MongoWriteException;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import de.kentoj.scrow.bukkit.friends.FriendRequest;
import de.kentoj.scrow.bukkit.friends.FriendRequestService;
import org.bson.Document;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Date;
import java.util.UUID;
public class MongoFriendRequestService implements FriendRequestService {
private static final String COLLECTION_NAME = "friend-requests";
private final MongoDatabase database;
public MongoFriendRequestService(MongoDatabase database) {
this.database = database;
Mono.when(
database.createCollection(COLLECTION_NAME),
collection().createIndex(
Indexes.compoundIndex(
Indexes.ascending("from"),
Indexes.ascending("to")
),
new IndexOptions().unique(true)
)
).block();
}
@Override
public Mono<@NotNull FriendRequest> getFriendRequest(UUID from, UUID to) {
return Mono.from(collection()
.find(Filters.and(
Filters.eq("from", from),
Filters.eq("to", to)
)))
.map(this::fromDocument);
}
@Override
public Mono<@NotNull Boolean> saveFriendRequest(FriendRequest friendRequest) {
return Mono.from(collection()
.insertOne(toDocument(friendRequest))
)
.map(__ -> true)
.onErrorResume(MongoWriteException.class, e ->
e.getError().getCategory() == ErrorCategory.DUPLICATE_KEY ?
Mono.just(false) :
Mono.error(e)
);
}
@Override
public Flux<@NotNull FriendRequest> getFriendRequestsTo(UUID playerId) {
return Flux.from(collection()
.find(Filters.eq("to", playerId)))
.map(this::fromDocument);
}
@Override
public Flux<@NotNull FriendRequest> getFriendRequestsFrom(UUID playerId) {
return Flux.from(collection()
.find()
.filter(Filters.eq("from", playerId)))
.map(this::fromDocument);
}
@Override
public Mono<@NotNull Boolean> deleteFriendRequest(UUID from, UUID to) {
return Mono.from(collection()
.deleteOne(Filters.and(
Filters.eq("from", from),
Filters.eq("to", to)
)))
.map(res -> res.getDeletedCount() > 0);
}
private MongoCollection<Document> collection() {
return database.getCollection(COLLECTION_NAME);
}
private Document toDocument(FriendRequest req) {
return new Document()
.append("from", req.getFrom())
.append("to", req.getTo())
.append("createdAt", Date.from(req.getCreatedAt()));
}
private FriendRequest fromDocument(Document doc) {
return new FriendRequestImpl(
doc.get("from", UUID.class),
doc.get("to", UUID.class),
doc.getDate("createdAt").toInstant()
);
}
}

View file

@ -31,7 +31,7 @@ public class PostgresFriendRequestService implements FriendRequestService {
}
@Override
public Mono<@NotNull FriendRequest> getFriendRequest(UUID from, UUID to) {
public Result<FriendRequest, String> getFriendRequest(UUID from, UUID to) {
return conFactory.create().sql("""
SELECT *
FROM friend-requests
@ -44,7 +44,7 @@ public class PostgresFriendRequestService implements FriendRequestService {
}
@Override
public Mono<@NotNull Boolean> saveFriendRequest(FriendRequest friendRequest) {
public Result<Boolean, String> saveFriendRequest(FriendRequest friendRequest) {
return conFactory.create().sql("""
INSERT INTO friend-requests (from_uuid, to_uuid, created_at)
VALUES (:from, :to, :created_at)
@ -57,7 +57,7 @@ public class PostgresFriendRequestService implements FriendRequestService {
}
@Override
public Flux<@NotNull FriendRequest> getFriendRequestsTo(UUID playerId) {
public Result<List<FriendRequest>, String> getFriendRequestsTo(UUID playerId) {
return conFactory.create().sql("""
SELECT *
FROM friend-requests
@ -69,7 +69,7 @@ public class PostgresFriendRequestService implements FriendRequestService {
}
@Override
public Flux<@NotNull FriendRequest> getFriendRequestsFrom(UUID playerId) {
public Result<List<FriendRequest>, String> getFriendRequestsFrom(UUID playerId) {
return conFactory.create().sql("""
SELECT *
FROM friend-requests
@ -81,7 +81,7 @@ public class PostgresFriendRequestService implements FriendRequestService {
}
@Override
public Mono<@NotNull Boolean> deleteFriendRequest(UUID from, UUID to) {
public Result<Boolean, String> deleteFriendRequest(UUID from, UUID to) {
return conFactory.create().sql("""
DELETE
FROM friend-requests

View file

@ -1,7 +1,7 @@
package de.kentoj.scrow.bukkit.playermanager;
import com.leakyabstractions.result.api.Result;
import de.kentoj.scrow.bukkit.PlayerManager;
import de.kentoj.scrow.bukkit.misc.PlayerManager;
import de.kentoj.scrowlib.messaging.MessageBroker;
import lombok.RequiredArgsConstructor;
import org.bson.Document;
@ -14,7 +14,7 @@ public class PlayerManagerImpl implements PlayerManager {
private final MessageBroker messageBroker;
@Override
public Result<Void, String> sendPlayer(UUID playerId, String instanceHandle) {
public Result<Object, String> sendPlayer(UUID playerId, String instanceHandle) {
return messageBroker.request("players.send", new Document()
.append("playerId", playerId)
.append("handle", instanceHandle))

View file

@ -22,7 +22,7 @@ public class LobbyCommand implements CommandExecutor<CommandSender> {
}
@Override
public CompletableFuture<Result<Void, String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object, String>> execute(CommandContext<CommandSender> ctx) {
Bukkit.dispatchCommand(ctx.getSender(), "play lobby");
return CompletableFuture.completedFuture(Results.success(null));
}

View file

@ -28,7 +28,7 @@ public class PlayCommand implements CommandExecutor<CommandSender> {
}
@Override
public CompletableFuture<Result<Void, String>> execute(CommandContext<CommandSender> ctx) {
public CompletableFuture<Result<Object, String>> execute(CommandContext<CommandSender> ctx) {
var gamemode = ctx.getArg(gamemodeArg);
var player = (Player) ctx.getSender();
return CompletableFuture.supplyAsync(() -> ScrowAPI.getInstanceManager().getInstances()