.
This commit is contained in:
parent
670eba418c
commit
13ced30434
122 changed files with 223 additions and 12 deletions
26
core/common/BUILD
Normal file
26
core/common/BUILD
Normal 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,
|
||||
)
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package de.kentoj.scrowlib.friends;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public interface Friendship {
|
||||
|
||||
OrderedUUIDPair uuids();
|
||||
|
||||
Instant createdAt();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package de.kentoj.scrowlib.messaging;
|
||||
|
||||
public class RemoteException extends RuntimeException {
|
||||
public RemoteException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package de.kentoj.scrowlib.player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface NetworkPlayerFactory {
|
||||
NetworkPlayer create(UUID uuid);
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue