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

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;
}
}