This commit is contained in:
kento2 2026-02-23 20:14:08 +01:00
parent f1e9ac5ea9
commit 179f94085f
15 changed files with 167 additions and 67 deletions

View file

@ -1,3 +1,7 @@
TODO
- separate error handling from processing
- tab completion
environment environment
----------- -----------

View file

@ -3,14 +3,13 @@ package de.kentoj.scrow.bukkit;
import com.mongodb.reactivestreams.client.MongoDatabase; import com.mongodb.reactivestreams.client.MongoDatabase;
import de.kentoj.scrow.bukkit.services.EconomyService; import de.kentoj.scrow.bukkit.services.EconomyService;
import de.kentoj.scrow.bukkit.services.command.CommandManager; import de.kentoj.scrow.bukkit.services.command.CommandManager;
import de.kentoj.scrow.bukkit.services.friends.FriendsService;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ScrowAPI { public class ScrowAPI {
@ -18,11 +17,13 @@ public class ScrowAPI {
@Getter @Getter
private static @Nullable MongoDatabase database; private static @Nullable MongoDatabase database;
@Getter @Getter
private static EconomyService economyService;
@Getter
private static CommandManager commandManager; private static CommandManager commandManager;
@Getter @Getter
private static Scheduler minecraftScheduler; private static Scheduler minecraftScheduler;
@Getter
private static EconomyService economyService;
@Getter
private static FriendsService friendsService;
@ApiStatus.Internal @ApiStatus.Internal
public static void setDatabase(@Nullable MongoDatabase database) { public static void setDatabase(@Nullable MongoDatabase database) {
@ -43,5 +44,10 @@ public class ScrowAPI {
public static void setMinecraftScheduler(Scheduler minecraftScheduler) { public static void setMinecraftScheduler(Scheduler minecraftScheduler) {
ScrowAPI.minecraftScheduler = minecraftScheduler; ScrowAPI.minecraftScheduler = minecraftScheduler;
} }
@ApiStatus.Internal
public static void setFriendsService(FriendsService friendsService) {
ScrowAPI.friendsService = friendsService;
}
} }

View file

@ -1,6 +1,8 @@
package de.kentoj.scrow.bukkit.services.command.execution; package de.kentoj.scrow.bukkit.services.command.execution;
import de.kentoj.scrow.bukkit.services.command.exception.CommandInvocationException;
public interface CommandExecutor { public interface CommandExecutor {
void execute(CommandContext ctx); void execute(CommandContext ctx) throws CommandInvocationException;
} }

View file

@ -57,7 +57,7 @@ public class PlayerArgumentType {
try { try {
player = Bukkit.getOfflinePlayer(UUID.fromString(rawInput)); player = Bukkit.getOfflinePlayer(UUID.fromString(rawInput));
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
throw new CommandInvocationException("Not a UUID or name of an online player"); throw new CommandInvocationException("Not a UUID or name of an online player -- " + rawInput);
} }
} }
return player; return player;
@ -75,6 +75,7 @@ public class PlayerArgumentType {
@Override @Override
public void checkValue(CommandContext ctx, OfflinePlayer value, String rawInput) throws CommandInvocationException { public void checkValue(CommandContext ctx, OfflinePlayer value, String rawInput) throws CommandInvocationException {
if (!value.hasPlayedBefore()) throw new CommandInvocationException("Player has never played before -- " + rawInput);
} }
}; };

View file

@ -0,0 +1,12 @@
package de.kentoj.scrow.bukkit.services.friends;
public class FriendRequestUsageException extends RuntimeException {
public FriendRequestUsageException(String message) {
super(message);
}
public FriendRequestUsageException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,33 @@
package de.kentoj.scrow.bukkit.services.friends;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.UUID;
public interface FriendsService {
Flux<@NotNull Friendship> getFriendships(UUID playerId);
/**
* @param initiator id of player who wished to end the friendship
* @param other id of the player the initiator wants to end the friendship with
* @throws FriendRequestUsageException
*/
Mono<@NotNull Boolean> removeFriend(UUID initiator, UUID other);
/**
* @param playerId
*/
Flux<@NotNull FriendRequest> getIncomingFriendRequests(UUID playerId);
Flux<@NotNull FriendRequest> getOutgoingFriendRequests(UUID playerId);
/**
* @param from id of player sending the friend request
* @param to id of the player the initiator wants to befriend
* @throws FriendRequestUsageException
*/
Mono<@NotNull Friendship> sendFriendRequest(UUID from, UUID to);
}

View file

@ -8,4 +8,9 @@ import java.util.UUID;
public interface Friendship { public interface Friendship {
Tuple2<UUID, UUID> getPlayerIds(); Tuple2<UUID, UUID> getPlayerIds();
Instant getCreatedAt(); Instant getCreatedAt();
default UUID getOther(UUID self) {
final var ids = getPlayerIds();
return ids.getFirst() != self ? ids.getFirst() : ids.getSecond();
}
} }

View file

@ -1,6 +1,7 @@
package de.kentoj.scrow.bukkit; package de.kentoj.scrow.bukkit;
import de.kentoj.scrow.bukkit.economy.command.CoinsCommand; import de.kentoj.scrow.bukkit.economy.CoinsCommand;
import de.kentoj.scrow.bukkit.friends.command.FriendCommand;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -14,6 +15,7 @@ public class CoreImplPlugin extends JavaPlugin {
{ {
ScrowAPI.getCommandManager().register(new CoinsCommand().create(), this); ScrowAPI.getCommandManager().register(new CoinsCommand().create(), this);
ScrowAPI.getCommandManager().register(new FriendCommand().create(), this);
} }
{ {
@ -33,9 +35,4 @@ public class CoreImplPlugin extends JavaPlugin {
} }
ScrowAPISurface.destroyScrowAPI(); ScrowAPISurface.destroyScrowAPI();
} }
@Override
public void onDisable() {
}
} }

View file

@ -47,7 +47,7 @@ public class ScrowAPISurface {
ScrowAPI.getDatabase() != null ? ScrowAPI.getDatabase() != null ?
new MongoEconomyService(ScrowAPI.getDatabase()) : new InMemoryEconomyService() new MongoEconomyService(ScrowAPI.getDatabase()) : new InMemoryEconomyService()
); );
// TODO friends service
Commands.setFactory(new CommandFactoryImpl()); Commands.setFactory(new CommandFactoryImpl());
ScrowAPI.setCommandManager(new BukkitCommandManager()); ScrowAPI.setCommandManager(new BukkitCommandManager());
} }

View file

@ -27,7 +27,7 @@ public class ArgumentReaderImpl implements ArgumentReader {
final var str = new StringBuilder(); final var str = new StringBuilder();
while (true) { while (true) {
if (buf.position() >= buf.limit()) break; if (buf.position() >= buf.limit()) break;
char c = buf.get(); final char c = buf.get();
if (c == ' ') break; if (c == ' ') break;
str.append(c); str.append(c);
} }
@ -40,7 +40,7 @@ public class ArgumentReaderImpl implements ArgumentReader {
var quoting = false; var quoting = false;
while (true) { while (true) {
if (buf.position() >= buf.limit()) break; if (buf.position() >= buf.limit()) break;
char c = buf.get(); final char c = buf.get();
if (c == '"') quoting = !quoting; if (c == '"') quoting = !quoting;
if (c == ' ' && !quoting) break; if (c == ' ' && !quoting) break;
str.append(c); str.append(c);

View file

@ -1,17 +1,18 @@
package de.kentoj.scrow.bukkit.cmds2.parsing; package de.kentoj.scrow.bukkit.cmds2.parsing;
import de.kentoj.scrow.bukkit.cmds2.CommandContextImpl; import de.kentoj.scrow.bukkit.cmds2.CommandContextImpl;
import de.kentoj.scrow.bukkit.services.command.literal.ArgumentReader; import de.kentoj.scrow.bukkit.services.command.exception.CommandInvocationException;
import de.kentoj.scrow.bukkit.services.command.execution.ArgumentData; import de.kentoj.scrow.bukkit.services.command.execution.ArgumentData;
import de.kentoj.scrow.bukkit.services.command.literal.ArgumentReader;
import de.kentoj.scrow.bukkit.services.command.literal.CommandArgument; import de.kentoj.scrow.bukkit.services.command.literal.CommandArgument;
import de.kentoj.scrow.bukkit.services.command.literal.Literal; import de.kentoj.scrow.bukkit.services.command.literal.Literal;
import de.kentoj.scrow.bukkit.services.command.literal.RootLiteral; import de.kentoj.scrow.bukkit.services.command.literal.RootLiteral;
import de.kentoj.scrow.bukkit.services.command.exception.CommandInvocationException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
public class CommandProcessor { public class CommandProcessor {
@ -22,28 +23,35 @@ public class CommandProcessor {
private final ArgumentReader argumentReader; private final ArgumentReader argumentReader;
public void execute(RootLiteral rootLiteral) { public void execute(RootLiteral rootLiteral) {
Literal lastLiteral = rootLiteral; Literal literal = rootLiteral;
do { do {
processArguments(lastLiteral); processArguments(literal);
if (argumentReader.isEOF()) { if (argumentReader.isEOF()) {
if (lastLiteral.getExecutor() == null) throw new CommandInvocationException("Incomplete command -- literal expected"); if (literal.getExecutor() == null) {
lastLiteral.getExecutor().execute(new CommandContextImpl(sender, arguments)); throw new CommandInvocationException("Incomplete command -- literal expected");
}
literal.getExecutor().execute(new CommandContextImpl(sender, arguments));
return; return;
} }
final var literalName = argumentReader.readWord(); final var literalName = argumentReader.readWord();
lastLiteral = lastLiteral.getLiteral(literalName); final var nextLiteral = literal.getLiteral(literalName);
if (lastLiteral == null) throw new CommandInvocationException("Unknown literal -- " + literalName); if (nextLiteral == null) {
final String validLiterals = literal.getLiterals().stream().map(Literal::getName).collect(Collectors.joining(", "));
throw new CommandInvocationException("Unknown literal -- " + literalName +
"\nValid literals: " + validLiterals);
}
literal = nextLiteral;
final boolean hasSenderPermission = lastLiteral.getRequiredPermission() == null || sender.hasPermission(lastLiteral.getRequiredPermission()); final boolean hasSenderPermission = literal.getRequiredPermission() == null || sender.hasPermission(literal.getRequiredPermission());
if (!hasSenderPermission) throw new CommandInvocationException("No permission"); if (!hasSenderPermission) throw new CommandInvocationException("No permission");
} while (lastLiteral.hasLiteral()); } while (literal.hasLiteral());
processArguments(lastLiteral); processArguments(literal);
if (lastLiteral.getExecutor() == null) throw new CommandInvocationException("Incomplete command."); if (literal.getExecutor() == null) throw new CommandInvocationException("Incomplete command.");
lastLiteral.getExecutor().execute(new CommandContextImpl(sender, arguments)); literal.getExecutor().execute(new CommandContextImpl(sender, arguments));
} }
private void processArguments(Literal node) { private void processArguments(Literal node) {
@ -55,13 +63,13 @@ public class CommandProcessor {
private <T> ArgumentData<T> parseArgument(CommandArgument<T> argument) { private <T> ArgumentData<T> parseArgument(CommandArgument<T> argument) {
if (argumentReader.isEOF()) { if (argumentReader.isEOF()) {
var defaultValue = argument.getDefaultValue(new CommandContextImpl(sender, arguments)); final var defaultValue = argument.getDefaultValue(new CommandContextImpl(sender, arguments));
if (defaultValue == null) throw new CommandInvocationException("Missing argument -- " + argument.getName()); if (defaultValue == null) throw new CommandInvocationException("Missing argument -- " + argument.getName());
return new ArgumentDataImpl<>(defaultValue, argument, null); return new ArgumentDataImpl<>(defaultValue, argument, null);
} }
final var rawInput = argument.getType().readRawInput(argumentReader); final var rawInput = argument.getType().readRawInput(argumentReader);
T value = argument.getType().parseInput(rawInput); final T value = argument.getType().parseInput(rawInput);
argument.getType().checkValue(new CommandContextImpl(sender, arguments), value, rawInput); argument.getType().checkValue(new CommandContextImpl(sender, arguments), value, rawInput);
return new ArgumentDataImpl<>(value, argument, rawInput); return new ArgumentDataImpl<>(value, argument, rawInput);
} }

View file

@ -1,4 +1,4 @@
package de.kentoj.scrow.bukkit.economy.command; package de.kentoj.scrow.bukkit.economy;
import de.kentoj.scrow.bukkit.ScrowAPI; import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.services.command.literal.CommandArgument; import de.kentoj.scrow.bukkit.services.command.literal.CommandArgument;

View file

@ -1,36 +0,0 @@
package de.kentoj.scrow.bukkit.friends;
import de.kentoj.scrow.bukkit.services.friends.FriendRequest;
import de.kentoj.scrow.bukkit.services.friends.Friendship;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.UUID;
public interface FriendshipService {
Flux<@NotNull Friendship> getFriendships(UUID playerId);
/**
* @param initiator id of player who wished to end the friendship
* @param other id of the player the initiator wants to end the friendship with
* @return true on success, false if no such friendship existed.
*/
Mono<@NotNull Boolean> endFriendship(UUID initiator, UUID other);
Flux<@NotNull FriendRequest> getIncomingFriendRequests(UUID playerId);
Flux<@NotNull FriendRequest> getOutgoingFriendRequests(UUID playerId);
/**
* @param initiator id of player sending the friend request
* @param to id of the player the initiator wants to befriend
* @return true on success, false if already requested
*/
Mono<@NotNull Boolean> sendFriendRequest(UUID initiator, UUID to);
Mono<@NotNull Friendship> acceptFriendRequest(UUID from, UUID to);
Mono<@NotNull Friendship> denyFriendRequest(UUID from, UUID to);
}

View file

@ -0,0 +1,20 @@
package de.kentoj.scrow.bukkit.friends.command;
import de.kentoj.scrow.bukkit.services.command.Commands;
import de.kentoj.scrow.bukkit.services.command.literal.RootLiteral;
public class FriendCommand {
private final RootLiteral rootLiteral;
public FriendCommand() {
rootLiteral = Commands.root("friend");
rootLiteral.addAliases("f", "friends");
rootLiteral.addLiteral(new FriendListLiteral().create());
}
public RootLiteral create() {
return rootLiteral;
}
}

View file

@ -0,0 +1,48 @@
package de.kentoj.scrow.bukkit.friends.command;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.services.command.Commands;
import de.kentoj.scrow.bukkit.services.command.execution.CommandContext;
import de.kentoj.scrow.bukkit.services.command.literal.Literal;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import reactor.core.scheduler.Schedulers;
import java.util.UUID;
public class FriendListLiteral {
private final Literal literal;
public FriendListLiteral() {
literal = Commands.literal("list");
literal.setExecutor(this::listFriends);
}
public Literal create() {
return literal;
}
private void listFriends(CommandContext ctx) {
ScrowAPI.getFriendsService().getFriendships(ctx.getSenderPlayer().getUniqueId())
.subscribeOn(Schedulers.boundedElastic())
.collectList()
.publishOn(ScrowAPI.getMinecraftScheduler())
.subscribe(friendships -> {
if (friendships.isEmpty()) {
ctx.getSender().sendMessage("You have no friends.");
} else {
ctx.getSender().sendMessage("You have " + friendships.size() + " friends:");
friendships.forEach(friendship -> {
final UUID friendId = friendship.getOther(ctx.getSenderPlayer().getUniqueId());
// TODO get name even if never online on the network
final OfflinePlayer friend = Bukkit.getOfflinePlayer(friendId);
ctx.getSender().sendMessage("- " + friend.getName());
});
}
}, error -> {
ctx.getSender().sendMessage("Error fetching friends");
// TODO improve handling
});
}
}