gradle time

This commit is contained in:
kento2 2026-07-05 17:44:01 +02:00
parent 7040a6ce1e
commit 9421ef511e
47 changed files with 482 additions and 226 deletions

View file

@ -24,6 +24,7 @@ dependencies {
api("de.kentoj.scrow:kencommandapi-core:0.30")
compileOnly("net.kyori:adventure-api:5.2.0")
compileOnly("net.kyori:adventure-text-minimessage:5.2.0")
}
publishing {

View file

@ -0,0 +1,30 @@
package de.kentoj.scrowlib.economy;
import java.util.UUID;
public interface EconomyService {
/**
* @throws IllegalArgumentException if amount is less than 0
*/
int getCoins(UUID playerId);
/**
* @param playerId uuid of player
* @throws IllegalArgumentException if amount is less than 0
*/
void depositCoins(UUID playerId, int amount);
/**
* @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,85 @@
package de.kentoj.scrowlib.economy;
import de.kentoj.scrow.bukkit.ScrowAPI;
import org.jdbi.v3.core.Jdbi;
import java.util.UUID;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
// TODO log operations to separate table
public class PostgresEconomyService implements EconomyService {
private final Jdbi jdbi;
public PostgresEconomyService() {
checkNotNull(ScrowAPI.getJdbi());
this.jdbi = ScrowAPI.getJdbi();
jdbi.withHandle(h -> h.execute("""
CREATE TABLE IF NOT EXISTS economy (
playerId UUID NOT NULL,
balance INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(playerId)
)
"""));
}
@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 getFrom();
UUID getTo();
Instant getCreatedAt();
}

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 getUuids();
Instant getCreatedAt();
}

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,25 @@
package de.kentoj.scrowlib.friends;
import lombok.Getter;
import java.util.UUID;
@Getter
public class OrderedUUIDPair {
private final UUID first;
private final UUID second;
public OrderedUUIDPair(UUID uuid1, UUID uuid2) {
if (uuid1.compareTo(uuid2) < 0) {
this.first = uuid1;
this.second = uuid2;
} else {
this.first = uuid2;
this.second = uuid1;
}
}
public UUID getOther(UUID self) {
return getFirst().equals(self) ? getSecond() : getFirst();
}
}

View file

@ -0,0 +1,20 @@
package de.kentoj.scrowlib.friends.friendship;
import de.kentoj.scrowlib.friends.Friendship;
import de.kentoj.scrowlib.friends.OrderedUUIDPair;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.time.Instant;
import java.util.UUID;
@Value
@RequiredArgsConstructor
public class FriendshipImpl implements Friendship {
OrderedUUIDPair uuids;
Instant createdAt;
public FriendshipImpl(UUID uuid1, UUID uuid2, Instant createdAt) {
this(new OrderedUUIDPair(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(new OrderedUUIDPair(uuid1, uuid2));
}
@Override
public List<Friendship> getFriendships(UUID playerId) {
return friendships.entrySet().stream()
.filter(ent ->
ent.getKey().getFirst().equals(playerId) || ent.getKey().getSecond().equals(playerId)
)
.map(Map.Entry::getValue)
.toList();
}
@Override
public boolean saveFriendship(Friendship friendship) {
return friendships.putIfAbsent(friendship.getUuids(), friendship) == null;
}
@Override
public boolean deleteFriendship(UUID uuid1, UUID uuid2) {
return friendships.remove(new OrderedUUIDPair(uuid1, uuid2)) != null;
}
}

View file

@ -0,0 +1,100 @@
package de.kentoj.scrowlib.friends.friendship;
import de.kentoj.scrow.bukkit.ScrowAPI;
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;
import static com.google.common.base.Preconditions.checkNotNull;
public class PostgresFriendshipService implements FriendshipService {
private final Jdbi jdbi;
public PostgresFriendshipService() {
checkNotNull(ScrowAPI.getJdbi());
jdbi = ScrowAPI.getJdbi();
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 = new OrderedUUIDPair(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.getFirst())
.bind("second", uuids.getSecond())
.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.getUuids().getFirst())
.bind("second", friendship.getUuids().getSecond())
.bind("createdAt", friendship.getCreatedAt())
.execute() > 0);
}
@Override
public boolean deleteFriendship(UUID uuid1, UUID uuid2) {
var uuids = new OrderedUUIDPair(uuid1, uuid2);
return jdbi.withHandle(h -> h.createUpdate("""
DELETE FROM friendships
WHERE first_uuid = :first AND second_uuid = :second
""")
.bind("first", uuids.getFirst())
.bind("second", uuids.getSecond())
.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,20 @@
package de.kentoj.scrowlib.friends.request;
import de.kentoj.scrowlib.friends.FriendRequest;
import lombok.AllArgsConstructor;
import lombok.Value;
import java.time.Instant;
import java.util.UUID;
@Value
@AllArgsConstructor
public class FriendRequestImpl implements FriendRequest {
UUID from;
UUID to;
Instant createdAt;
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.getTo().equals(playerId))
.toList();
}
@Override
public List<FriendRequest> getFriendRequestsFrom(UUID playerId) {
return requests.stream()
.filter(req -> req.getFrom().equals(playerId))
.toList();
}
@Override
public FriendRequest getFriendRequest(UUID from, UUID to) {
return requests.stream()
.filter(req -> req.getFrom().equals(from) && req.getTo().equals(to))
.findFirst()
.orElse(null);
}
@Override
public boolean saveFriendRequest(FriendRequest friendRequest) {
return requests.add(friendRequest);
}
@Override
public boolean deleteFriendRequest(UUID from, UUID to) {
return requests.removeIf(req -> req.getFrom().equals(from) && req.getTo().equals(to));
}
}

View file

@ -0,0 +1,109 @@
package de.kentoj.scrowlib.friends.request;
import de.kentoj.scrow.bukkit.ScrowAPI;
import de.kentoj.scrowlib.friends.FriendRequest;
import de.kentoj.scrowlib.friends.FriendRequestService;
import lombok.extern.slf4j.Slf4j;
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;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class PostgresFriendRequestService implements FriendRequestService {
private final Jdbi jdbi;
public PostgresFriendRequestService() {
checkNotNull(ScrowAPI.getJdbi());
jdbi = ScrowAPI.getJdbi();
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.getFrom())
.bind("to", friendRequest.getTo())
.bind("createdAt", friendRequest.getCreatedAt())
.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

@ -20,22 +20,27 @@ public class MessageBroker {
public MessageBroker(Connection nats) {
this.nats = nats;
this.dispatcher = nats.createDispatcher();
}
public void publish(String subject, Document document) {
nats.publish(subject, DocumentRepository.toBytes(document));
}
public void publish(String subject, Result<Document, String> result) {
nats.publish(subject, ResultRepository.toBytes(result));
this.publish(subject, ResultRepository.toDocument(result));
}
/**
* subscribes to a subject, handler may run asynchronously
*/
public void subscribe(String subject, Consumer<Document> handler) {
dispatcher.subscribe(subject, msg -> {
executor.submit(() -> handler.accept(DocumentRepository.fromMessage(msg)));
});
dispatcher.subscribe(subject, msg ->
executor.submit(() -> handler.accept(DocumentRepository.fromMessage(msg))));
}
/**
* handler may run asynchronously
*/
public void handle(String subject, RequestHandler handler) {
dispatcher.subscribe(subject, msg -> {
if (msg.getReplyTo() == null) {

View file

@ -10,11 +10,10 @@ import org.bson.Document;
@NoArgsConstructor(access = AccessLevel.NONE)
public class ResultRepository {
public static byte[] toBytes(Result<Document, String> result) {
var doc = result.hasSuccess()
public static Document toDocument(Result<Document, String> result) {
return result.hasSuccess()
? new Document("data", result.getSuccess().orElseThrow())
: new Document("error", result.getFailure().orElseThrow());
return DocumentRepository.toBytes(doc);
}
public static Result<Document, String> fromDocument(Document doc) {

View file

@ -0,0 +1,8 @@
package de.kentoj.scrowlib.player;
import net.kyori.adventure.audience.Audience;
public interface NetworkPlayer extends Audience {
void sendToInstance(String handle);
}

View file

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

View file

@ -0,0 +1,66 @@
package de.kentoj.scrowlib.player;
import de.kentoj.scrowlib.messaging.MessageBroker;
import lombok.RequiredArgsConstructor;
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;
@RequiredArgsConstructor
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;
@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)));
}
}