This commit is contained in:
kento2 2026-02-14 20:19:23 +01:00
parent 07fa0efab7
commit 145a3116bc
29 changed files with 721 additions and 62 deletions

View file

@ -0,0 +1,28 @@
package de.kentoj.scrow;
import reactor.core.publisher.Mono;
import java.util.UUID;
public class Main {
public static void main(String[] args) {
ScrowAPISurface.initScrowAPI(false);
{
var playerId = UUID.randomUUID();
var economyService = ScrowAPI.getEconomyService();
Mono.when(
economyService.setCoins(playerId, 120),
economyService.depositCoins(playerId, 100)
)
.then(economyService.tryWithdrawCoins(playerId, 300))
.doOnNext(a -> System.out.println("withdraw 1 success " + a))
.then(economyService.tryWithdrawCoins(playerId, 50))
.doOnNext(a -> System.out.println("withdraw 2 success " + a))
.then(economyService.getCoins(playerId))
.doOnNext(a -> System.out.println("coins: " + a))
.block();
}
ScrowAPISurface.destroyScrowAPI();
}
}

View file

@ -0,0 +1,49 @@
package de.kentoj.scrow;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import de.kentoj.scrow.economy.InMemoryEconomyService;
import de.kentoj.scrow.economy.MongoEconomyService;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.bson.UuidRepresentation;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.jetbrains.annotations.Nullable;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ScrowAPISurface {
private static @Nullable MongoClient databaseClient = null;
public static void initScrowAPI(boolean enableDatabase) {
if (enableDatabase) {
var pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
var codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(pojoCodecProvider)
);
var settings = MongoClientSettings.builder()
.codecRegistry(codecRegistry)
.uuidRepresentation(UuidRepresentation.STANDARD)
.applyConnectionString(new ConnectionString("mongodb://localhost:27017/scrow"))
.build();
databaseClient = MongoClients.create(settings);
}
ScrowAPI.setEconomyService(
ScrowAPI.getDatabase() != null ?
new MongoEconomyService(ScrowAPI.getDatabase()) : new InMemoryEconomyService()
);
}
public static void destroyScrowAPI() {
if (databaseClient != null) {
databaseClient.close();
}
}
}

View file

@ -0,0 +1,47 @@
package de.kentoj.scrow.economy;
import com.google.common.base.Preconditions;
import de.kentoj.scrow.services.EconomyService;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
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 Mono<@NotNull Integer> getCoins(UUID playerId) {
return Mono.fromSupplier(() -> coins.getOrDefault(playerId, 0));
}
@Override
public Mono<@NotNull Void> setCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.fromSupplier(() -> coins.put(playerId, amount)).then();
}
@Override
public Mono<@NotNull Void> depositCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.fromRunnable(() -> {
var cur = coins.getOrDefault(playerId, amount);
coins.put(playerId, cur + amount);
});
}
@Override
public Mono<@NotNull Boolean> tryWithdrawCoins(UUID playerId, int amount) {
Preconditions.checkArgument(amount >= 0, "amount can not be negative");
return Mono.fromSupplier(() -> {
var cur = coins.getOrDefault(playerId, 0);
if (cur < amount) return false;
coins.put(playerId, cur - amount);
return true;
});
}
}

View file

@ -1,28 +0,0 @@
package de.kentoj.scrow.economy;
import de.kentoj.scrow.services.EconomyService;
import reactor.core.publisher.Mono;
import java.util.UUID;
public class Main {
public static void main(String[] args) {
var con = new MongoDbHelper();
var playerId = UUID.randomUUID();
EconomyService economyService = new EconomyServiceImpl(con.getDatabase());
Mono.when(
economyService.setCoins(playerId, 120),
economyService.depositCoins(playerId, 100)
)
.then(economyService.tryWithdrawCoins(playerId, 300))
.doOnNext(a -> System.out.println("withdraw 1 success " + a))
.then(economyService.tryWithdrawCoins(playerId, 50))
.doOnNext(a -> System.out.println("withdraw 2 success " + a))
.then(economyService.getCoins(playerId))
.doOnNext(a -> System.out.println("coins: " + a))
.block();
con.close();
}
}

View file

@ -1,32 +0,0 @@
package de.kentoj.scrow.economy;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import com.mongodb.reactivestreams.client.MongoDatabase;
import lombok.Getter;
import org.bson.UuidRepresentation;
import java.io.Closeable;
public class MongoDbHelper implements Closeable {
private final MongoClient con;
@Getter
private final MongoDatabase database;
public MongoDbHelper() {
var settings = MongoClientSettings.builder()
.applyConnectionString(new ConnectionString("mongodb://localhost:27017/scrow?ssl=false"))
.uuidRepresentation(UuidRepresentation.STANDARD)
.build();
con = MongoClients.create(settings);
database = con.getDatabase("scrow");
}
@Override
public void close() {
con.close();
}
}

View file

@ -13,12 +13,12 @@ import reactor.core.publisher.Mono;
import java.util.UUID;
public class EconomyServiceImpl implements EconomyService {
public class MongoEconomyService implements EconomyService {
private static final String COLLECTION_NAME = "economy";
private final @NotNull MongoDatabase database;
public EconomyServiceImpl(MongoDatabase database) {
public MongoEconomyService(MongoDatabase database) {
this.database = database;
Mono.from(database.createCollection(COLLECTION_NAME)).block();
}

View file

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

View file

@ -0,0 +1,36 @@
package de.kentoj.scrow.friends.friendship;
import com.google.common.base.Preconditions;
import de.kentoj.scrow.services.friends.Friendship;
import de.kentoj.scrow.util.pair.Tuple2;
import lombok.Value;
import org.bson.codecs.pojo.annotations.BsonCreator;
import org.bson.codecs.pojo.annotations.BsonProperty;
import org.bson.types.ObjectId;
import java.time.Instant;
import java.util.UUID;
@Value
public class BsonFriendship implements Friendship {
ObjectId id;
Tuple2<UUID, UUID> playerIds;
Instant createdAt;
@BsonCreator
public BsonFriendship(
@BsonProperty("_id") ObjectId id,
@BsonProperty("playerIds") Tuple2<UUID, UUID> playerIds,
@BsonProperty("createdAt") Instant createdAt
) {
Preconditions.checkArgument(playerIds.getFirst() != playerIds.getSecond());
this.id = id;
this.playerIds = toOrderedTuple(playerIds);
this.createdAt = createdAt;
}
public BsonFriendship(Tuple2<UUID, UUID> playerIds, Instant createdAt) {
this(ObjectId.get(), playerIds, createdAt);
}
}

View file

@ -0,0 +1,27 @@
package de.kentoj.scrow.friends.friendship;
import de.kentoj.scrow.services.friends.Friendship;
import de.kentoj.scrow.util.pair.Tuple2;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.UUID;
public interface FriendshipRepository {
Flux<@NotNull Friendship> getFriendships(UUID playerId);
Mono<@NotNull Friendship> getFriendship(Tuple2<UUID, UUID> playerIds);
/**
* @return True if friendship saved, false if friendship already exists
*/
Mono<@NotNull Boolean> saveFriendship(Tuple2<UUID, UUID> playerIds, Instant createdAt);
/**
* @return True if friendship deleted, false if none found
*/
Mono<@NotNull Boolean> deleteFriendship(Tuple2<UUID, UUID> playerIds);
}

View file

@ -0,0 +1,22 @@
package de.kentoj.scrow.friends.friendship;
import de.kentoj.scrow.util.pair.Tuple2;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.UUID;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class FriendshipUtils {
/**
* ensures constant order
* @return an ordered tuple
*/
public static Tuple2<UUID, UUID> toOrderedTuple(Tuple2<UUID, UUID> playerIds) {
if (playerIds.getFirst().compareTo(playerIds.getSecond()) < 0)
return new Tuple2<>(playerIds.getFirst(), playerIds.getSecond());
else
return new Tuple2<>(playerIds.getSecond(), playerIds.getFirst());
}
}

View file

@ -0,0 +1,86 @@
package de.kentoj.scrow.friends.friendship;
import com.mongodb.ErrorCategory;
import com.mongodb.MongoWriteException;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import de.kentoj.scrow.services.friends.Friendship;
import de.kentoj.scrow.util.pair.Tuple2;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.UUID;
public class MongoFriendshipRepository implements FriendshipRepository {
private static final String COLLECTION_NAME = "friendships";
private final MongoDatabase database;
public MongoFriendshipRepository(MongoDatabase database) {
this.database = database;
database.createCollection(COLLECTION_NAME);
collection().createIndex(
Indexes.compoundIndex(
Indexes.ascending("playerIds.first"),
Indexes.ascending("playerIds.second")
),
new IndexOptions().unique(true)
);
}
@Override
public Flux<@NotNull Friendship> getFriendships(UUID playerId) {
return Flux.from(collection()
.find(Filters.or(
Filters.eq("playerIds.first", playerId),
Filters.eq("playerIds.second", playerId)
)));
}
@Override
public Mono<@NotNull Friendship> getFriendship(Tuple2<UUID, UUID> playerIds) {
playerIds = FriendshipUtils.toOrderedTuple(playerIds);
return Mono.from(collection()
.find(Filters.and(
Filters.eq("playerIds.first", playerIds.getFirst()),
Filters.eq("playerIds.second", playerIds.getSecond())
)));
}
@Override
public Mono<@NotNull Boolean> saveFriendship(Tuple2<UUID, UUID> playerIds, Instant createdAt) {
var friendship = new BsonFriendship(playerIds, createdAt);
return Mono.from(collection()
.insertOne(friendship)
)
.map(__ -> true)
.onErrorResume(e -> {
if (!(e instanceof MongoWriteException)) return Mono.error(e);
if (((MongoWriteException) e).getError().getCategory() != ErrorCategory.DUPLICATE_KEY)
return Mono.error(e);
return Mono.just(false);
});
}
@Override
public Mono<@NotNull Boolean> deleteFriendship(Tuple2<UUID, UUID> playerIds) {
playerIds = FriendshipUtils.toOrderedTuple(playerIds);
return Mono.from(collection()
.deleteOne(Filters.and(
Filters.eq("playerIds.first", playerIds.getFirst()),
Filters.eq("playerIds.second", playerIds.getSecond())
)))
.map(res -> res.getDeletedCount() > 0);
}
private MongoCollection<BsonFriendship> collection() {
return database.getCollection(COLLECTION_NAME, BsonFriendship.class);
}
}

View file

@ -0,0 +1,37 @@
package de.kentoj.scrow.friends.request;
import com.google.common.base.Preconditions;
import de.kentoj.scrow.services.friends.FriendRequest;
import lombok.Value;
import org.bson.codecs.pojo.annotations.BsonCreator;
import org.bson.codecs.pojo.annotations.BsonProperty;
import org.bson.types.ObjectId;
import java.time.Instant;
import java.util.UUID;
@Value
public class BsonFriendRequest implements FriendRequest {
ObjectId requestId;
UUID from;
UUID to;
Instant createdAt;
@BsonCreator
public BsonFriendRequest(
@BsonProperty("_id") ObjectId requestId,
@BsonProperty("from") UUID from,
@BsonProperty("to") UUID to,
@BsonProperty("createdAt") Instant createdAt
) {
Preconditions.checkArgument(from != to, "friend-request can not have same 'from' and 'to' value");
this.requestId = requestId;
this.from = from;
this.to = to;
this.createdAt = createdAt;
}
public BsonFriendRequest(UUID from, UUID to, Instant createdAt) {
this(ObjectId.get(), from, to, createdAt);
}
}

View file

@ -0,0 +1,28 @@
package de.kentoj.scrow.friends.request;
import de.kentoj.scrow.services.friends.FriendRequest;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.UUID;
public interface FriendRequestRepository {
Flux<@NotNull FriendRequest> getIncomingFriendRequests(UUID playerId);
Flux<@NotNull FriendRequest> getOutgoingFriendRequests(UUID playerId);
Mono<@NotNull FriendRequest> getFriendRequest(UUID from, UUID to);
/**
* @return True if request saved, false if already existing request found.
*/
Mono<@NotNull Boolean> saveFriendRequest(UUID from, UUID to, Instant createdAt);
/**
* @return True if request deleted, false if none found.
*/
Mono<@NotNull Boolean> deleteFriendRequest(UUID from, UUID to);
}

View file

@ -0,0 +1,85 @@
package de.kentoj.scrow.friends.request;
import com.mongodb.ErrorCategory;
import com.mongodb.MongoWriteException;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import de.kentoj.scrow.services.friends.FriendRequest;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.UUID;
public class MongoFriendRequestRepository implements FriendRequestRepository {
private static final String COLLECTION_NAME = "friend-requests";
private final MongoDatabase database;
public MongoFriendRequestRepository(MongoDatabase database) {
this.database = database;
database.createCollection(COLLECTION_NAME);
collection().createIndex(
Indexes.compoundIndex(
Indexes.ascending("from"),
Indexes.ascending("to")
),
new IndexOptions().unique(true)
);
}
@Override
public Mono<@NotNull FriendRequest> getFriendRequest(UUID from, UUID to) {
return Mono.from(collection()
.find(Filters.and(
Filters.eq("from", from),
Filters.eq("to", to)
))
.first());
}
@Override
public Mono<@NotNull Boolean> saveFriendRequest(UUID from, UUID to, Instant createdAt) {
var request = new BsonFriendRequest(from, to, createdAt);
return Mono.from(collection()
.insertOne(request)
)
.map(__ -> true)
.onErrorResume(e -> {
if (!(e instanceof MongoWriteException)) return Mono.error(e);
if (((MongoWriteException) e).getError().getCategory() != ErrorCategory.DUPLICATE_KEY)
return Mono.error(e);
return Mono.just(false);
});
}
@Override
public Flux<@NotNull FriendRequest> getIncomingFriendRequests(UUID playerId) {
return Flux.from(collection().find(Filters.eq("to", playerId)));
}
@Override
public Flux<@NotNull FriendRequest> getOutgoingFriendRequests(UUID playerId) {
return Flux.from(collection().find(Filters.eq("from", playerId)));
}
@Override
public Mono<@NotNull Boolean> deleteFriendRequest(UUID from, UUID to) {
return Mono.from(collection()
.deleteOne(Filters.and(
Filters.eq("from", from),
Filters.eq("to", to)
)))
.map(res -> res.getDeletedCount() > 0);
}
private MongoCollection<BsonFriendRequest> collection() {
return database.getCollection(COLLECTION_NAME, BsonFriendRequest.class);
}
}