This commit is contained in:
kento2 2026-08-08 03:41:09 +02:00
parent 670eba418c
commit 13ced30434
122 changed files with 223 additions and 12 deletions

0
core/BUILD Normal file
View file

43
core/bukkit/BUILD Normal file
View file

@ -0,0 +1,43 @@
load("@rules_jvm_external//:defs.bzl", "artifact")
load("@rules_java//java:java_library.bzl", "java_library")
load("@rules_java//java:java_binary.bzl", "java_binary")
java_library(
name = "api",
srcs = glob(["api/main/java/**/*.java"]),
visibility = ["//visibility:public"],
deps = [
"//core/common",
artifact("io.papermc.paper:paper-api"),
artifact("de.kentoj.scrow:kencommandapi-bukkit"),
],
exports = [
"//core/common",
artifact("de.kentoj.scrow:kencommandapi-bukkit"),
artifact("org.mongodb:bson"),
],
)
java_binary(
name = "_plugin",
srcs = glob(["plugin/main/java/**/*.java"]),
create_executable = False,
resources = glob(["plugin/main/resources/**"]),
resource_strip_prefix = "core/bukkit/plugin/main/resources",
deps = [
":api",
artifact("net.luckperms:api"),
artifact("io.papermc.paper:paper-api"),
artifact("io.nats:jnats"),
artifact("net.kyori:adventure-key:5.2.0"),
artifact("org.postgresql:postgresql"),
],
)
genrule(
name = "plugin",
srcs = [":_plugin_deploy.jar"],
outs = ["plugin.jar"],
cmd = "cp $< $@",
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,172 @@
package de.kentoj.scrow.bukkit;
import de.kentoj.kencommandapi.CommandAPI;
import de.kentoj.scrow.bukkit.minigame.MinigameManager;
import de.kentoj.scrow.bukkit.region.RegionManager;
import de.kentoj.scrowlib.economy.EconomyService;
import de.kentoj.scrowlib.friends.FriendRequestService;
import de.kentoj.scrowlib.friends.FriendshipService;
import de.kentoj.scrowlib.instancemanager.InstanceManager;
import de.kentoj.scrowlib.messaging.MessageBroker;
import de.kentoj.scrowlib.player.NetworkPlayer;
import de.kentoj.scrowlib.player.NetworkPlayerFactory;
import de.kentoj.scrowlib.player.PlayerPrefixProvider;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.jdbi.v3.core.Jdbi;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class ScrowAPI {
public ScrowAPI() {
}
/**
* Might be null in testing environments
*/
private static @Nullable Jdbi jdbi;
@ApiStatus.Internal
public static Plugin plugin;
private static EconomyService economyService;
private static FriendshipService friendshipService;
private static FriendRequestService friendRequestService;
private static MessageBroker messageBroker;
private static InstanceManager instanceManager;
private static RegionManager regionManager;
private static CommandAPI<Player> playerCommands;
private static CommandAPI<CommandSender> commands;
private static PlayerPrefixProvider playerPrefixProvider;
private static final MinigameManager minigameManager = new MinigameManager();
private static NetworkPlayerFactory playerFactory;
public static NetworkPlayer getPlayer(OfflinePlayer player) {
return getPlayer(player.getUniqueId());
}
public static NetworkPlayer getPlayer(UUID uuid) {
return playerFactory.create(uuid);
}
@ApiStatus.Internal
public static void setEconomyService(EconomyService economyService) {
ScrowAPI.economyService = economyService;
}
@ApiStatus.Internal
public static void setCorePlugin(Plugin plugin) {
ScrowAPI.plugin = plugin;
}
@ApiStatus.Internal
public static void setFriendshipService(FriendshipService friendshipService) {
ScrowAPI.friendshipService = friendshipService;
}
@ApiStatus.Internal
public static void setFriendRequestService(FriendRequestService friendRequestService) {
ScrowAPI.friendRequestService = friendRequestService;
}
@ApiStatus.Internal
public static void setMessageBroker(MessageBroker messageBroker) {
ScrowAPI.messageBroker = messageBroker;
}
@ApiStatus.Internal
public static void setInstanceManager(InstanceManager instanceManager) {
ScrowAPI.instanceManager = instanceManager;
}
@ApiStatus.Internal
public static void setJdbi(@Nullable Jdbi jdbi) {
ScrowAPI.jdbi = jdbi;
}
@ApiStatus.Internal
public static void setRegionManager(RegionManager regionManager) {
ScrowAPI.regionManager = regionManager;
}
@ApiStatus.Internal
public static void setPlayerFactory(NetworkPlayerFactory playerFactory) {
ScrowAPI.playerFactory = playerFactory;
}
@ApiStatus.Internal
public static void setPlayerPrefixProvider(PlayerPrefixProvider playerPrefixProvider) {
ScrowAPI.playerPrefixProvider = playerPrefixProvider;
}
@ApiStatus.Internal
public static void setPlayerCommands(CommandAPI<Player> playerCommands) {
ScrowAPI.playerCommands = playerCommands;
}
@ApiStatus.Internal
public static void setCommands(CommandAPI<CommandSender> commands) {
ScrowAPI.commands = commands;
}
public static @Nullable Jdbi jdbi() {
return jdbi;
}
public static Plugin plugin() {
return plugin;
}
public static EconomyService economyService() {
return economyService;
}
public static FriendshipService friendshipService() {
return friendshipService;
}
public static FriendRequestService friendRequestService() {
return friendRequestService;
}
public static MessageBroker messageBroker() {
return messageBroker;
}
public static InstanceManager instanceManager() {
return instanceManager;
}
/**
* Regions are not a reliable enough to detect fast movements.
* The way regions work is that every few ticks, all regions are checked for players.
* @see org.bukkit.event.player.PlayerMoveEvent
*/
public static RegionManager regionManager() {
return regionManager;
}
public static CommandAPI<Player> playerCommands() {
return playerCommands;
}
public static CommandAPI<CommandSender> commands() {
return commands;
}
public static PlayerPrefixProvider playerPrefixProvider() {
return playerPrefixProvider;
}
public static NetworkPlayerFactory playerFactory() {
return playerFactory;
}
public static MinigameManager minigameManager() {
return minigameManager;
}
}

View file

@ -0,0 +1,6 @@
package de.kentoj.scrow.bukkit.gameserver;
public interface GameServer {
void destroy();
}

View file

@ -0,0 +1,8 @@
package de.kentoj.scrow.bukkit.gameserver;
public interface GameServerPool {
GameServer prepareServer(String gameName);
void returnServer(String name);
}

View file

@ -0,0 +1,24 @@
package de.kentoj.scrow.bukkit.minigame;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import java.util.concurrent.CompletableFuture;
public interface Minigame {
ScrowMessageStyle messageStyle();
/**
* Required participant count
*/
int minParticipants();
/**
* Maximally allowed participant count
*/
int maxParticipants();
PlayerManager playerManager();
CompletableFuture<Void> start();
}

View file

@ -0,0 +1,24 @@
package de.kentoj.scrow.bukkit.minigame;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class MinigameManager {
private final Map<UUID, Minigame> playerToMinigameMap = new HashMap<>();
public @Nullable Minigame getMinigameByPlayer(OfflinePlayer player) {
return playerToMinigameMap.get(player.getUniqueId());
}
public void setMinigame(OfflinePlayer player, @Nullable Minigame minigame) {
if (minigame == null)
playerToMinigameMap.remove(player.getUniqueId());
else
playerToMinigameMap.put(player.getUniqueId(), minigame);
}
}

View file

@ -0,0 +1,67 @@
package de.kentoj.scrow.bukkit.minigame;
import de.kentoj.scrow.bukkit.ScrowAPI;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
public final class PlayerManager {
private final Set<UUID> participants = new HashSet<>();
private final Set<UUID> spectators = new HashSet<>();
private final MinigameManager minigameManager = ScrowAPI.minigameManager();
private final Minigame minigame;
public PlayerManager(Minigame minigame) {
this.minigame = minigame;
}
public Stream<Player> getParticipants() {
return participants.stream().map(Bukkit::getPlayer);
}
public Stream<Player> getSpectators() {
return spectators.stream().map(Bukkit::getPlayer);
}
public int getParticipantCount() {
return participants.size();
}
public void addParticipant(Player player) {
participants.add(player.getUniqueId());
minigameManager.setMinigame(player, minigame);
}
/**
* @return if the player was a participant
*/
public boolean removeParticipant(Player player) {
minigameManager.setMinigame(player, null);
return participants.remove(player.getUniqueId());
}
public boolean isParticipant(Player player) {
return participants.contains(player.getUniqueId());
}
public void addSpectator(Player player) {
spectators.add(player.getUniqueId());
}
/**
* @return if the player was in a spectator
*/
public boolean removeSpectator(Player player) {
return spectators.remove(player.getUniqueId());
}
public Stream<Player> getAllPlayers() {
return Stream.concat(getParticipants(), getSpectators());
}
}

View file

@ -0,0 +1,68 @@
package de.kentoj.scrow.bukkit.minigame.lobby;
import de.kentoj.scrow.bukkit.minigame.Minigame;
import de.kentoj.scrow.bukkit.minigame.phase.CompositePhase;
import de.kentoj.scrow.bukkit.minigame.phaseflow.LinearPhaseFlow;
import de.kentoj.scrow.bukkit.minigame.phaseflow.PhaseContext;
import org.bukkit.event.EventPriority;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.hanging.HangingBreakByEntityEvent;
import org.bukkit.event.player.*;
public class LobbyPhase<T extends Minigame> extends CompositePhase<T> {
private final LinearPhaseFlow subPhaseFlow = new LinearPhaseFlow("lobby");
public LobbyPhase(PhaseContext<T> ctx) {
super(ctx);
PhaseContext<T> subCtx = new PhaseContext<>(ctx.game(), subPhaseFlow);
subPhaseFlow.add(new WaitForMinimumPlayersPhase<>(subCtx));
subPhaseFlow.add(new WaitForMaximumPlayersPhase<>(subCtx));
}
@Override
protected void onStart() {
super.onStart();
ctx.listeners().on(PlayerJoinEvent.class, EventPriority.LOWEST, this::onJoin);
ctx.listeners().on(PlayerQuitEvent.class, EventPriority.LOWEST, this::onQuit);
//noinspection unchecked
ctx.listeners().cancel(
HangingBreakByEntityEvent.class,
PlayerInteractAtEntityEvent.class,
PlayerInteractEntityEvent.class,
PlayerDropItemEvent.class,
BlockExplodeEvent.class,
EntityExplodeEvent.class,
EntityDamageEvent.class,
FoodLevelChangeEvent.class,
BlockBreakEvent.class,
BlockPlaceEvent.class,
EntityTargetLivingEntityEvent.class
);
}
private void onJoin(PlayerJoinEvent ev) {
ctx.players().addParticipant(ev.getPlayer());
ctx.players().getAllPlayers().forEach(p ->
p.sendMessage("§8[§a+§8] §7" + ev.getPlayer().getName()));
}
private void onQuit(PlayerQuitEvent ev) {
if (ctx.players().removeParticipant(ev.getPlayer()))
ctx.players().getAllPlayers().forEach(p ->
p.sendMessage("§8[§c-§8] §7" + ev.getPlayer().getName()));
}
@Override
public LinearPhaseFlow subPhaseFlow() {
return subPhaseFlow;
}
}

View file

@ -0,0 +1,44 @@
package de.kentoj.scrow.bukkit.minigame.lobby;
import de.kentoj.scrow.bukkit.minigame.Minigame;
import de.kentoj.scrow.bukkit.minigame.phase.Phase;
import de.kentoj.scrow.bukkit.minigame.phaseflow.PhaseContext;
import org.bukkit.event.player.PlayerQuitEvent;
class WaitForMaximumPlayersPhase<T extends Minigame> extends Phase<T> {
private int secsLeft;
public WaitForMaximumPlayersPhase(PhaseContext<T> ctx) {
super(ctx);
}
@Override
public void onStart() {
this.secsLeft = 5;
ctx.listeners().on(PlayerQuitEvent.class, this::onQuit);
}
private void onQuit(PlayerQuitEvent ev) {
var isEnough = ctx.players().getParticipantCount() >= ctx.game().minParticipants();
if (!isEnough) {
//deployedServer
ctx.phases().rewindPhase();
}
}
@Override
protected void onTick(int tick) {
if (tick % 20 == 0) {
ctx.players().getAllPlayers().forEach(player ->
player.sendTitle("Game starting in " + secsLeft + " seconds", "have fun", 5, 20, 10)
);
secsLeft--;
if (secsLeft == 0) ctx.advancePhase();
}
}
@Override
protected void onCancel() {
}
}

View file

@ -0,0 +1,27 @@
package de.kentoj.scrow.bukkit.minigame.lobby;
import de.kentoj.scrow.bukkit.minigame.Minigame;
import de.kentoj.scrow.bukkit.minigame.phase.Phase;
import de.kentoj.scrow.bukkit.minigame.phaseflow.PhaseContext;
import org.bukkit.event.player.PlayerJoinEvent;
class WaitForMinimumPlayersPhase<T extends Minigame> extends Phase<T> {
public WaitForMinimumPlayersPhase(PhaseContext<T> ctx) {
super(ctx);
}
@Override
protected void onStart() {
ctx.listeners().on(PlayerJoinEvent.class, this::onJoin);
}
private void onJoin(PlayerJoinEvent ev) {
boolean isEnoughParticipants = ctx.players().getParticipantCount() >= ctx.game().minParticipants();
if (isEnoughParticipants) ctx.advancePhase();
}
@Override
protected void onCancel() {
}
}

View file

@ -0,0 +1,22 @@
package de.kentoj.scrow.bukkit.minigame.phase;
import de.kentoj.scrow.bukkit.minigame.Minigame;
import de.kentoj.scrow.bukkit.minigame.phaseflow.PhaseContext;
public abstract class CompositePhase<T extends Minigame> extends Phase<T> {
public CompositePhase(PhaseContext<T> ctx) {
super(ctx);
}
public abstract PhaseFlow subPhaseFlow();
@Override
protected void onStart() {
subPhaseFlow().start().thenRun(super::advancePhase);
}
@Override
protected void onCancel() {
}
}

View file

@ -0,0 +1,10 @@
package de.kentoj.scrow.bukkit.minigame.phase;
public interface IPhase {
void start();
void end();
void cancel();
}

View file

@ -0,0 +1,77 @@
package de.kentoj.scrow.bukkit.minigame.phase;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.minigame.Minigame;
import de.kentoj.scrow.bukkit.minigame.phaseflow.PhaseContext;
import org.bukkit.scheduler.BukkitRunnable;
public abstract class Phase<T extends Minigame> implements IPhase {
protected final PhaseContext<T> ctx;
private boolean isActive = false;
protected Phase(PhaseContext<T> ctx) {
this.ctx = ctx;
}
/**
* Shall call {@link PhaseFlow#advancePhase()}
*/
protected abstract void onStart();
/**
* Called together with {@link Phase#onEnd()} but only called when the phase was cancelled
* due to an error
*/
protected abstract void onCancel();
protected void onEnd() {
}
protected void onTick(int tick) {
}
@Override
public void cancel() {
end();
onCancel();
}
public final void start() {
if (isActive) return;
isActive = true;
onStart();
runTicker();
}
public final void end() {
if (!isActive) return;
ctx.listeners().unregisterAll();
onEnd();
isActive = false;
}
/**
* end the current phase and advance to the next.
* same as {@link PhaseFlow#advancePhase()}.
*/
public final void advancePhase() {
ctx.phases().advancePhase();
}
private void runTicker() {
new BukkitRunnable() {
int tick = 0;
@Override
public void run() {
if (!isActive) {
this.cancel();
return;
}
onTick(tick++);
}
}.runTaskTimer(ScrowAPI.plugin, 1, 1);
}
}

View file

@ -0,0 +1,25 @@
package de.kentoj.scrow.bukkit.minigame.phase;
import java.util.concurrent.CompletableFuture;
public interface PhaseFlow {
String name();
/**
* end the current phase, go to the next phase and start it
*/
void advancePhase();
/**
* end the current phase, go to the previous phase and start it
*/
void rewindPhase();
void handlePhaseError();
/**
* CompletableFuture is completed when the game finishes
*/
CompletableFuture<Void> start();
}

View file

@ -0,0 +1,7 @@
package de.kentoj.scrow.bukkit.minigame.phase.event;
@FunctionalInterface
public interface EventHandler<E> {
void handle(E ev);
}

View file

@ -0,0 +1,29 @@
package de.kentoj.scrow.bukkit.minigame.phase.event;
import com.google.common.base.Preconditions;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
public interface PhaseListeners {
<E extends Event> void on(Class<E> eventClass, EventPriority priority, EventHandler<E> handler);
default <E extends Event> void on(Class<E> eventClass, EventHandler<E> handler) {
this.on(eventClass, EventPriority.NORMAL, handler);
}
default void cancel(Class<? extends Event>... classes) {
for (var clazz : classes) {
Preconditions.checkArgument(Cancellable.class.isAssignableFrom(clazz),
clazz.getCanonicalName() + " does not implement Cancellable");
on(clazz, ev -> ((Cancellable) ev).setCancelled(true));
}
}
/**
* Unregisters all listeners bound to this phase.
* Note: this is automatically called on phase end.
*/
void unregisterAll();
}

View file

@ -0,0 +1,24 @@
package de.kentoj.scrow.bukkit.minigame.phase.event;
import de.kentoj.scrow.bukkit.ScrowAPI;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
public class PhaseListenersImpl implements PhaseListeners, Listener {
@Override
public final <E extends Event> void on(Class<E> clazz, EventPriority priority, EventHandler<E> handler) {
Bukkit.getPluginManager().registerEvent(clazz, this, priority, (__, ev) -> {
//noinspection unchecked
handler.handle((E) ev);
}, ScrowAPI.plugin);
}
@Override
public void unregisterAll() {
HandlerList.unregisterAll(this);
}
}

View file

@ -0,0 +1,116 @@
package de.kentoj.scrow.bukkit.minigame.phaseflow;
import com.google.common.base.Preconditions;
import de.kentoj.scrow.bukkit.minigame.phase.IPhase;
import de.kentoj.scrow.bukkit.minigame.phase.PhaseFlow;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class LinearPhaseFlow implements PhaseFlow {
private final Logger log = LoggerFactory.getLogger(LinearPhaseFlow.class);
private final String name;
private final List<IPhase> phases = new ArrayList<>();
private CompletableFuture<Void> onEnd = null;
private int curInd = -1;
public LinearPhaseFlow(String name) {
this.name = name;
}
public void add(IPhase phase) {
this.phases.add(phase);
}
@Override
public void advancePhase() {
IPhase cur;
cur = current();
if (cur != null) {
cur.cancel();
}
curInd++;
cur = current();
if (cur == null) {
onEnd.complete(null);
log.info("phase flow {} ended", name());
return;
}
startPhase(cur);
}
@Override
public void rewindPhase() {
var cur = current();
Preconditions.checkNotNull(cur, "trying to rewind phase but no phase is active");
cur.cancel();
curInd--;
cur = current();
if (cur != null)
startPhase(cur);
}
@Override
public void handlePhaseError() {
var cur = current();
Preconditions.checkNotNull(cur, "trying to rewind phase but no phase is active");
cur.cancel();
if (curInd == 0) {
startPhase(cur, false);
return;
}
curInd--;
cur = current();
startPhase(cur);
}
@Override
public CompletableFuture<Void> start() {
Preconditions.checkState(onEnd == null, "attempting to start game that is already active");
onEnd = new CompletableFuture<>();
onEnd.thenRun(() -> onEnd = null);
log.info("starting phase flow {}", name());
advancePhase();
return onEnd;
}
private void startPhase(IPhase phase) {
this.startPhase(phase, true);
}
private void startPhase(IPhase phase, boolean recoverOnError) {
if (!recoverOnError) {
phase.start();
return;
}
try {
log.info("starting phase {}", phase.getClass().getCanonicalName());
phase.start();
} catch (Exception e) {
log.error("failed starting phase {}: rewinding phase", phase.getClass().getCanonicalName(), e);
handlePhaseError();
}
}
private @Nullable IPhase current() {
if (curInd < 0 || curInd >= phases.size()) return null;
return phases.get(curInd);
}
@Override
public String name() {
return name;
}
}

View file

@ -0,0 +1,43 @@
package de.kentoj.scrow.bukkit.minigame.phaseflow;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.scrow.bukkit.minigame.Minigame;
import de.kentoj.scrow.bukkit.minigame.PlayerManager;
import de.kentoj.scrow.bukkit.minigame.phase.PhaseFlow;
import de.kentoj.scrow.bukkit.minigame.phase.event.PhaseListeners;
import de.kentoj.scrow.bukkit.minigame.phase.event.PhaseListenersImpl;
public class PhaseContext<T extends Minigame> {
private final T game;
private final PhaseFlow phaseFlow;
private final PhaseListeners listeners = new PhaseListenersImpl();
public PhaseContext(T game, PhaseFlow phaseFlow) {
this.game = game;
this.phaseFlow = phaseFlow;
}
public T game() {
return game;
}
public PhaseFlow phases() {
return phaseFlow;
}
public PlayerManager players() {
return game.playerManager();
}
public MessageStyle style() {
return game.messageStyle();
}
public PhaseListeners listeners() {
return listeners;
}
public void advancePhase() {
phaseFlow.advancePhase();
}
}

View file

@ -0,0 +1,12 @@
package de.kentoj.scrow.bukkit.region;
import java.util.UUID;
public abstract class AbstractRegion implements Region {
private final UUID uuid = UUID.randomUUID();
public UUID uuid() {
return uuid;
}
}

View file

@ -0,0 +1,9 @@
package de.kentoj.scrow.bukkit.region;
import org.bukkit.Chunk;
public record ChunkPos(int x, int z) {
public static ChunkPos fromChunk(Chunk chunk) {
return new ChunkPos(chunk.getX(), chunk.getZ());
}
}

View file

@ -0,0 +1,40 @@
package de.kentoj.scrow.bukkit.region;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* region consisting of multiple regions
*/
public class CompositeRegion extends AbstractRegion {
private final Region[] regions;
public CompositeRegion(Region[] regions) {
this.regions = regions;
}
@Override
public boolean isInRegion(Location location) {
for (Region region : regions) {
if (region.isInRegion(location)) return true;
}
return false;
}
@Override
public List<ChunkPos> chunks() {
return Arrays.stream(regions)
.map(Region::chunks)
.reduce(new ArrayList<>(), (list, chunks) -> {
list.addAll(chunks);
return list;
})
.stream()
.distinct()
.toList();
}
}

View file

@ -0,0 +1,43 @@
package de.kentoj.scrow.bukkit.region;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
public class CuboidRegion extends AbstractRegion {
private final Range xRange;
private final Range yRange;
private final Range zRange;
public CuboidRegion(Location loc1, Location loc2) {
xRange = Range.of(loc1.getX(), loc2.getX());
yRange = Range.of(loc1.getY(), loc2.getY());
zRange = Range.of(loc1.getY(), loc2.getY());
}
@Override
public boolean isInRegion(Location location) {
return xRange.contains(location.getX())
&& yRange.contains(location.getY())
&& zRange.contains(location.getZ());
}
@Override
public List<ChunkPos> chunks() {
List<ChunkPos> res = new ArrayList<>();
int minChunkX = (int) Math.floor(xRange.min()) << 4;
int maxChunkX = (int) Math.ceil(xRange.max()) << 4;
for (int chunkX = minChunkX; chunkX < maxChunkX; chunkX++) {
int minChunkZ = (int) Math.floor(zRange.min()) << 4;
int maxChunkZ = (int) Math.ceil(zRange.max()) << 4;
for (int chunkZ = minChunkZ; chunkZ < maxChunkZ; chunkZ++) {
res.add(new ChunkPos(chunkX, chunkZ));
}
}
return res;
}
}

View file

@ -0,0 +1,45 @@
package de.kentoj.scrow.bukkit.region;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.List;
public class CylinderRegion extends AbstractRegion {
private final Location center;
private final double height;
private final double radius;
public CylinderRegion(Location center, double height, double radius) {
this.center = center;
this.height = height;
this.radius = radius;
}
@Override
public boolean isInRegion(Location location) {
if (location.getY() < center.getY()) return false;
if (location.getY() > center.getY() + height) return false;
double diffX = location.getX() - center.getX();
double diffZ = location.getZ() - center.getZ();
return diffX * diffX + diffZ * diffZ <= radius * radius;
}
@Override
public List<ChunkPos> chunks() {
List<ChunkPos> res = new ArrayList<>();
int a = (int) Math.ceil(radius / 16);
var centerChunkX = center.getBlockX() >> 4;
var centerChunkZ = center.getBlockZ() >> 4;
for (int x = -a; x <= a; x++) {
for (int z = -a; z <= a; z++) {
res.add(new ChunkPos(centerChunkX + x, centerChunkZ + z));
}
}
return res;
}
}

View file

@ -0,0 +1,18 @@
package de.kentoj.scrow.bukkit.region;
public record Range(
double min,
double max
) {
public boolean contains(double num) {
return min <= num && num <= max;
}
public double getLength() {
return max - min;
}
public static Range of(double a, double b) {
return new Range(Math.min(a, b), Math.max(a, b));
}
}

View file

@ -0,0 +1,25 @@
package de.kentoj.scrow.bukkit.region;
import org.bukkit.Location;
import java.util.List;
import java.util.UUID;
public interface Region {
/**
* A random uuid assigned to the Region at creation
*/
UUID uuid();
/**
* only called if location is in {@link Region#chunks()}
*/
boolean isInRegion(Location location);
/**
* Used for optimization: isInRegion is only called if the location is in one of these chunks
* @return Chunks that the region touches
*/
List<ChunkPos> chunks();
}

View file

@ -0,0 +1,21 @@
package de.kentoj.scrow.bukkit.region;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.UUID;
/**
* Regions are not a reliable enough to detect fast movements.
* The way regions work is that every few ticks, all regions are checked for players.
* @see org.bukkit.event.player.PlayerMoveEvent
*/
public interface RegionManager {
List<Region> getRegions(ChunkPos chunkPos);
@Nullable Region getRegion(UUID regionUuid);
void registerRegion(Region region);
void unregisterRegion(Region region);
}

View file

@ -0,0 +1,18 @@
package de.kentoj.scrow.bukkit.region.event;
import de.kentoj.scrow.bukkit.region.Region;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerEvent;
public abstract class RegionEvent extends PlayerEvent {
private final Region region;
protected RegionEvent(Region region, Player player) {
super(player);
this.region = region;
}
public Region getRegion() {
return region;
}
}

View file

@ -0,0 +1,37 @@
package de.kentoj.scrow.bukkit.region.event;
import de.kentoj.scrow.bukkit.region.Region;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class RegionJoinEvent extends RegionEvent implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private boolean isCancelled;
public RegionJoinEvent(Region region, Player player) {
super(region, player);
}
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public boolean isCancelled() {
return this.isCancelled;
}
@Override
public void setCancelled(boolean isCancelled) {
this.isCancelled = isCancelled;
}
}

View file

@ -0,0 +1,37 @@
package de.kentoj.scrow.bukkit.region.event;
import de.kentoj.scrow.bukkit.region.Region;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class RegionLeaveEvent extends RegionEvent implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private boolean isCancelled;
public RegionLeaveEvent(Region region, Player player) {
super(region, player);
}
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public boolean isCancelled() {
return this.isCancelled;
}
@Override
public void setCancelled(boolean isCancelled) {
this.isCancelled = isCancelled;
}
}

View file

@ -0,0 +1,66 @@
package de.kentoj.scrow.bukkit.scheduler;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public interface Task<T> {
CompletableFuture<T> toFuture();
default Task<?> runSync(Runnable task) {
return mapSync(__ -> {
task.run();
return null;
});
}
default <S> Task<S> supplySync(Supplier<S> supplier) {
return mapSync(_ -> supplier.get());
}
default Task<?> thenSync(Consumer<T> consumer) {
return mapSync(previous -> {
consumer.accept(previous);
return null;
});
}
<S> Task<S> mapSync(Function<T, S> mapper);
default Task<?> runAsync(Runnable task) {
return mapAsync(_ -> {
task.run();
return null;
});
}
default <S> Task<S> supplyAsync(Supplier<S> supplier) {
return mapAsync(_ -> supplier.get());
}
default Task<?> thenAsync(Consumer<T> consumer) {
return mapAsync(previous -> {
consumer.accept(previous);
return null;
});
}
<S> Task<S> mapAsync(Function<T, S> mapper);
Task<T> yieldIf(Predicate<T> precondition);
default T await() {
try {
return toFuture().get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,101 @@
package de.kentoj.scrow.bukkit.scheduler;
import de.kentoj.scrow.bukkit.ScrowAPI;
import org.bukkit.Bukkit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* A Task runs entirely async, when sync is used, a subtask is run on the main thread and the async thread
* blocks until the subtask returns.
*
* @param <T> type of computed result
*/
public class TaskImpl<T> implements Task<T> {
private static final Logger log = LoggerFactory.getLogger(TaskImpl.class);
private static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
private final Supplier<TaskState<T>> compute;
private volatile TaskState<T> cachedResult = null;
TaskImpl(Supplier<TaskState<T>> compute) {
this.compute = compute;
}
@Override
public CompletableFuture<T> toFuture() {
return CompletableFuture.supplyAsync(() -> {
try {
return state().value();
} catch (Exception ex) {
log.error("failed completing future", ex);
throw new CompletionException(ex);
}
}, EXECUTOR);
}
/**
* Maps the last result on the main thread
*/
public <S> Task<S> mapSync(Function<T, S> mapper) {
return new TaskImpl<>(() -> {
var s = state();
if (s.yielded()) return yielded(s);
try {
return TaskState.completed(Bukkit.getScheduler()
.callSyncMethod(ScrowAPI.plugin, () -> mapper.apply(s.value()))
.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
});
}
/**
* Maps the last result async
*/
public <S> Task<S> mapAsync(Function<T, S> mapper) {
return new TaskImpl<>(() -> {
var s = state();
if (s.yielded()) return yielded(s);
return TaskState.completed(mapper.apply(state().value()));
});
}
@Override
public Task<T> yieldIf(Predicate<T> precondition) {
var s = state();
if (!s.yielded() && precondition.test(s.value())) {
return new TaskImpl<>(() -> TaskState.yielded(s.value()));
}
return this;
}
@SuppressWarnings("unchecked")
private <S> TaskState<S> yielded(TaskState<T> state) {
try {
return (TaskState<S>) state;
} catch (ClassCastException ex) {
log.error("cannot yield when type of next task differs", ex);
throw ex;
}
}
private TaskState<T> state() {
synchronized (this) {
if (cachedResult == null)
cachedResult = compute.get();
return cachedResult;
}
}
}

View file

@ -0,0 +1,13 @@
package de.kentoj.scrow.bukkit.scheduler;
import org.jetbrains.annotations.Nullable;
public record TaskState<T>(@Nullable T value, boolean yielded) {
public static <T> TaskState<T> yielded(@Nullable T value) {
return new TaskState<>(value, true);
}
public static <T> TaskState<T> completed(@Nullable T value) {
return new TaskState<>(value, false);
}
}

View file

@ -0,0 +1,55 @@
package de.kentoj.scrow.bukkit.scheduler;
import de.kentoj.scrow.bukkit.ScrowAPI;
import org.bukkit.Bukkit;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Supplier;
public class Tasks {
private Tasks() {
}
public static <T> Task<T> supplyAsync(Supplier<T> supplier) {
return new TaskImpl<>(() -> TaskState.completed(supplier.get()));
}
public static Task<?> runAsync(Runnable task) {
return supplyAsync(() -> {
task.run();
return null;
});
}
public static <T> Task<T> supplySync(Supplier<T> supplier) {
return new TaskImpl<>(() -> {
try {
return TaskState.completed(Bukkit.getScheduler()
.callSyncMethod(ScrowAPI.plugin, supplier::get)
.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
});
}
public static Task<?> runSync(Runnable task) {
return supplySync(() -> {
task.run();
return null;
});
}
public static void awaitAll(Task<?>... tasks) {
var cfs = Arrays.stream(tasks)
.map(Task::toFuture)
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(cfs).join();
}
}

View file

@ -0,0 +1,48 @@
package de.kentoj.scrow.bukkit;
import de.kentoj.scrow.bukkit.style.ChatStyleListener;
import de.kentoj.scrow.bukkit.command.economy.CoinsCommand;
import de.kentoj.scrow.bukkit.command.friends.FriendCommand;
import de.kentoj.scrow.bukkit.command.instance.InstanceCache;
import de.kentoj.scrow.bukkit.command.instance.InstanceCommand;
import de.kentoj.scrow.bukkit.command.instance.LobbyCommand;
import de.kentoj.scrow.bukkit.command.instance.PlayCommand;
import de.kentoj.scrow.bukkit.internal.player.prefix.CachedPrefixProvider;
import de.kentoj.scrow.bukkit.internal.player.prefix.LuckPermsPrefixSetter;
import de.kentoj.scrow.bukkit.style.TabStyleListener;
import net.luckperms.api.LuckPermsProvider;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public final class CoreImplPlugin extends JavaPlugin {
@Override
public void onEnable() {
var cachedPrefixProvider = new CachedPrefixProvider();
ScrowAPISurface.initScrowAPI(this, cachedPrefixProvider);
ScrowAPI.playerCommands().register(new CoinsCommand().rootLiteral());
ScrowAPI.playerCommands().register(new FriendCommand().rootLiteral());
var instanceCache = new InstanceCache();
ScrowAPI.commands().register(new InstanceCommand(instanceCache).rootLiteral());
ScrowAPI.playerCommands().register(new PlayCommand().rootLiteral());
ScrowAPI.playerCommands().register(new LobbyCommand().rootLiteral());
var luckperms = LuckPermsProvider.get();
new LuckPermsPrefixSetter(luckperms, cachedPrefixProvider).init(this);
getServer().getPluginManager().registerEvents(new ChatStyleListener(cachedPrefixProvider), this);
getServer().getPluginManager().registerEvents(new TabStyleListener(cachedPrefixProvider), this);
// TODO/FIXME
//registerServer();
}
@Override
public void onDisable() {
ScrowAPISurface.destroyScrowAPI();
}
}

View file

@ -0,0 +1,90 @@
package de.kentoj.scrow.bukkit;
import de.kentoj.kencommandapi.CommandAPI;
import de.kentoj.scrow.bukkit.internal.player.NetworkPlayerWatchdog;
import de.kentoj.scrow.bukkit.internal.region.RegionManagerImpl;
import de.kentoj.scrow.bukkit.internal.region.RegionPlayerTracker;
import de.kentoj.scrow.bukkit.internal.region.RegionTask;
import de.kentoj.scrowlib.economy.InMemoryEconomyService;
import de.kentoj.scrowlib.economy.PostgresEconomyService;
import de.kentoj.scrowlib.friends.friendship.InMemoryFriendshipService;
import de.kentoj.scrowlib.friends.friendship.PostgresFriendshipService;
import de.kentoj.scrowlib.friends.request.InMemoryFriendRequestService;
import de.kentoj.scrowlib.friends.request.PostgresFriendRequestService;
import de.kentoj.scrowlib.instancemanager.InstanceManagerImpl;
import de.kentoj.scrowlib.messaging.InMemoyMessageBroker;
import de.kentoj.scrowlib.messaging.NatsMessageBroker;
import de.kentoj.scrowlib.player.NetworkPlayerImpl;
import de.kentoj.scrowlib.player.PlayerPrefixProvider;
import io.nats.client.Nats;
import io.nats.client.Options;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.jdbi.v3.core.Jdbi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Properties;
public class ScrowAPISurface {
private static final Logger log = LoggerFactory.getLogger(ScrowAPISurface.class);
private ScrowAPISurface() {
}
public static void initScrowAPI(Plugin plugin, PlayerPrefixProvider prefixProvider) {
ScrowAPI.setCorePlugin(plugin);
var natsHost = System.getenv("HOST_NATS");
if (natsHost != null) {
try {
var options = Options.builder()
.server(natsHost)
.pedantic()
.build();
ScrowAPI.setMessageBroker(new NatsMessageBroker(Nats.connect(options)));
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
} else {
log.error("HOST_NATS not set -> Using in-memory dummy message broker");
ScrowAPI.setMessageBroker(new InMemoyMessageBroker());
}
var hostPostgres = System.getenv().get("HOST_POSTGRES");
if (hostPostgres != null) {
try {
Class.forName("org.postgresql.Driver", true, ScrowAPISurface.class.getClassLoader());
var props = new Properties();
props.setProperty("user", "postgres");
ScrowAPI.setJdbi(Jdbi.create(hostPostgres, props));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
} else {
log.error("HOST_POSTGRES not set -> Using in-memory database");
}
ScrowAPI.setPlayerFactory(playerId -> new NetworkPlayerImpl(ScrowAPI.messageBroker(), playerId));
ScrowAPI.setRegionManager(new RegionManagerImpl());
ScrowAPI.setCommands(new CommandAPI<>(plugin, CommandSender.class));
ScrowAPI.setPlayerCommands(new CommandAPI<>(plugin, Player.class));
ScrowAPI.setInstanceManager(new InstanceManagerImpl(ScrowAPI.messageBroker()));
ScrowAPI.setEconomyService(ScrowAPI.jdbi() != null ?
new PostgresEconomyService(ScrowAPI.jdbi()) : new InMemoryEconomyService());
ScrowAPI.setFriendRequestService(ScrowAPI.jdbi() != null ?
new PostgresFriendRequestService(ScrowAPI.jdbi()) : new InMemoryFriendRequestService());
ScrowAPI.setFriendshipService(ScrowAPI.jdbi() != null ?
new PostgresFriendshipService(ScrowAPI.jdbi()) : new InMemoryFriendshipService());
ScrowAPI.setPlayerPrefixProvider(prefixProvider);
new RegionTask(new RegionPlayerTracker()).runTaskTimer(plugin, 10, 10);
new NetworkPlayerWatchdog().listen();
}
public static void destroyScrowAPI() {
}
}

View file

@ -0,0 +1,44 @@
package de.kentoj.scrow.bukkit.command.economy;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.entity.Player;
import java.util.concurrent.CompletableFuture;
public final class CoinsCommand {
private final RootLiteral<Player> rootLiteral;
private final MessageStyle style;
public CoinsCommand() {
style = ScrowMessageStyle.GENERIC;
rootLiteral = Literal.<Player>builder("coins", "bal", "balance")
.withPermission("command.coins.get.self")
.withExecutor(this::execute)
.withLiteral(new CoinsSetLiteral(style).literal())
.withLiteral(new CoinsGetLiteral(style).literal())
.asRootLiteral(style);
}
public CompletableFuture<Result<Object, String>> execute(CommandContext<Player> ctx) {
var player = ctx.sender();
return Tasks.supplyAsync(() ->
ScrowAPI.economyService().getCoins(player.getUniqueId()))
.<Result<Object, String>>mapSync(amount -> {
player.sendMessage(style.ok("You have " + amount + "$"));
return Results.success(new Object());
}).toFuture();
}
public RootLiteral<Player> rootLiteral() {
return rootLiteral;
}
}

View file

@ -0,0 +1,49 @@
package de.kentoj.scrow.bukkit.command.economy;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
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 org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.concurrent.CompletableFuture;
public final class CoinsGetLiteral {
private final Literal<Player> literal;
private final MessageStyle style;
private final CommandArgumentSpec<Player, OfflinePlayer> playerArg;
public CoinsGetLiteral(MessageStyle style) {
this.style = style;
playerArg = CommandArgumentSpec.builder("player", new OfflinePlayerArgumentType<Player>())
.withDefaultValue(CommandContext::sender)
.build();
literal = Literal.<Player>builder("get")
.withPermission("command.coins.get.others")
.withArgument(playerArg)
.withExecutor(this::execute)
.build();
}
private CompletableFuture<Result<Object, String>> execute(CommandContext<Player> ctx) {
var player = ctx.getArg(playerArg);
return Tasks.supplyAsync(() ->
ScrowAPI.economyService().getCoins(player.getUniqueId()))
.<Result<Object, String>>mapSync(amount -> {
ctx.sender().sendMessage(style.ok(player.getName() + " has " + amount + "$"));
return Results.success(new Object());
}).toFuture();
}
public Literal<Player> literal() {
return literal;
}
}

View file

@ -0,0 +1,54 @@
package de.kentoj.scrow.bukkit.command.economy;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.argument.types.IntegerArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
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 org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.concurrent.CompletableFuture;
public final class CoinsSetLiteral {
private final Literal<Player> literal;
private final MessageStyle style;
private final CommandArgumentSpec<Player, OfflinePlayer> playerArg;
private final CommandArgumentSpec<Player, Integer> amountArg;
CoinsSetLiteral(MessageStyle style) {
this.style = style;
playerArg = CommandArgumentSpec.builder("player", new OfflinePlayerArgumentType<Player>())
.withDefaultValue(CommandContext::sender)
.build();
amountArg = CommandArgumentSpec.builder("amount", new IntegerArgumentType<Player>())
.build();
literal = Literal.<Player>builder("set")
.withPermission("command.coins.set")
.withArgument(playerArg)
.withArgument(amountArg)
.withExecutor(this::execute)
.build();
}
private CompletableFuture<Result<Object, String>> execute(CommandContext<Player> ctx) {
var player = ctx.getArg(playerArg);
var amount = ctx.getArg(amountArg);
return Tasks.runAsync(() ->
ScrowAPI.economyService().setCoins(player.getUniqueId(), amount))
.<Result<Object, String>>supplySync(() -> {
ctx.sender().sendMessage(style.ok(player.getName() + " now has " + amount + "$"));
return Results.success(new Object());
}).toFuture();
}
public Literal<Player> literal() {
return literal;
}
}

View file

@ -0,0 +1,122 @@
package de.kentoj.scrow.bukkit.command.friends;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.type.OfflinePlayerArgumentType;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Task;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import de.kentoj.scrowlib.friends.FriendRequestService;
import de.kentoj.scrowlib.friends.FriendshipService;
import de.kentoj.scrowlib.friends.friendship.FriendshipImpl;
import de.kentoj.scrowlib.friends.request.FriendRequestImpl;
import net.kyori.adventure.text.Component;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public final class FriendAddLiteral {
private final Literal<Player> literal;
private final CommandArgumentSpec<Player, OfflinePlayer> playerArg;
private final ScrowMessageStyle style;
private final FriendRequestService requestService = ScrowAPI.friendRequestService();
private final FriendshipService friendsService = ScrowAPI.friendshipService();
FriendAddLiteral(ScrowMessageStyle style) {
this.style = style;
playerArg = CommandArgumentSpec.builder("player", new OfflinePlayerArgumentType<Player>())
.build();
literal = Literal.<Player>builder("add")
.withArgument(playerArg)
.withExecutor(this::execute)
.build();
}
private CompletableFuture<Result<Object, String>> execute(CommandContext<Player> ctx) {
var self = ctx.sender();
var other = ctx.getArg(playerArg);
return Tasks.supplyAsync(() -> findAction(self, other)
.mapSuccess(action -> {
if (action == AddAction.SEND_REQUEST) sendRequest(self, other);
else if (action == AddAction.ACCEPT_REQUEST) acceptRequest(self, other);
return new Object();
}))
.toFuture();
}
private void sendRequest(Player self, OfflinePlayer other) {
Tasks.runAsync(() ->
requestService.saveFriendRequest(new FriendRequestImpl(self.getUniqueId(), other.getUniqueId())))
.runSync(() -> {
self.sendMessage(style.ok("Successfully sent " + other.getName() + " a friend request."));
var msg = Component.text(self.getName() + " sent you a friend request.")
.append(FriendCommand.friendRequestOptions(self.getName(), true));
ScrowAPI.getPlayer(other).sendMessage(style.ok(msg));
})
.await();
}
private void acceptRequest(Player self, OfflinePlayer other) {
Tasks.runAsync(() -> Tasks.awaitAll(
Tasks.runAsync(() -> requestService.deleteFriendRequest(self.getUniqueId(), other.getUniqueId())),
Tasks.runAsync(() -> requestService.deleteFriendRequest(other.getUniqueId(), self.getUniqueId())),
Tasks.runAsync(() -> friendsService.saveFriendship(new FriendshipImpl(self.getUniqueId(), other.getUniqueId(), Instant.now()))
)))
.runSync(() -> {
self.sendMessage(style.ok("You are now friends with " + other.getName() + "."));
ScrowAPI.getPlayer(other).sendMessage(style.ok("You are now friends with " + self.getName() + "."));
})
.await();
}
private Result<AddAction, String> findAction(Player self, OfflinePlayer other) {
if (self.getUniqueId() == other.getUniqueId())
return Results.failure("You cannot add yourself.");
var isAlreadyFriendTask = checkAlreadyFriends(self, other);
var isAlreadySent = checkAlreadySent(self.getUniqueId(), other.getUniqueId());
var isShouldAccept = checkShouldAccept(self, other);
Tasks.awaitAll(isAlreadyFriendTask, isAlreadyFriendTask, isShouldAccept);
if (isAlreadyFriendTask.await())
return Results.failure("You are already friends with " + other.getName() + ".");
if (isAlreadySent.await())
return Results.failure("You've already sent " + other.getName() + " a friend request.");
if (isShouldAccept.await())
return Results.success(AddAction.ACCEPT_REQUEST);
return Results.success(AddAction.SEND_REQUEST);
}
private Task<Boolean> checkShouldAccept(Player self, OfflinePlayer other) {
return Tasks.supplyAsync(() -> requestService.getFriendRequest(other.getUniqueId(), self.getUniqueId()))
.mapSync(Objects::nonNull);
}
private Task<Boolean> checkAlreadyFriends(Player self, OfflinePlayer other) {
return Tasks.supplyAsync(() -> friendsService.getFriendship(self.getUniqueId(), other.getUniqueId()))
.mapSync(Objects::nonNull);
}
private Task<Boolean> checkAlreadySent(UUID self, UUID other) {
return Tasks.supplyAsync(() -> requestService.getFriendRequest(self, other))
.mapSync(Objects::nonNull);
}
public Literal<Player> literal() {
return literal;
}
private enum AddAction {
SEND_REQUEST,
ACCEPT_REQUEST
}
}

View file

@ -0,0 +1,45 @@
package de.kentoj.scrow.bukkit.command.friends;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
public final class FriendCommand {
private final RootLiteral<Player> rootLiteral;
public FriendCommand() {
var style = new ScrowMessageStyle("Friends", NamedTextColor.AQUA);
rootLiteral = Literal.<Player>builder("f", "friend")
.withLiteral(new FriendListLiteral(style).literal())
.withLiteral(new FriendAddLiteral(style).literal())
.withLiteral(new FriendRemoveLiteral(style).literal())
.withLiteral(new FriendRequestDeclineLiteral(style).literal())
.withLiteral(new FriendRequestsLiteral(style).literal())
.asRootLiteral(style);
}
public static Component friendRequestOptions(String requestSenderName, boolean runInstantly) {
return Component.text(" ")
.append(Component.text("[accept]")
.hoverEvent(HoverEvent.showText(Component.text("click to accept")))
.clickEvent(ClickEvent.runCommand("/f add " + requestSenderName))
.color(NamedTextColor.GREEN))
.append(Component.text(" "))
.append(Component.text("[decline]")
.hoverEvent(HoverEvent.showText(Component.text("click to decline")))
.clickEvent(runInstantly
? ClickEvent.runCommand("/f decline " + requestSenderName)
: ClickEvent.suggestCommand("/f decline " + requestSenderName))
.color(NamedTextColor.RED));
}
public RootLiteral<Player> rootLiteral() {
return rootLiteral;
}
}

View file

@ -0,0 +1,74 @@
package de.kentoj.scrow.bukkit.command.friends;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static java.util.Objects.requireNonNull;
import static net.kyori.adventure.text.Component.text;
public final class FriendListLiteral {
private final Literal<Player> literal;
private final ScrowMessageStyle style;
FriendListLiteral(ScrowMessageStyle style) {
this.style = style;
literal = Literal.<Player>builder("list")
.withExecutor(this::displayFriendList)
.build();
}
private CompletableFuture<Result<Object, String>> displayFriendList(CommandContext<Player> ctx) {
var sender = ctx.sender();
return Tasks.supplyAsync(() ->
ScrowAPI.friendshipService().getFriendships(sender.getUniqueId()))
.<Result<Object, String>>mapSync(friendships -> {
var friends = friendships.stream()
.map(friendship -> Bukkit.getOfflinePlayer(friendship.uuids().getOther(sender.getUniqueId())))
.sorted(this::compareByOnlineStatus)
.toList();
if (friends.isEmpty()) return Results.failure("You have no friends. Use /friend add <player>");
sender.sendMessage(formatList(friends));
return Results.success(new Object());
}).toFuture();
}
private int compareByOnlineStatus(OfflinePlayer p1, OfflinePlayer p2) {
if (p1.isOnline() && !p2.isOnline()) return 1;
if (!p1.isOnline() && p2.isOnline()) return -1;
return 0;
}
private Component formatList(List<OfflinePlayer> friends) {
var message = style.ok(MiniMessage.miniMessage().deserialize("Your friends:"));
for (var friend : friends) {
var status = MiniMessage.miniMessage().deserialize(friend.isOnline()
? " <gray>(<green>online<gray>)"
: " <gray>(<red>offline<gray>)");
message = message
.appendNewline()
.append(text("").color(style.prefixColor()))
.append(text(requireNonNull(friend.getName())).color(NamedTextColor.GRAY))
.append(status);
}
return message;
}
public Literal<Player> literal() {
return literal;
}
}

View file

@ -0,0 +1,48 @@
package de.kentoj.scrow.bukkit.command.friends;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.type.OfflinePlayerArgumentType;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.concurrent.CompletableFuture;
public final class FriendRemoveLiteral {
private final Literal<Player> literal;
private final CommandArgumentSpec<Player, OfflinePlayer> playerArg;
private final ScrowMessageStyle style;
FriendRemoveLiteral(ScrowMessageStyle style) {
this.style = style;
playerArg = CommandArgumentSpec.<Player, OfflinePlayer>builder("player", new OfflinePlayerArgumentType<>())
.build();
literal = Literal.<Player>builder("remove")
.withArgument(playerArg)
.withExecutor(this::removeFriend)
.build();
}
private CompletableFuture<Result<Object, String>> removeFriend(CommandContext<Player> ctx) {
var friend = ctx.getArg(playerArg);
return Tasks.supplyAsync(() ->
ScrowAPI.friendshipService().deleteFriendship(friend.getUniqueId(), ctx.sender().getUniqueId()))
.<Result<Object, String>>mapSync(deleted -> {
if (!deleted)
return Results.failure("You are not friends with " + friend.getName() + ".");
ctx.sender().sendMessage(style.ok("You have ended the friendship with " + friend.getName() + "."));
return Results.success(new Object());
}).toFuture();
}
public Literal<Player> literal() {
return literal;
}
}

View file

@ -0,0 +1,51 @@
package de.kentoj.scrow.bukkit.command.friends;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.type.OfflinePlayerArgumentType;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import de.kentoj.scrowlib.friends.FriendRequestService;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import java.util.concurrent.CompletableFuture;
public final class FriendRequestDeclineLiteral {
private final Literal<Player> literal;
private final CommandArgumentSpec<Player, OfflinePlayer> playerArg;
private final ScrowMessageStyle style;
private final FriendRequestService requestService = ScrowAPI.friendRequestService();
FriendRequestDeclineLiteral(ScrowMessageStyle style) {
this.style = style;
playerArg = CommandArgumentSpec.builder("player", new OfflinePlayerArgumentType<Player>())
.build();
literal = Literal.<Player>builder("reject", "decline")
.withArgument(playerArg)
.withExecutor(this::declineFriendRequest)
.build();
}
private CompletableFuture<Result<Object, String>> declineFriendRequest(CommandContext<Player> ctx) {
var other = ctx.getArg(playerArg);
return Tasks.supplyAsync(() ->
requestService.deleteFriendRequest(other.getUniqueId(), ctx.sender().getUniqueId()))
.<Result<Object, String>>mapSync(deleted -> {
if (!deleted)
return Results.failure("There's no incoming friend request from " + other.getName() + ".");
ctx.sender().sendMessage(style.ok("Friend request declined."));
ScrowAPI.getPlayer(other).sendMessage(style.ok(ctx.sender().getName() + " has declined your friend request."));
return Results.success(new Object());
}).toFuture();
}
public Literal<Player> literal() {
return literal;
}
}

View file

@ -0,0 +1,66 @@
package de.kentoj.scrow.bukkit.command.friends;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import de.kentoj.scrowlib.friends.FriendRequest;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static net.kyori.adventure.text.Component.text;
public final class FriendRequestsLiteral {
private final Literal<Player> literal;
private final ScrowMessageStyle style;
FriendRequestsLiteral(ScrowMessageStyle style) {
this.style = style;
literal = Literal.<Player>builder("requests")
.withExecutor(this::displayFriendRequests)
.build();
}
private CompletableFuture<Result<Object, String>> displayFriendRequests(CommandContext<Player> ctx) {
return Tasks.supplyAsync(() ->
ScrowAPI.friendRequestService().getFriendRequestsTo(ctx.sender().getUniqueId()))
.<Result<Object, String>>mapAsync(list -> {
if (list.isEmpty()) return Results.failure("You have no friend requests.");
var requesters = list.stream()
.sorted(this::compareByAge)
.map(fr -> Bukkit.getOfflinePlayer(fr.from()).getName());
ctx.sender().sendMessage(formatList(requesters.toList()));
return Results.success(new Object());
}).toFuture();
}
private int compareByAge(FriendRequest a, FriendRequest b) {
return a.createdAt().isAfter(b.createdAt()) ? 1 : -1;
}
private Component formatList(List<String> requesters) {
var message = style.ok(MiniMessage.miniMessage().deserialize("Open friend requests:"));
for (String requester : requesters) {
message = message
.appendNewline()
.append(text("").color(style.prefixColor()))
.append(Component.text(requester).color(NamedTextColor.GRAY)
.append(FriendCommand.friendRequestOptions(requester, false)));
}
return message;
}
public Literal<Player> literal() {
return literal;
}
}

View file

@ -0,0 +1,40 @@
package de.kentoj.scrow.bukkit.command.instance;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrowlib.instancemanager.ServerInstance;
import org.bson.Document;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class InstanceCache {
private final ConcurrentMap<String, ServerInstance> cache = new ConcurrentHashMap<>();
public InstanceCache() {
ScrowAPI.messageBroker().subscribe("event.instance.up", doc -> {
var inst = toInstance(doc);
cache.put(inst.handle(), inst);
});
ScrowAPI.messageBroker().subscribe("event.instance.down", doc ->
cache.remove(doc.getString("handle")));
}
public Collection<ServerInstance> getInstances() {
return cache.values();
}
public @Nullable ServerInstance getInstance(String handle) {
return cache.get(handle);
}
private ServerInstance toInstance(Document doc) {
return new ServerInstance(
doc.getString("handle"),
doc.getString("template"),
doc.getInteger("port")
);
}
}

View file

@ -0,0 +1,25 @@
package de.kentoj.scrow.bukkit.command.instance;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
public final class InstanceCommand {
private final RootLiteral<CommandSender> rootLiteral;
public InstanceCommand(InstanceCache instanceCache) {
var style = new ScrowMessageStyle("server-manager", NamedTextColor.RED);
rootLiteral = Literal.<CommandSender>builder("instance", "server-manager")
.withLiteral(new InstanceListLiteral(style).literal())
.withLiteral(new InstanceDeployLiteral(style).literal())
.withLiteral(new InstanceDestroyLiteral(instanceCache, style).literal())
.asRootLiteral(style);
}
public RootLiteral<CommandSender> rootLiteral() {
return rootLiteral;
}
}

View file

@ -0,0 +1,48 @@
package de.kentoj.scrow.bukkit.command.instance;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.argument.types.StringArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.command.CommandSender;
import java.util.concurrent.CompletableFuture;
public final class InstanceDeployLiteral {
private final Literal<CommandSender> literal;
private final CommandArgumentSpec<CommandSender, String> templateArg;
private final ScrowMessageStyle style;
InstanceDeployLiteral(ScrowMessageStyle style) {
this.style = style;
templateArg = CommandArgumentSpec.builder("template", new StringArgumentType<CommandSender>())
.build();
literal = Literal.<CommandSender>builder("deploy")
.withArgument(templateArg)
.withExecutor(this::deployInstance)
.build();
}
private CompletableFuture<Result<Object, String>> deployInstance(CommandContext<CommandSender> ctx) {
var template = ctx.getArg(templateArg);
ctx.sender().sendMessage(style.ok("Deploying instance... this may take a while."));
return Tasks.supplyAsync(() ->
ScrowAPI.instanceManager().deployInstance(template))
.<Result<Object, String>>mapSync(instance -> {
ctx.sender().sendMessage(style.ok("Deployed instance of " + template + " with handle " + instance.handle() + "."));
// FIXME handle unknown server
return Results.success(new Object());
}).toFuture();
}
public Literal<CommandSender> literal() {
return literal;
}
}

View file

@ -0,0 +1,52 @@
package de.kentoj.scrow.bukkit.command.instance;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.argument.types.StringArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import de.kentoj.scrowlib.instancemanager.ServerInstance;
import org.bukkit.command.CommandSender;
import java.util.concurrent.CompletableFuture;
public final class InstanceDestroyLiteral {
private final Literal<CommandSender> literal;
private final CommandArgumentSpec<CommandSender, String> handleArg;
private final ScrowMessageStyle style;
InstanceDestroyLiteral(InstanceCache cache, ScrowMessageStyle style) {
this.style = style;
handleArg = CommandArgumentSpec.builder("handle", new StringArgumentType<CommandSender>())
.withSuggestionProvider(_ ->
cache.getInstances().stream()
.map(ServerInstance::handle)
.toList())
.build();
literal = Literal.<CommandSender>builder("destroy")
.withArgument(handleArg)
.withExecutor(this::destroyInstance)
.build();
}
private CompletableFuture<Result<Object, String>> destroyInstance(CommandContext<CommandSender> ctx) {
var handle = ctx.getArg(handleArg);
return Tasks.supplyAsync(() -> ScrowAPI.instanceManager().destroyInstance(handle))
.<Result<Object, String>>mapSync(found -> {
if (!found)
return Results.failure("No instance with that handle found.");
ctx.sender().sendMessage(style.ok("Destroyed server with handle " + handle + "."));
return Results.success(new Object());
}).toFuture();
}
public Literal<CommandSender> literal() {
return literal;
}
}

View file

@ -0,0 +1,44 @@
package de.kentoj.scrow.bukkit.command.instance;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender;
import java.util.concurrent.CompletableFuture;
public final class InstanceListLiteral {
private final Literal<CommandSender> literal;
private final ScrowMessageStyle style;
InstanceListLiteral(ScrowMessageStyle style) {
this.style = style;
this.literal = Literal.<CommandSender>builder("list")
.withExecutor(this::displayInstanceList)
.build();
}
private CompletableFuture<Result<Object, String>> displayInstanceList(CommandContext<CommandSender> ctx) {
return Tasks.supplyAsync(() ->
ScrowAPI.instanceManager().instances())
.<Result<Object, String>>mapSync(instances -> {
var msg = Component.text("Running instances:");
for (var inst : instances) {
msg = msg.appendNewline()
.append(Component.text(" - " + inst.handle() + " on port " + inst.port()));
}
ctx.sender().sendMessage(style.ok(msg));
return Results.success(new Object());
}).toFuture();
}
public Literal<CommandSender> literal() {
return literal;
}
}

View file

@ -0,0 +1,30 @@
package de.kentoj.scrow.bukkit.command.instance;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public final class LobbyCommand {
private final RootLiteral<Player> rootLiteral;
public LobbyCommand() {
rootLiteral = Literal.<Player>builder("lobby", "l", "hub")
.withSyncExecutor(this::sendToLobby)
.asRootLiteral(ScrowMessageStyle.GENERIC);
}
private Result<Object, String> sendToLobby(CommandContext<Player> ctx) {
Bukkit.dispatchCommand(ctx.sender(), "play lobby");
return Results.success(new Object());
}
public RootLiteral<Player> rootLiteral() {
return rootLiteral;
}
}

View file

@ -0,0 +1,52 @@
package de.kentoj.scrow.bukkit.command.instance;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.kencommandapi.api.argument.CommandArgumentSpec;
import de.kentoj.kencommandapi.api.argument.types.StringArgumentType;
import de.kentoj.kencommandapi.api.invocation.CommandContext;
import de.kentoj.kencommandapi.api.literal.Literal;
import de.kentoj.kencommandapi.api.literal.RootLiteral;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.scheduler.Tasks;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import org.bukkit.entity.Player;
import java.util.concurrent.CompletableFuture;
public final class PlayCommand {
private final RootLiteral<Player> rootLiteral;
private final CommandArgumentSpec<Player, String> gamemodeArg;
public PlayCommand() {
var style = ScrowMessageStyle.GENERIC;
gamemodeArg = CommandArgumentSpec.builder("gamemode", new StringArgumentType<Player>()).build();
rootLiteral = Literal.<Player>builder("play", "queue")
.withArgument(gamemodeArg)
.withExecutor(this::execute)
.asRootLiteral(style);
}
private CompletableFuture<Result<Object, String>> execute(CommandContext<Player> ctx) {
var gamemode = ctx.getArg(gamemodeArg);
return Tasks.supplyAsync(() -> ScrowAPI.instanceManager().instances())
.mapAsync(instances -> {
var instance = instances.stream()
.filter(inst -> inst.template().equals(gamemode))
.findFirst()
.orElse(null);
if (instance == null)
return Results.failure("No instance for " + gamemode + " found.");
return Results.ofCallable(() -> {
ScrowAPI.getPlayer(ctx.sender()).sendToInstance(instance.handle());
return new Object();
}).mapFailure(Exception::getMessage);
}).toFuture();
}
public RootLiteral<Player> rootLiteral() {
return rootLiteral;
}
}

View file

@ -0,0 +1,59 @@
package de.kentoj.scrow.bukkit.internal.player;
import de.kentoj.scrow.bukkit.ScrowAPI;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bson.Document;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
import java.util.function.BiConsumer;
public class NetworkPlayerWatchdog {
private static final MiniMessage MM = MiniMessage.miniMessage();
public void listen() {
handle("player.sendMessage", (player, doc) -> {
var msg = MM.deserialize(doc.getString("message"));
player.sendMessage(msg);
});
// TODO test methods below
handle("player.sendActionBar", (player, doc) -> {
var msg = MM.deserialize(doc.getString("message"));
player.sendActionBar(msg);
});
handle("player.playSound", (player, doc) ->
player.playSound(toSound(doc)));
handle("player.playSound1", (player, doc) ->
player.playSound(toSound(doc), doc.getDouble("x"), doc.getDouble("y"), doc.getDouble("z")));
handle("player.stopSound", (player, doc) ->
player.stopSound(toSound(doc)));
}
private @NotNull Sound toSound(Document doc) {
@SuppressWarnings("PatternValidation")
var name = Key.key("namespace", doc.getString("name"));
var source = Sound.Source.valueOf(doc.getString("source"));
return Sound.sound(
name,
source,
doc.getDouble("volume").floatValue(),
doc.getDouble("pitch").floatValue());
}
private void handle(String subject, BiConsumer<Player, Document> handler) {
ScrowAPI.messageBroker().subscribe(subject, doc -> {
var player = Bukkit.getPlayer(doc.get("playerId", UUID.class));
if (player != null)
handler.accept(player, doc);
});
}
}

View file

@ -0,0 +1,37 @@
package de.kentoj.scrow.bukkit.internal.player.prefix;
import de.kentoj.scrowlib.player.PlayerPrefixProvider;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.util.*;
public class CachedPrefixProvider implements PlayerPrefixProvider {
private final Map<UUID, @Nullable Component> prefixes = new HashMap<>();
private final List<ChangeListener> listeners = new ArrayList<>();
@Override
public @Nullable Component getPrefix(UUID playerId) {
return prefixes.get(playerId);
}
@Override
public Closeable onChange(ChangeListener changeListener) {
listeners.add(changeListener);
return () -> listeners.remove(changeListener);
}
public void setPrefix(UUID playerId, @Nullable Component prefix) {
if (prefix == null) {
prefixes.remove(playerId);
} else {
var oldValue = prefixes.put(playerId, prefix);
var hasChanged = !Objects.equals(oldValue, prefix);
if (hasChanged) {
listeners.forEach(l -> l.onChange(playerId, prefix));
}
}
}
}

View file

@ -0,0 +1,68 @@
package de.kentoj.scrow.bukkit.internal.player.prefix;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.event.user.UserDataRecalculateEvent;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class LuckPermsPrefixSetter implements Listener {
private static final MiniMessage MM = MiniMessage.miniMessage();
private final LuckPerms luckPerms;
private final CachedPrefixProvider cache;
public LuckPermsPrefixSetter(LuckPerms luckPerms, CachedPrefixProvider cache) {
this.luckPerms = luckPerms;
this.cache = cache;
}
@SuppressWarnings("resource")
public void init(Plugin plugin) {
luckPerms.getEventBus().subscribe(UserDataRecalculateEvent.class, this::onDataChange);
Bukkit.getPluginManager().registerEvents(this, plugin);
}
private void onDataChange(UserDataRecalculateEvent ev) {
var playerId = ev.getUser().getUniqueId();
if (Bukkit.getPlayer(playerId) == null) return;
var prefix = ev.getData().getMetaData().getPrefix();
updatePrefix(playerId, prefix);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlayerJoin(PlayerJoinEvent ev) {
var playerId = ev.getPlayer().getUniqueId();
var user = luckPerms.getUserManager().getUser(playerId);
if (user == null) return;
var prefix = user.getCachedData().getMetaData().getPrefix();
updatePrefix(playerId, prefix);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent ev) {
var playerId = ev.getPlayer().getUniqueId();
cache.setPrefix(playerId, null);
}
private void updatePrefix(UUID playerId, @Nullable String rawPrefix) {
if (rawPrefix == null) {
cache.setPrefix(playerId, null);
return;
}
var prefixComponent = MM.deserialize(rawPrefix);
cache.setPrefix(playerId, prefixComponent);
}
}

View file

@ -0,0 +1,53 @@
package de.kentoj.scrow.bukkit.internal.region;
import de.kentoj.scrow.bukkit.region.ChunkPos;
import de.kentoj.scrow.bukkit.region.Region;
import de.kentoj.scrow.bukkit.region.RegionManager;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class RegionManagerImpl implements RegionManager {
private final Map<Long, List<UUID>> chunkIndex = new HashMap<>();
private final Map<UUID, Region> regions = new HashMap<>();
@Override
public List<Region> getRegions(ChunkPos chunkPos) {
long key = toKey(chunkPos.x(), chunkPos.z());
var uuids = chunkIndex.get(key);
if (uuids == null) return Collections.emptyList();
return uuids.stream().map(regions::get).toList();
}
@Override
public @Nullable Region getRegion(UUID regionUuid) {
return regions.get(regionUuid);
}
@Override
public void registerRegion(Region region) {
for (ChunkPos chunkPos : region.chunks()) {
var key = toKey(chunkPos.x(), chunkPos.z());
chunkIndex.computeIfAbsent(key, __ -> new ArrayList<>())
.add(region.uuid());
}
regions.put(region.uuid(), region);
}
@Override
public void unregisterRegion(Region region) {
for (ChunkPos chunkPos : region.chunks()) {
var key = toKey(chunkPos.x(), chunkPos.z());
var list = chunkIndex.get(key);
if (list == null) return;
list.remove(region.uuid());
}
regions.remove(region.uuid());
}
public long toKey(int x, int z) {
// chatgpt
return (((long) x) << 32) ^ (z & 0xffffffffL);
}
}

View file

@ -0,0 +1,39 @@
package de.kentoj.scrow.bukkit.internal.region;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.region.Region;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class RegionPlayerTracker {
private final Map<UUID, List<UUID>> playersInRegion = new HashMap<>();
public void registerInRegion(Region region, Player player) {
playersInRegion.computeIfAbsent(region.uuid(), __ -> new ArrayList<>())
.add(player.getUniqueId());
}
public void unregisterInRegion(Region region, Player player) {
var uuids = playersInRegion.get(region.uuid());
if (uuids == null) return;
uuids.remove(player.getUniqueId());
}
public Map<Region, Stream<Player>> getRegionsWithPlayers() {
return playersInRegion.entrySet().stream().collect(Collectors.toMap(
ent -> ScrowAPI.regionManager().getRegion(ent.getKey()),
ent -> ent.getValue().stream().map(Bukkit::getPlayer)
));
}
public boolean isInRegion(Region region, Player player) {
var uuids = playersInRegion.get(region.uuid());
if (uuids == null) return false;
return uuids.contains(player.getUniqueId());
}
}

View file

@ -0,0 +1,46 @@
package de.kentoj.scrow.bukkit.internal.region;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrow.bukkit.region.ChunkPos;
import de.kentoj.scrow.bukkit.region.Region;
import de.kentoj.scrow.bukkit.region.event.RegionJoinEvent;
import de.kentoj.scrow.bukkit.region.event.RegionLeaveEvent;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitRunnable;
public class RegionTask extends BukkitRunnable {
private final RegionPlayerTracker playerTracker;
public RegionTask(RegionPlayerTracker playerTracker) {
this.playerTracker = playerTracker;
}
@Override
public void run() {
playerTracker.getRegionsWithPlayers().forEach((region, players) ->
players.forEach(player -> {
if (!region.isInRegion(player.getLocation())) {
var ev = new RegionLeaveEvent(region, player);
Bukkit.getPluginManager().callEvent(ev);
if (!ev.isCancelled())
playerTracker.unregisterInRegion(region, player);
}
})
);
for (var player : Bukkit.getOnlinePlayers()) {
var regions = ScrowAPI.regionManager().getRegions(ChunkPos.fromChunk(player.getLocation().getChunk()));
for (Region region : regions) {
var tracked = playerTracker.isInRegion(region, player);
var inside = region.isInRegion(player.getLocation());
if (!tracked && inside) {
var ev = new RegionJoinEvent(region, player);
Bukkit.getPluginManager().callEvent(ev);
if (!ev.isCancelled())
playerTracker.registerInRegion(region, player);
}
}
}
}
}

View file

@ -0,0 +1,25 @@
package de.kentoj.scrow.bukkit.style;
import de.kentoj.scrowlib.player.PlayerPrefixProvider;
import io.papermc.paper.event.player.AsyncChatEvent;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public final class ChatStyleListener implements Listener {
private final PlayerPrefixProvider prefixProvider;
public ChatStyleListener(PlayerPrefixProvider prefixProvider) {
this.prefixProvider = prefixProvider;
}
@EventHandler
public void onChat(AsyncChatEvent ev) {
var prefix = prefixProvider.getPrefix(ev.getPlayer().getUniqueId());
Component formattedMessage = StyleHelper.formatMessage(ev.getPlayer().name(), prefix, ev.message());
ev.setCancelled(true);
Bukkit.broadcast(formattedMessage);
}
}

View file

@ -0,0 +1,26 @@
package de.kentoj.scrow.bukkit.style;
import de.kentoj.scrowlib.utils.ComponentUtils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.jetbrains.annotations.Nullable;
public class StyleHelper {
private StyleHelper() {}
public static Component formatName(Component playerName, @Nullable Component prefix) {
Component result = prefix != null
? prefix.append(Component.text("").color(NamedTextColor.DARK_GRAY))
: Component.empty();
return result.append(playerName.color(NamedTextColor.GRAY));
}
public static Component formatMessage(Component playerName, @Nullable Component prefix, Component message) {
Component result = prefix != null
? prefix.append(Component.text("", NamedTextColor.DARK_GRAY))
: Component.empty();
return result.append(playerName.color(NamedTextColor.GRAY).append(Component.text(": ")))
.append(ComponentUtils.resolveURLS(message).colorIfAbsent(NamedTextColor.GRAY));
}
}

View file

@ -0,0 +1,37 @@
package de.kentoj.scrow.bukkit.style;
import de.kentoj.scrowlib.player.PlayerPrefixProvider;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public final class TabStyleListener implements Listener {
private final PlayerPrefixProvider prefixProvider;
public TabStyleListener(PlayerPrefixProvider prefixProvider) {
this.prefixProvider = prefixProvider;
//noinspection resource
prefixProvider.onChange(this::onPrefixChange);
}
private void onPrefixChange(UUID playerID, @Nullable Component prefix) {
var player = Bukkit.getPlayer(playerID);
if (player == null) return;
Component formattedName = StyleHelper.formatName(player.name(), prefix);
player.playerListName(formattedName);
}
@EventHandler(ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent ev) {
var prefix = prefixProvider.getPrefix(ev.getPlayer().getUniqueId());
Component formattedName = StyleHelper.formatName(ev.getPlayer().name(), prefix);
ev.getPlayer().playerListName(formattedName);
}
}

View file

@ -0,0 +1,6 @@
name: "CoreBukkit"
version: "${version}"
depend: [ "LuckPerms" ]
main: "de.kentoj.scrow.bukkit.CoreImplPlugin"
api-version: "1.21"
author: "Kento2"

26
core/common/BUILD Normal file
View file

@ -0,0 +1,26 @@
load("@rules_jvm_external//:defs.bzl", "artifact")
load("@rules_java//java:java_library.bzl", "java_library")
shared_deps = [
artifact("com.leakyabstractions:result-api"),
artifact("com.leakyabstractions:result"),
artifact("org.jetbrains:annotations"),
artifact("com.google.guava:guava"),
artifact("org.slf4j.slf4j:api"),
artifact("de.kentoj.scrow:kencommandapi-core"),
artifact("net.kyori:adventure-api"),
artifact("net.kyori:adventure-text-minimessage"),
artifact("org.jdbi:jdbi3-core"),
artifact("org.mongodb:bson"),
]
java_library(
name = "common",
srcs = glob(["src/main/java/**/*.java"]),
visibility = ["//visibility:public"],
deps = shared_deps + [
artifact("org.jdbi:jdbi3-postgres"),
artifact("io.nats:jnats"),
],
exports = shared_deps,
)

View file

@ -0,0 +1,39 @@
package de.kentoj.scrowlib.convention;
import de.kentoj.kencommandapi.api.platform.MessageStyle;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
public record ScrowMessageStyle(
Component prefix,
String prefixText,
TextColor prefixColor
) implements MessageStyle {
public static ScrowMessageStyle GENERIC = new ScrowMessageStyle("Scrow", TextColor.color(0xe5377f));
public ScrowMessageStyle(String prefixText, TextColor prefixColor) {
this(Component.text(prefixText).color(prefixColor), prefixText, prefixColor);
}
@Override
public Component ok(Component msg) {
return prefix.append(Component.text("").color(NamedTextColor.GRAY))
.append(msg.colorIfAbsent(NamedTextColor.GRAY));
}
@Override
public Component err(Component msg) {
return ok(msg.color(NamedTextColor.RED));
}
@Override
public Component exception(Component msg) {
return ok(Component.text("ERROR: ")
.color(NamedTextColor.DARK_RED)
.append(msg)
.decorate(TextDecoration.BOLD));
}
}

View file

@ -0,0 +1,32 @@
package de.kentoj.scrowlib.economy;
import java.util.UUID;
public interface EconomyService {
/**
* @throws IllegalArgumentException if amount is less than 0
*/
int getCoins(UUID playerId);
/**
* adds coins to the players account
* @param playerId uuid of player
* @throws IllegalArgumentException if amount is less than 0
*/
void depositCoins(UUID playerId, int amount);
/**
* tries to take coins from the player's account or returns false on insufficient funds
* @param playerId uuid of player
* @return true on success, false on insufficient funds
* @throws IllegalArgumentException if amount is less than 0
*/
boolean tryWithdrawCoins(UUID playerId, int amount);
/**
* @param playerId uuid of player
* @throws IllegalArgumentException if amount is less than 0
*/
void setCoins(UUID playerId, int amount);
}

View file

@ -0,0 +1,39 @@
package de.kentoj.scrowlib.economy;
import com.google.common.base.Preconditions;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class InMemoryEconomyService implements EconomyService {
private final Map<UUID, Integer> coins = new HashMap<>();
@Override
public int getCoins(UUID playerId) {
return coins.getOrDefault(playerId, 0);
}
@Override
public void setCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
coins.put(playerId, amount);
}
@Override
public void depositCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
var cur = coins.getOrDefault(playerId, 0);
coins.put(playerId, cur + amount);
}
@Override
public boolean tryWithdrawCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
var cur = coins.getOrDefault(playerId, 0);
if (cur < amount) return false;
coins.put(playerId, cur - amount);
return true;
}
}

View file

@ -0,0 +1,82 @@
package de.kentoj.scrowlib.economy;
import org.jdbi.v3.core.Jdbi;
import java.util.UUID;
import static com.google.common.base.Preconditions.checkArgument;
// TODO log operations to separate table
public class PostgresEconomyService implements EconomyService {
private final Jdbi jdbi;
public PostgresEconomyService(Jdbi jdbi) {
this.jdbi = jdbi;
jdbi.withHandle(h -> h.execute("""
CREATE TABLE IF NOT EXISTS economy (
playerId UUID NOT NULL,
balance INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(playerId)
)
"""));
}
@Override
public int getCoins(UUID playerId) {
return jdbi.withHandle(h -> h
.createQuery("SELECT balance FROM economy WHERE playerId = :playerId")
.bind("playerId", playerId)
.map(row -> row.getColumn("balance", Integer.class))
.findOne()
.orElse(0));
}
@Override
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)
DO UPDATE
SET balance = :amount
WHERE economy.playerId = :playerId
""")
.bind("playerId", playerId)
.bind("amount", amount)
.execute());
}
@Override
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)
DO UPDATE
SET balance = economy.balance + EXCLUDED.balance
WHERE economy.playerId = :playerId
""")
.bind("playerId", playerId)
.bind("amount", amount)
.execute());
}
@Override
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)
.execute() > 0);
}
}

View file

@ -0,0 +1,10 @@
package de.kentoj.scrowlib.friends;
import java.time.Instant;
import java.util.UUID;
public interface FriendRequest {
UUID from();
UUID to();
Instant createdAt();
}

View file

@ -0,0 +1,23 @@
package de.kentoj.scrowlib.friends;
import java.util.List;
import java.util.UUID;
public interface FriendRequestService {
List<FriendRequest> getFriendRequestsTo(UUID playerId);
List<FriendRequest> getFriendRequestsFrom(UUID playerId);
FriendRequest getFriendRequest(UUID from, UUID to);
/**
* @return True if request saved, false if already existing request found.
*/
boolean saveFriendRequest(FriendRequest friendRequest);
/**
* @return True if request deleted, false if none found.
*/
boolean deleteFriendRequest(UUID from, UUID to);
}

View file

@ -0,0 +1,10 @@
package de.kentoj.scrowlib.friends;
import java.time.Instant;
public interface Friendship {
OrderedUUIDPair uuids();
Instant createdAt();
}

View file

@ -0,0 +1,21 @@
package de.kentoj.scrowlib.friends;
import java.util.List;
import java.util.UUID;
public interface FriendshipService {
Friendship getFriendship(UUID uuid1, UUID uuid2);
List<Friendship> getFriendships(UUID playerId);
/*
* @return True if friendship saved, false if friendship already exists
*/
boolean saveFriendship(Friendship playerIds);
/**
* @return True if friendship deleted, false if none found
*/
boolean deleteFriendship(UUID uuid1, UUID uuid2);
}

View file

@ -0,0 +1,24 @@
package de.kentoj.scrowlib.friends;
import java.util.UUID;
public record OrderedUUIDPair(
UUID first,
UUID second
) {
public static OrderedUUIDPair of(UUID a, UUID b) {
UUID first, second;
if (a.compareTo(b) < 0) {
first = a;
second = b;
} else {
first = b;
second = a;
}
return new OrderedUUIDPair(first, second);
}
public UUID getOther(UUID self) {
return first().equals(self) ? second() : first();
}
}

View file

@ -0,0 +1,16 @@
package de.kentoj.scrowlib.friends.friendship;
import de.kentoj.scrowlib.friends.Friendship;
import de.kentoj.scrowlib.friends.OrderedUUIDPair;
import java.time.Instant;
import java.util.UUID;
public record FriendshipImpl(
OrderedUUIDPair uuids,
Instant createdAt
) implements Friendship {
public FriendshipImpl(UUID uuid1, UUID uuid2, Instant createdAt) {
this(OrderedUUIDPair.of(uuid1, uuid2), createdAt);
}
}

View file

@ -0,0 +1,40 @@
package de.kentoj.scrowlib.friends.friendship;
import de.kentoj.scrowlib.friends.Friendship;
import de.kentoj.scrowlib.friends.FriendshipService;
import de.kentoj.scrowlib.friends.OrderedUUIDPair;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class InMemoryFriendshipService implements FriendshipService {
private final Map<OrderedUUIDPair, Friendship> friendships = new HashMap<>();
@Override
public Friendship getFriendship(UUID uuid1, UUID uuid2) {
return friendships.get(OrderedUUIDPair.of(uuid1, uuid2));
}
@Override
public List<Friendship> getFriendships(UUID playerId) {
return friendships.entrySet().stream()
.filter(ent ->
ent.getKey().first().equals(playerId) || ent.getKey().second().equals(playerId)
)
.map(Map.Entry::getValue)
.toList();
}
@Override
public boolean saveFriendship(Friendship friendship) {
return friendships.putIfAbsent(friendship.uuids(), friendship) == null;
}
@Override
public boolean deleteFriendship(UUID uuid1, UUID uuid2) {
return friendships.remove(OrderedUUIDPair.of(uuid1, uuid2)) != null;
}
}

View file

@ -0,0 +1,96 @@
package de.kentoj.scrowlib.friends.friendship;
import de.kentoj.scrowlib.friends.Friendship;
import de.kentoj.scrowlib.friends.FriendshipService;
import de.kentoj.scrowlib.friends.OrderedUUIDPair;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.statement.StatementContext;
import org.jetbrains.annotations.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public class PostgresFriendshipService implements FriendshipService {
private final Jdbi jdbi;
public PostgresFriendshipService(Jdbi jdbi) {
this.jdbi = jdbi;
jdbi.useHandle(h -> h.execute("""
CREATE TABLE IF NOT EXISTS friendships (
first_uuid UUID NOT NULL,
second_uuid UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (first_uuid, second_uuid),
CONSTRAINT friendship_no_self_friendship
CHECK (first_uuid <> second_uuid)
)
"""));
}
@Override
public @Nullable Friendship getFriendship(UUID uuid1, UUID uuid2) {
var uuids = OrderedUUIDPair.of(uuid1, uuid2);
return jdbi.withHandle(h -> h.createQuery("""
SELECT first_uuid, second_uuid, created_at
FROM friendships
WHERE first_uuid = :first AND second_uuid = :second
""")
.bind("first", uuids.first())
.bind("second", uuids.second())
.map(this::parseFriendship)
.findFirst().orElse(null));
}
@Override
public List<Friendship> getFriendships(UUID playerId) {
return jdbi.withHandle(h -> h.createQuery("""
SELECT first_uuid, second_uuid, created_at
FROM friendships
WHERE first_uuid = :playerId OR second_uuid = :playerId
""")
.bind("playerId", playerId)
.map(this::parseFriendship)
.list());
}
@Override
public boolean saveFriendship(Friendship friendship) {
return jdbi.withHandle(h -> h.createUpdate("""
INSERT INTO friendships (first_uuid, second_uuid, created_at)
VALUES (:first, :second, :createdAt)
ON CONFLICT (first_uuid, second_uuid) DO NOTHING
""")
.bind("first", friendship.uuids().first())
.bind("second", friendship.uuids().second())
.bind("createdAt", friendship.createdAt())
.execute() > 0);
}
@Override
public boolean deleteFriendship(UUID uuid1, UUID uuid2) {
var uuids = OrderedUUIDPair.of(uuid1, uuid2);
return jdbi.withHandle(h -> h.createUpdate("""
DELETE FROM friendships
WHERE first_uuid = :first AND second_uuid = :second
""")
.bind("first", uuids.first())
.bind("second", uuids.second())
.execute() > 0);
}
private Friendship parseFriendship(ResultSet rs, StatementContext __) throws SQLException {
for (int i = 0; i < 5; i++) {
System.out.println("parsing friendship");
}
return new FriendshipImpl(
rs.getObject("first_uuid", UUID.class),
rs.getObject("second_uuid", UUID.class),
rs.getObject("created_at", OffsetDateTime.class).toInstant()
);
}
}

View file

@ -0,0 +1,16 @@
package de.kentoj.scrowlib.friends.request;
import de.kentoj.scrowlib.friends.FriendRequest;
import java.time.Instant;
import java.util.UUID;
public record FriendRequestImpl(
UUID from,
UUID to,
Instant createdAt
) implements FriendRequest {
public FriendRequestImpl(UUID from, UUID to) {
this(from, to, Instant.now());
}
}

View file

@ -0,0 +1,46 @@
package de.kentoj.scrowlib.friends.request;
import de.kentoj.scrowlib.friends.FriendRequest;
import de.kentoj.scrowlib.friends.FriendRequestService;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class InMemoryFriendRequestService implements FriendRequestService {
private final Set<FriendRequest> requests = new HashSet<>();
@Override
public List<FriendRequest> getFriendRequestsTo(UUID playerId) {
return requests.stream()
.filter(req -> req.to().equals(playerId))
.toList();
}
@Override
public List<FriendRequest> getFriendRequestsFrom(UUID playerId) {
return requests.stream()
.filter(req -> req.from().equals(playerId))
.toList();
}
@Override
public FriendRequest getFriendRequest(UUID from, UUID to) {
return requests.stream()
.filter(req -> req.from().equals(from) && req.to().equals(to))
.findFirst()
.orElse(null);
}
@Override
public boolean saveFriendRequest(FriendRequest friendRequest) {
return requests.add(friendRequest);
}
@Override
public boolean deleteFriendRequest(UUID from, UUID to) {
return requests.removeIf(req -> req.from().equals(from) && req.to().equals(to));
}
}

View file

@ -0,0 +1,102 @@
package de.kentoj.scrowlib.friends.request;
import de.kentoj.scrowlib.friends.FriendRequest;
import de.kentoj.scrowlib.friends.FriendRequestService;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.statement.StatementContext;
import org.jetbrains.annotations.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public class PostgresFriendRequestService implements FriendRequestService {
private final Jdbi jdbi;
public PostgresFriendRequestService(Jdbi jdbi) {
this.jdbi = jdbi;
jdbi.useHandle(h -> h.execute("""
CREATE TABLE IF NOT EXISTS friendrequests (
from_uuid UUID NOT NULL,
to_uuid UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (from_uuid, to_uuid),
CONSTRAINT friendship_no_self_request
CHECK (from_uuid <> to_uuid)
)
"""));
}
@Override
public @Nullable FriendRequest getFriendRequest(UUID from, UUID to) {
return jdbi.withHandle(h -> h.createQuery("""
SELECT *
FROM friendrequests
WHERE from_uuid = :from AND to_uuid = :to
""")
.bind("from", from)
.bind("to", to)
.map(this::parseFriendRequest)
.findFirst()
.orElse(null));
}
@Override
public boolean saveFriendRequest(FriendRequest friendRequest) {
return jdbi.withHandle(h -> h.createUpdate("""
INSERT INTO friendrequests (from_uuid, to_uuid, created_at)
VALUES (:from, :to, :createdAt)
""")
.bind("from", friendRequest.from())
.bind("to", friendRequest.to())
.bind("createdAt", friendRequest.createdAt())
.execute() > 0);
}
@Override
public List<FriendRequest> getFriendRequestsTo(UUID playerId) {
return jdbi.withHandle(h -> h.createQuery("""
SELECT *
FROM friendrequests
WHERE to_uuid = :to
""")
.bind("to", playerId)
.map(this::parseFriendRequest)
.collectIntoList());
}
@Override
public List<FriendRequest> getFriendRequestsFrom(UUID playerId) {
return jdbi.withHandle(h -> h.createQuery("""
SELECT *
FROM friendrequests
WHERE from_uuid = :from
""")
.bind("from", playerId)
.map(this::parseFriendRequest)
.collectIntoList());
}
@Override
public boolean deleteFriendRequest(UUID from, UUID to) {
return jdbi.withHandle(h -> h.createUpdate("""
DELETE
FROM friendrequests
WHERE from_uuid = :from AND to_uuid = :to
""")
.bind("from", from)
.bind("to", to)
.execute() > 0);
}
private FriendRequest parseFriendRequest(ResultSet rs, StatementContext __) throws SQLException {
return new FriendRequestImpl(
rs.getObject("from_uuid", UUID.class),
rs.getObject("to_uuid", UUID.class),
rs.getObject("created_at", OffsetDateTime.class).toInstant()
);
}
}

View file

@ -0,0 +1,34 @@
package de.kentoj.scrowlib.instancemanager;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public interface InstanceManager {
/**
* Runs blocking.
* Deploys an instance of template.
*/
ServerInstance deployInstance(String template);
/**
* Runs blocking.
*
* @return if the instance was found
*/
boolean destroyInstance(String handle);
/**
* Runs blocking.
*
* @return list of running instances
*/
List<ServerInstance> instances();
/**
* Runs blocking
*/
@Nullable
ServerInstance getInstance(String handle);
}

View file

@ -0,0 +1,53 @@
package de.kentoj.scrowlib.instancemanager;
import de.kentoj.scrowlib.messaging.MessageBroker;
import de.kentoj.scrowlib.messaging.RemoteException;
import org.bson.Document;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class InstanceManagerImpl implements InstanceManager {
private final MessageBroker messageBroker;
public InstanceManagerImpl(MessageBroker messageBroker) {
this.messageBroker = messageBroker;
}
@Override
public ServerInstance deployInstance(String template) {
var document = messageBroker.request("instance.deploy", new Document("template", template));
return ServerInstance.fromDocument(document);
}
@Override
public boolean destroyInstance(String handle) {
try {
messageBroker.request("instance.destroy", new Document("handle", handle));
return true;
} catch (RemoteException ex) {
if (wasNotFound(ex.getMessage())) return false;
throw ex;
}
}
@Override
public List<ServerInstance> instances() {
var document = messageBroker.request("instance.list", new Document());
return document.getList("instances", Document.class)
.stream()
.map(ServerInstance::fromDocument)
.toList();
}
@Override
public @Nullable ServerInstance getInstance(String handle) {
var document = messageBroker.request("instance.get", new Document("handle", handle));
return ServerInstance.fromDocument(document);
}
private boolean wasNotFound(String failure) {
return "no instance found".equals(failure);
}
}

View file

@ -0,0 +1,24 @@
package de.kentoj.scrowlib.instancemanager;
import org.bson.Document;
import static com.google.common.base.Preconditions.checkNotNull;
public record ServerInstance(
String handle,
String template,
int port
) {
public ServerInstance {
checkNotNull(template);
checkNotNull(handle);
}
static ServerInstance fromDocument(Document document) {
return new ServerInstance(
document.getString("handle"),
document.getString("template"),
document.getInteger("port")
);
}
}

View file

@ -0,0 +1,54 @@
package de.kentoj.scrowlib.messaging;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
public class InMemoyMessageBroker implements MessageBroker {
private final Logger log = LoggerFactory.getLogger(InMemoyMessageBroker.class);
private final Map<String, List<Consumer<Document>>> subscribers = new HashMap<>();
private final Map<String, RequestHandler> handlers = new HashMap<>();
@Override
public void publish(String subject, Document document) {
var subs = subscribers.get(subject);
if (subs == null) return;
subs.forEach(s -> s.accept(document));
}
@Override
public void subscribe(String subject, Consumer<Document> consumer) {
subscribers.computeIfAbsent(subject, _ -> new ArrayList<>())
.add(consumer);
}
@Override
public void handle(String subject, RequestHandler handler) {
var oldValue = handlers.put(subject, handler);
if (oldValue != null) log.warn("handler overwrites previous handler for subject " + subject);
}
@Override
public Document request(String subject, Document document) {
var handler = handlers.get(subject);
if (handler == null)
throw new RuntimeException("No request handler for " + subject + " found");
try {
return handler.handle(document).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,42 @@
package de.kentoj.scrowlib.messaging;
import org.bson.Document;
import java.util.function.Consumer;
public interface MessageBroker {
/**
* Runs blocking.
* publishes to the message broker
*
* @param subject <a href="https://docs.nats.io/nats-concepts/subjects">subject</a> to publish to
*/
void publish(String subject, Document document);
/**
* May block until subscription registered.
* subscribes to a subject, consumer may run asynchronously
*
* @param subject <a href="https://docs.nats.io/nats-concepts/subjects">subject</a> to publish to
*/
void subscribe(String subject, Consumer<Document> consumer);
/**
* Registers a handler for a subject, the handler will receive requests and compute some result.
* The result is sent back as a reply.
*
* @param subject <a href="https://docs.nats.io/nats-concepts/subjects">subject</a> to publish to
*/
void handle(String subject, RequestHandler handler);
/**
* Runs blocking.
* Executes an RPC call: The document is published and this method starts listening for incoming
* replies. The returned Result holds either a document with the requested data, or an error as a string
*
* @param subject <a href="https://docs.nats.io/nats-concepts/subjects">subject</a> to publish to
* @return RemoteException if the remote throws an exception while handling the request
*/
Document request(String subject, Document document);
}

View file

@ -0,0 +1,80 @@
package de.kentoj.scrowlib.messaging;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import de.kentoj.scrowlib.utils.DocumentRepository;
import io.nats.client.Connection;
import io.nats.client.Dispatcher;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
public class NatsMessageBroker implements MessageBroker {
private final Logger log = LoggerFactory.getLogger(NatsMessageBroker.class);
private static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
private final Connection nats;
private final Dispatcher dispatcher;
public NatsMessageBroker(Connection nats) {
this.nats = nats;
this.dispatcher = nats.createDispatcher();
}
@Override
public void publish(String subject, Document document) {
nats.publish(subject, DocumentRepository.toBytes(document));
}
@Override
public void subscribe(String subject, Consumer<Document> consumer) {
dispatcher.subscribe(subject, msg ->
EXECUTOR.submit(() -> consumer.accept(DocumentRepository.fromBytes(msg.getData()))));
}
@Override
public void handle(String subject, RequestHandler handler) {
dispatcher.subscribe(subject, msg -> {
if (msg.getReplyTo() == null) {
log.warn("request in {} has replyTo unset; ignoring", subject);
return;
}
Result<Document, String> result;
try {
var reply = handler.handle(DocumentRepository.fromBytes(msg.getData())).get();
result = Results.success(reply);
} catch (ExecutionException ex) {
result = Results.failure(ex.getCause().getMessage());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw ex;
}
final var finalResult = result;
EXECUTOR.submit(() -> publish(msg.getReplyTo(), ResultRepository.toDocument(finalResult)));
});
}
@Override
public Document request(String subject, Document document) {
var reply = nats.request(subject, DocumentRepository.toBytes(document)).join();
var result = ResultRepository.fromDocument(DocumentRepository.fromBytes(reply.getData()));
return unwrap(result);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
private static <S> S unwrap(Result<S, String> result) {
Optional<String> err = result.getFailure();
if (err.isPresent())
throw new RemoteException(err.get());
return result.getSuccess().get();
}
}

View file

@ -0,0 +1,7 @@
package de.kentoj.scrowlib.messaging;
public class RemoteException extends RuntimeException {
public RemoteException(String message) {
super(message);
}
}

View file

@ -0,0 +1,15 @@
package de.kentoj.scrowlib.messaging;
import org.bson.Document;
import java.util.concurrent.Future;
@FunctionalInterface
public interface RequestHandler {
/**
* Processes a request and replies
*
* @return document to reply with
*/
Future<Document> handle(Document doc);
}

View file

@ -0,0 +1,25 @@
package de.kentoj.scrowlib.messaging;
import com.leakyabstractions.result.api.Result;
import com.leakyabstractions.result.core.Results;
import org.bson.Document;
class ResultRepository {
private ResultRepository() {
}
static Document toDocument(Result<Document, String> result) {
return result.hasSuccess()
? new Document("data", result.getSuccess().orElseThrow())
: new Document("error", result.getFailure().orElseThrow());
}
static Result<Document, String> fromDocument(Document doc) {
var error = doc.getString("error");
return error != null
? Results.failure(error)
: Results.success(doc.get("data", Document.class));
}
}

View file

@ -0,0 +1,15 @@
package de.kentoj.scrowlib.messaging;
import org.bson.Document;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public interface SyncRequestHandler extends RequestHandler {
Document handleSync(Document doc);
@Override
default Future<Document> handle(Document doc) {
return CompletableFuture.completedFuture(handleSync(doc));
}
}

View file

@ -0,0 +1,24 @@
package de.kentoj.scrowlib.player;
import de.kentoj.scrowlib.messaging.MessageBroker;
import net.kyori.adventure.audience.Audience;
/**
* All methods here are NON-blocking.
* NetworkPlayer represents a Player that MAY be online on any of the minecraft, wrapped
* in an {@link Audience}
* servers in the network.
* <p>
* If the player is not online, these methods DO NOTHING.
* Most methods here do nothing even if the Player is valid(as is the nature of Audiences)
* <p>
* this interface should be used mainly for basic communication(e.g. sending messages).
* For more complex operations, use the {@link MessageBroker}.
*
* @see NetworkPlayerImpl
* @see MessageBroker
*/
public interface NetworkPlayer extends Audience {
void sendToInstance(String handle);
}

View file

@ -0,0 +1,8 @@
package de.kentoj.scrowlib.player;
import java.util.UUID;
@FunctionalInterface
public interface NetworkPlayerFactory {
NetworkPlayer create(UUID uuid);
}

View file

@ -0,0 +1,69 @@
package de.kentoj.scrowlib.player;
import de.kentoj.scrowlib.messaging.MessageBroker;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bson.Document;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NetworkPlayerImpl implements NetworkPlayer {
private static final MiniMessage MM = MiniMessage.miniMessage();
private static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
private final MessageBroker messageBroker;
private final UUID playerId;
public NetworkPlayerImpl(MessageBroker messageBroker, UUID playerId) {
this.messageBroker = messageBroker;
this.playerId = playerId;
}
@Override
public void sendMessage(@NotNull Component message) {
requestAsync("player.sendMessage", new Document("message", MM.serialize(message)));
}
@Override
public void sendActionBar(@NotNull Component message) {
requestAsync("player.sendActionBar", new Document("message", MM.serialize(message)));
}
@Override
public void playSound(@NotNull Sound sound) {
requestAsync("player.playSound", new Document("message", toDocument(sound)));
}
@Override
public void playSound(@NotNull Sound sound, double x, double y, double z) {
requestAsync("player.playSound1", new Document("message", toDocument(sound)
.append("x", x)
.append("y", y)
.append("z", z)));
}
@Override
public void stopSound(@NotNull Sound sound) {
requestAsync("player.stopSound", toDocument(sound));
}
private Document toDocument(Sound sound) {
return new Document("name", sound.name().value())
.append("pitch", sound.pitch())
.append("volume", sound.volume())
.append("source", sound.source().name());
}
@Override
public void sendToInstance(String handle) {
requestAsync("player.send", new Document("handle", handle));
}
private void requestAsync(String topic, Document document) {
EXECUTOR.submit(() -> messageBroker.publish(topic, document
.append("playerId", playerId)));
}
}

View file

@ -0,0 +1,18 @@
package de.kentoj.scrowlib.player;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.util.UUID;
public interface PlayerPrefixProvider {
@Nullable Component getPrefix(UUID playerId);
Closeable onChange(ChangeListener onChange);
interface ChangeListener {
void onChange(UUID playerId, @Nullable Component newPrefix);
}
}

View file

@ -0,0 +1,24 @@
package de.kentoj.scrowlib.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.event.ClickEvent;
import java.util.regex.Pattern;
public class ComponentUtils {
private static final Pattern URL_REGEX = Pattern.compile("(https?://)?[a-z0-9]+(\\.[a-z0-9]+)*(\\.[a-z0-9]{1,10})((/+)[^/ ]*)*", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
private static final TextReplacementConfig REPLACER = TextReplacementConfig.builder()
.match(URL_REGEX)
.replacement((c) -> c.clickEvent(ClickEvent.openUrl(c.content().startsWith("http") ? c.content() : "https://" + c.content())))
.build();
private ComponentUtils() {
}
public static Component resolveURLS(Component component) {
return component.replaceText(REPLACER);
}
}

View file

@ -0,0 +1,20 @@
package de.kentoj.scrowlib.utils;
import org.bson.Document;
import org.jetbrains.annotations.ApiStatus;
public class DocumentRepository {
private DocumentRepository() {
}
@ApiStatus.Obsolete
public static Document fromBytes(byte[] bytes) {
return Document.parse(new String(bytes));
}
@ApiStatus.Obsolete
public static byte[] toBytes(Document document) {
return document.toJson().getBytes();
}
}

View file

@ -0,0 +1,16 @@
package de.kentoj.scrowlib.utils;
public class EnvUtils {
private EnvUtils() {
}
/**
* @throws IllegalStateException if the var is not set
*/
public static String envOrThrow(String name) {
var env = System.getenv(name);
if (env == null) throw new IllegalStateException("environment variable " + name + " not set");
return env;
}
}

31
core/velocity/BUILD Normal file
View file

@ -0,0 +1,31 @@
load("@rules_jvm_external//:defs.bzl", "artifact")
load("@rules_java//java:java_library.bzl", "java_library")
load("@rules_java//java:java_binary.bzl", "java_binary")
java_library(
name = "api",
srcs = glob(["api/main/java/**/*.java"]),
visibility = ["//visibility:public"],
deps = [
"//core/common",
artifact("com.velocitypowered:velocity-api"),
artifact("de.kentoj.scrow:kencommandapi-velocity"),
],
exports = [
"//core/common",
artifact("de.kentoj.scrow:kencommandapi-velocity"),
artifact("org.mongodb:bson"),
],
)
java_binary(
name = "plugin",
srcs = glob(["plugin/main/java/**/*.java"]),
create_executable = False,
deps = [
":api",
artifact("com.velocitypowered:velocity-api"),
artifact("com.google.inject:guice"),
artifact("io.nats:jnats"),
],
)

View file

@ -0,0 +1,85 @@
package de.kentoj.scrow.velocity;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import de.kentoj.kencommandapi.CommandAPI;
import de.kentoj.scrowlib.instancemanager.InstanceManager;
import de.kentoj.scrowlib.messaging.MessageBroker;
import de.kentoj.scrowlib.player.NetworkPlayer;
import de.kentoj.scrowlib.player.NetworkPlayerFactory;
import org.jdbi.v3.core.Jdbi;
import org.jetbrains.annotations.ApiStatus;
import java.util.UUID;
public class ScrowAPI {
private static Jdbi jdbi;
private static CommandAPI<CommandSource> commands;
private static CommandAPI<Player> playerCommands;
private static MessageBroker messageBroker;
private static InstanceManager instanceManager;
private static NetworkPlayerFactory playerFactory;
public static NetworkPlayer getPlayer(Player player) {
return getPlayer(player.getUniqueId());
}
public static NetworkPlayer getPlayer(UUID uuid) {
return playerFactory.create(uuid);
}
@ApiStatus.Internal
public static void initMessageBroker(MessageBroker messageBroker) {
ScrowAPI.messageBroker = messageBroker;
}
@ApiStatus.Internal
public static void initInstanceManager(InstanceManager instanceManager) {
ScrowAPI.instanceManager = instanceManager;
}
@ApiStatus.Internal
public static void initJdbi(Jdbi jdbi) {
ScrowAPI.jdbi = jdbi;
}
@ApiStatus.Internal
public static void initPlayerFactory(NetworkPlayerFactory playerFactory) {
ScrowAPI.playerFactory = playerFactory;
}
@ApiStatus.Internal
public static void initPlayerCommands(CommandAPI<Player> playerCommands) {
ScrowAPI.playerCommands = playerCommands;
}
@ApiStatus.Internal
public static void initCommands(CommandAPI<CommandSource> commands) {
ScrowAPI.commands = commands;
}
public static Jdbi jdbi() {
return jdbi;
}
public static CommandAPI<CommandSource> commands() {
return commands;
}
public static CommandAPI<Player> playerCommands() {
return playerCommands;
}
public static MessageBroker messageBroker() {
return messageBroker;
}
public static InstanceManager instanceManager() {
return instanceManager;
}
public static NetworkPlayerFactory playerFactory() {
return playerFactory;
}
}

View file

@ -0,0 +1,49 @@
package de.kentoj.scrow.corevelocity;
import com.google.inject.Inject;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.ProxyServer;
import de.kentoj.scrow.corevelocity.misc.OnlineCommand;
import de.kentoj.scrow.corevelocity.network.FriendNotifications;
import de.kentoj.scrow.corevelocity.network.SendPlayerWatchdog;
import de.kentoj.scrow.corevelocity.network.ServerRegisterWatchdog;
import de.kentoj.scrow.corevelocity.privmsg.LastTargetCache;
import de.kentoj.scrow.corevelocity.privmsg.MsgCommand;
import de.kentoj.scrow.corevelocity.privmsg.PrivMsgHandler;
import de.kentoj.scrow.corevelocity.privmsg.ReplyCommand;
import de.kentoj.scrow.velocity.ScrowAPI;
import de.kentoj.scrowlib.convention.ScrowMessageStyle;
import net.kyori.adventure.text.format.NamedTextColor;
@Plugin(
id = "corevelocity",
name = "CoreVelocity",
authors = {"gradlehater57"},
version = "0"
)
public final class CoreVelocityPlugin {
@Inject
private ProxyServer server;
@Subscribe
private void onInit(ProxyInitializeEvent __) {
ScrowAPISurface.initScrowAPI(server);
var lastTargetCache = new LastTargetCache();
var style = new ScrowMessageStyle("MSG", NamedTextColor.LIGHT_PURPLE);
var privmsgHelper = new PrivMsgHandler(style);
ScrowAPI.playerCommands().register(new ReplyCommand(server, lastTargetCache, privmsgHelper, style).rootLiteral());
ScrowAPI.playerCommands().register(new MsgCommand(server, lastTargetCache, privmsgHelper, style).rootLiteral());
ScrowAPI.commands().register(new OnlineCommand(server).rootLiteral());
var registerWatchdog = new ServerRegisterWatchdog(server);
var sendPlayerWatchdog = new SendPlayerWatchdog(server);
registerWatchdog.listen();
sendPlayerWatchdog.listen();
server.getEventManager().register(this, new FriendNotifications());
}
}

View file

@ -0,0 +1,64 @@
package de.kentoj.scrow.corevelocity;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import de.kentoj.kencommandapi.CommandAPI;
import de.kentoj.scrow.velocity.ScrowAPI;
import de.kentoj.scrowlib.instancemanager.InstanceManagerImpl;
import de.kentoj.scrowlib.messaging.InMemoyMessageBroker;
import de.kentoj.scrowlib.messaging.NatsMessageBroker;
import de.kentoj.scrowlib.player.NetworkPlayerImpl;
import io.nats.client.Nats;
import io.nats.client.Options;
import org.jdbi.v3.core.Jdbi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Properties;
public class ScrowAPISurface {
private static final Logger log = LoggerFactory.getLogger(ScrowAPISurface.class);
public static void initScrowAPI(ProxyServer server) {
var natsHost = System.getenv("HOST_NATS");
if (natsHost != null) {
try {
var options = Options.builder()
.server(natsHost)
.pedantic()
.build();
ScrowAPI.initMessageBroker(new NatsMessageBroker(Nats.connect(options)));
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
} else {
log.error("HOST_NATS not set -> Using in-memory dummy message broker");
ScrowAPI.initMessageBroker(new InMemoyMessageBroker());
}
var hostPostgres = System.getenv().get("HOST_POSTGRES");
if (hostPostgres != null) {
try {
Class.forName("org.postgresql.Driver", true, ScrowAPISurface.class.getClassLoader());
var props = new Properties();
props.setProperty("user", "postgres");
ScrowAPI.initJdbi(Jdbi.create(hostPostgres, props));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
} else {
log.error("HOST_POSTGRES not set -> Using in-memory database");
}
ScrowAPI.initInstanceManager(new InstanceManagerImpl(ScrowAPI.messageBroker()));
ScrowAPI.initCommands(new CommandAPI<>(server, CommandSource.class));
ScrowAPI.initPlayerCommands(new CommandAPI<>(server, Player.class));
ScrowAPI.initPlayerFactory(playerId -> new NetworkPlayerImpl(ScrowAPI.messageBroker(), playerId));
}
public static void destroyScrowAPI() {
}
}

Some files were not shown because too many files have changed in this diff Show more