From 63bf99f7f89eba3ccd75df776f51934614c1b88f Mon Sep 17 00:00:00 2001 From: Joris Guffens Date: Wed, 7 Dec 2022 20:12:05 +0100 Subject: [PATCH] The new and improved stats plugin --- .../guflimc/brick/stats/api/StatsManager.java | 42 +++-- .../guflimc/brick/stats/api/actor/Actor.java | 76 ++++++++ .../brick/stats/api/container/Container.java | 31 ++++ .../brick/stats/api/container/Record.java | 14 ++ .../stats/api/container/StatsContainer.java | 16 -- .../stats/api/container/StatsRecord.java | 13 -- .../guflimc/brick/stats/api/event/Event.java | 4 +- .../stats/api/relation/RelationProvider.java | 8 +- .../common/BrickStatsDatabaseContext.java | 8 +- .../brick/stats/common/BrickStatsManager.java | 172 +++++++++++------- .../common/container/BrickStatsContainer.java | 80 +++++--- .../brick/stats/common/domain/DActor.java | 58 ++++++ .../brick/stats/common/domain/DRecord.java | 70 +++++++ .../stats/common/domain/DStatsRecord.java | 84 --------- .../dbmigrations/h2/1.0__initial.sql | 26 ++- .../dbmigrations/model/1.0__initial.model.xml | 15 +- .../dbmigrations/mysql/1.0__initial.sql | 26 ++- .../brick/stats/spigot/SpigotBrickStats.java | 8 +- .../stats/spigot/listeners/BlockListener.java | 5 +- .../stats/spigot/listeners/DeathListener.java | 5 +- .../MovementTask.java} | 7 +- .../PlayTimeTask.java} | 7 +- 22 files changed, 520 insertions(+), 255 deletions(-) create mode 100644 api/src/main/java/com/guflimc/brick/stats/api/actor/Actor.java create mode 100644 api/src/main/java/com/guflimc/brick/stats/api/container/Container.java create mode 100644 api/src/main/java/com/guflimc/brick/stats/api/container/Record.java delete mode 100644 api/src/main/java/com/guflimc/brick/stats/api/container/StatsContainer.java delete mode 100644 api/src/main/java/com/guflimc/brick/stats/api/container/StatsRecord.java create mode 100644 common/src/main/java/com/guflimc/brick/stats/common/domain/DActor.java create mode 100644 common/src/main/java/com/guflimc/brick/stats/common/domain/DRecord.java delete mode 100644 common/src/main/java/com/guflimc/brick/stats/common/domain/DStatsRecord.java rename spigot/src/main/java/com/guflimc/brick/stats/spigot/{jobs/MovementJob.java => tasks/MovementTask.java} (84%) rename spigot/src/main/java/com/guflimc/brick/stats/spigot/{jobs/PlayTimeJob.java => tasks/PlayTimeTask.java} (61%) diff --git a/api/src/main/java/com/guflimc/brick/stats/api/StatsManager.java b/api/src/main/java/com/guflimc/brick/stats/api/StatsManager.java index 2095288..6d5ab09 100644 --- a/api/src/main/java/com/guflimc/brick/stats/api/StatsManager.java +++ b/api/src/main/java/com/guflimc/brick/stats/api/StatsManager.java @@ -1,35 +1,53 @@ package com.guflimc.brick.stats.api; -import com.guflimc.brick.stats.api.container.StatsContainer; +import com.guflimc.brick.stats.api.actor.Actor; +import com.guflimc.brick.stats.api.container.Container; +import com.guflimc.brick.stats.api.container.Record; import com.guflimc.brick.stats.api.event.SubscriptionBuilder; import com.guflimc.brick.stats.api.key.StatsKey; import com.guflimc.brick.stats.api.relation.RelationProvider; import org.jetbrains.annotations.NotNull; -import java.util.UUID; +import java.util.List; import java.util.function.IntFunction; public interface StatsManager { - int read(@NotNull UUID id, @NotNull StatsKey key); + Container find(@NotNull Actor... actors); - int read(@NotNull UUID id, UUID relation, @NotNull StatsKey key); + Container find(@NotNull Actor.ActorSet actors); - void update(@NotNull UUID id, @NotNull StatsKey key, @NotNull IntFunction updater); + // - void update(@NotNull UUID id, UUID relation, @NotNull StatsKey key, @NotNull IntFunction updater); + /** + * This will also update permutations of the actor set. + */ + void update(@NotNull Actor.ActorSet actors, @NotNull StatsKey key, @NotNull IntFunction updater); - default void write(@NotNull UUID id, @NotNull StatsKey key, int value) { - write(id, null, key, value); + /** + * This will first find the full actor set by traversing relations and then {@link #update(Actor.ActorSet, StatsKey, IntFunction)}. + */ + void update(@NotNull Actor actor, @NotNull StatsKey key, @NotNull IntFunction updater); + + // + + List select(@NotNull String actorType, @NotNull StatsKey key, int limit, boolean asc); + + default List select(@NotNull String actorType, @NotNull StatsKey key, int limit) { + return select(actorType, key, limit, false); } - default void write(@NotNull UUID id, UUID relation, @NotNull StatsKey key, int value) { - update(id, relation, key, ignored -> value); + default List select(@NotNull String actorType, @NotNull StatsKey key) { + return select(actorType, key, Integer.MAX_VALUE); } - StatsContainer readAll(@NotNull UUID id); + // + + void registerRelations(@NotNull RelationProvider relationProvider); + + void unregisterRelations(@NotNull RelationProvider relationProvider); - void registerRelationProvider(@NotNull RelationProvider relationProvider); + // SubscriptionBuilder subscribe(); diff --git a/api/src/main/java/com/guflimc/brick/stats/api/actor/Actor.java b/api/src/main/java/com/guflimc/brick/stats/api/actor/Actor.java new file mode 100644 index 0000000..80a4d7b --- /dev/null +++ b/api/src/main/java/com/guflimc/brick/stats/api/actor/Actor.java @@ -0,0 +1,76 @@ +package com.guflimc.brick.stats.api.actor; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Stream; + +public interface Actor { + + UUID id(); + + String type(); + + // + + static Actor player(@NotNull UUID id) { + return new TempActor(id, "PLAYER"); + } + + record TempActor(@NotNull UUID id, @NotNull String type) implements Actor { + } + + // + + class ActorSet implements Iterable { + + private final Set actors; + + public ActorSet(@NotNull Collection c) { + if ( c.isEmpty() ) { + throw new IllegalArgumentException("ActorSet must contain at least one actor."); + } + this.actors = Set.copyOf(c); + } + + public ActorSet(@NotNull T... actors) { + this(Set.of(actors)); + } + + public int size() { + return actors.size(); + } + + public Actor first() { + return iterator().next(); + } + + public Stream stream() { + return actors.stream(); + } + + public Set copySet() { + return new HashSet<>(actors); + } + + @NotNull + @Override + public Iterator iterator() { + return actors.iterator(); + } + + // + + + @Override + public int hashCode() { + return actors.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ActorSet as && actors.equals(as.actors); + } + } +} + diff --git a/api/src/main/java/com/guflimc/brick/stats/api/container/Container.java b/api/src/main/java/com/guflimc/brick/stats/api/container/Container.java new file mode 100644 index 0000000..ead85d6 --- /dev/null +++ b/api/src/main/java/com/guflimc/brick/stats/api/container/Container.java @@ -0,0 +1,31 @@ +package com.guflimc.brick.stats.api.container; + +import com.guflimc.brick.stats.api.actor.Actor; +import com.guflimc.brick.stats.api.key.StatsKey; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.IntFunction; + +public interface Container { + + Actor.ActorSet actors(); + + Collection stats(); + + // + + Optional find(@NotNull StatsKey key); + + int read(@NotNull StatsKey key); + + void update(@NotNull StatsKey key, @NotNull IntFunction updater); + + default void write(@NotNull StatsKey key, int value) { + update(key, i -> value); + } + +} diff --git a/api/src/main/java/com/guflimc/brick/stats/api/container/Record.java b/api/src/main/java/com/guflimc/brick/stats/api/container/Record.java new file mode 100644 index 0000000..a96c757 --- /dev/null +++ b/api/src/main/java/com/guflimc/brick/stats/api/container/Record.java @@ -0,0 +1,14 @@ +package com.guflimc.brick.stats.api.container; + +import com.guflimc.brick.stats.api.actor.Actor; +import com.guflimc.brick.stats.api.key.StatsKey; + +public interface Record { + + Actor.ActorSet actors(); + + StatsKey key(); + + int value(); + +} diff --git a/api/src/main/java/com/guflimc/brick/stats/api/container/StatsContainer.java b/api/src/main/java/com/guflimc/brick/stats/api/container/StatsContainer.java deleted file mode 100644 index 3f7ded0..0000000 --- a/api/src/main/java/com/guflimc/brick/stats/api/container/StatsContainer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.guflimc.brick.stats.api.container; - -import com.guflimc.brick.stats.api.key.StatsKey; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -public interface StatsContainer { - - UUID id(); - - int read(@NotNull StatsKey key); - - int read(UUID relation, @NotNull StatsKey key); - -} diff --git a/api/src/main/java/com/guflimc/brick/stats/api/container/StatsRecord.java b/api/src/main/java/com/guflimc/brick/stats/api/container/StatsRecord.java deleted file mode 100644 index 1f9cb8c..0000000 --- a/api/src/main/java/com/guflimc/brick/stats/api/container/StatsRecord.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.guflimc.brick.stats.api.container; - -import java.util.UUID; - -public interface StatsRecord { - - UUID id(); - - UUID relation(); - - int value(); - -} diff --git a/api/src/main/java/com/guflimc/brick/stats/api/event/Event.java b/api/src/main/java/com/guflimc/brick/stats/api/event/Event.java index 1fb9967..dba60c0 100644 --- a/api/src/main/java/com/guflimc/brick/stats/api/event/Event.java +++ b/api/src/main/java/com/guflimc/brick/stats/api/event/Event.java @@ -1,8 +1,8 @@ package com.guflimc.brick.stats.api.event; -import com.guflimc.brick.stats.api.container.StatsRecord; +import com.guflimc.brick.stats.api.container.Record; import com.guflimc.brick.stats.api.key.StatsKey; import org.jetbrains.annotations.NotNull; -public record Event(@NotNull StatsKey key, @NotNull StatsRecord record, int previousValue) { +public record Event(@NotNull Record record, int previousValue) { } \ No newline at end of file diff --git a/api/src/main/java/com/guflimc/brick/stats/api/relation/RelationProvider.java b/api/src/main/java/com/guflimc/brick/stats/api/relation/RelationProvider.java index 4c00fa6..ad4496e 100644 --- a/api/src/main/java/com/guflimc/brick/stats/api/relation/RelationProvider.java +++ b/api/src/main/java/com/guflimc/brick/stats/api/relation/RelationProvider.java @@ -1,14 +1,14 @@ package com.guflimc.brick.stats.api.relation; +import com.guflimc.brick.stats.api.actor.Actor; import com.guflimc.brick.stats.api.key.StatsKey; import org.jetbrains.annotations.NotNull; - -import java.util.Optional; -import java.util.UUID; +import org.jetbrains.annotations.Nullable; @FunctionalInterface public interface RelationProvider { - Optional relation(@NotNull UUID entityId, @NotNull StatsKey key); + @Nullable + Actor provide(@NotNull Actor actor, @NotNull StatsKey key); } diff --git a/common/src/main/java/com/guflimc/brick/stats/common/BrickStatsDatabaseContext.java b/common/src/main/java/com/guflimc/brick/stats/common/BrickStatsDatabaseContext.java index 979812b..b33cff5 100644 --- a/common/src/main/java/com/guflimc/brick/stats/common/BrickStatsDatabaseContext.java +++ b/common/src/main/java/com/guflimc/brick/stats/common/BrickStatsDatabaseContext.java @@ -3,7 +3,8 @@ import com.guflimc.brick.orm.ebean.database.EbeanConfig; import com.guflimc.brick.orm.ebean.database.EbeanDatabaseContext; import com.guflimc.brick.orm.ebean.database.EbeanMigrations; -import com.guflimc.brick.stats.common.domain.DStatsRecord; +import com.guflimc.brick.stats.common.domain.DActor; +import com.guflimc.brick.stats.common.domain.DRecord; import io.ebean.annotation.Platform; import java.io.IOException; @@ -29,7 +30,10 @@ protected Class[] applicableClasses() { } private static final Class[] APPLICABLE_CLASSES = new Class[]{ - DStatsRecord.class + DRecord.class, + + DActor.ActorPK.class, + DActor.class }; public static void main(String[] args) throws IOException, SQLException { diff --git a/common/src/main/java/com/guflimc/brick/stats/common/BrickStatsManager.java b/common/src/main/java/com/guflimc/brick/stats/common/BrickStatsManager.java index 3b0ce2f..54ac8ef 100644 --- a/common/src/main/java/com/guflimc/brick/stats/common/BrickStatsManager.java +++ b/common/src/main/java/com/guflimc/brick/stats/common/BrickStatsManager.java @@ -3,13 +3,15 @@ import com.guflimc.brick.orm.api.database.DatabaseContext; import com.guflimc.brick.scheduler.api.Scheduler; import com.guflimc.brick.stats.api.StatsManager; -import com.guflimc.brick.stats.api.container.StatsContainer; -import com.guflimc.brick.stats.api.container.StatsRecord; +import com.guflimc.brick.stats.api.actor.Actor; +import com.guflimc.brick.stats.api.container.Container; +import com.guflimc.brick.stats.api.container.Record; import com.guflimc.brick.stats.api.event.*; import com.guflimc.brick.stats.api.key.StatsKey; import com.guflimc.brick.stats.api.relation.RelationProvider; import com.guflimc.brick.stats.common.container.BrickStatsContainer; -import com.guflimc.brick.stats.common.domain.DStatsRecord; +import com.guflimc.brick.stats.common.domain.DActor; +import com.guflimc.brick.stats.common.domain.DRecord; import com.guflimc.brick.stats.common.event.AbstractSubscriptionBuilder; import com.guflimc.brick.stats.common.event.subscriptions.AbstractSubscription; import org.jetbrains.annotations.NotNull; @@ -19,117 +21,159 @@ import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.function.IntFunction; +import java.util.stream.Collectors; public class BrickStatsManager implements StatsManager { - private final Map containers = new ConcurrentHashMap<>(); - private final Set relationProviders = new CopyOnWriteArraySet<>(); - - private final Queue savingQueue = new ArrayDeque<>(); private final DatabaseContext databaseContext; + private final Queue savingQueue = new ArrayDeque<>(); + + private final Map actors = new ConcurrentHashMap<>(); + private final Map containers = new ConcurrentHashMap<>(); + + private final Set subscriptions = new HashSet<>(); + + private final Set relationProviders = new CopyOnWriteArraySet<>(); + public BrickStatsManager(DatabaseContext databaseContext, Scheduler scheduler) { this.databaseContext = databaseContext; - List records = databaseContext.findAllAsync(DStatsRecord.class).join(); - Map> mapped = new HashMap<>(); - records.forEach(r -> { - mapped.computeIfAbsent(r.id(), (x) -> new ArrayList<>()); - mapped.get(r.id()).add(r); + // load actors + databaseContext.findAllAsync(DActor.class).join() + .forEach(actor -> actors.put(new Actor.TempActor(actor.id(), actor.type()), actor)); - if (r.relation() != null) { - mapped.computeIfAbsent(r.relation(), (x) -> new ArrayList<>()); - mapped.get(r.relation()).add(r); - } - }); - - mapped.keySet().forEach(id -> { - containers.put(id, new BrickStatsContainer(id, mapped.get(id))); - }); + // load records + databaseContext.findAllAsync(DRecord.class).join() + .forEach(record -> containers.computeIfAbsent(record.actors(), + a -> new BrickStatsContainer(this, a)) + .add(record)); + // save task scheduler.asyncRepeating(() -> save(30), 5, TimeUnit.SECONDS); + + // info task + scheduler.asyncRepeating(() -> { + System.out.println(" ----- Stats of BrickStats -----"); + System.out.println("Actors: " + actors.size()); + System.out.println("Containers: " + containers.size()); + System.out.println("Records: " + containers.values().stream().mapToInt(c -> c.stats().size()).sum()); + }, 300, TimeUnit.SECONDS); } public void save(int max) { - Set recordsToSave = new HashSet<>(); + Set entitiesToSave = new HashSet<>(); for (int i = 0; i < max; i++) { - DStatsRecord record = savingQueue.poll(); - if (record == null) { + Object entity = savingQueue.poll(); + if (entity == null) { break; } - recordsToSave.add(record); + entitiesToSave.add(entity); } - databaseContext.persistAsync(recordsToSave).join(); + databaseContext.persistAsync(entitiesToSave).join(); } - private BrickStatsContainer find(@NotNull UUID id) { - containers.computeIfAbsent(id, (v) -> new BrickStatsContainer(id, List.of())); - return containers.get(id); - } + // - @Override - public int read(@NotNull UUID id, @NotNull StatsKey key) { - return read(id, null, key); + private DActor find(@NotNull Actor.TempActor actor) { + if (actors.containsKey(actor)) { + return actors.get(actor); + } + DActor dactor = new DActor(actor.id(), actor.type()); + actors.put(actor, dactor); + return dactor; } @Override - public int read(@NotNull UUID id, UUID relation, @NotNull StatsKey key) { - return find(id).read(relation, key); - } + public Container find(Actor.@NotNull ActorSet actors) { + Actor.ActorSet copy = new Actor.ActorSet(actors.stream() + .map(a -> a instanceof Actor.TempActor ta ? find(ta) : a).toList()); - @Override - public void update(@NotNull UUID id, @NotNull StatsKey key, @NotNull IntFunction updater) { - update(id, null, key, updater); + if (!containers.containsKey(copy)) { + containers.put(copy, new BrickStatsContainer(this, copy)); + } + + return containers.get(copy); } @Override - public void update(@NotNull UUID id, UUID relation, @NotNull StatsKey key, @NotNull IntFunction updater) { - update(id, relation, key, updater, new HashSet<>()); + public Container find(@NotNull Actor... actors) { + return find(new Actor.ActorSet(actors)); } - public void update(@NotNull UUID id, UUID relation, @NotNull StatsKey key, - @NotNull IntFunction updater, @NotNull Collection passed) { - DStatsRecord rec = find(id).find(relation, key); - int previousValue = rec.value(); + // - rec.setValue(updater.apply(rec.value())); - savingQueue.add(rec); - handleUpdate(key, rec, previousValue); + @Override + public void update(Actor.@NotNull ActorSet actors, @NotNull StatsKey key, @NotNull IntFunction updater) { + find(actors).update(key, updater); - if (key.parent() != null) { - update(id, relation, key.parent(), updater, new HashSet<>()); + if (actors.size() <= 1) { + return; } - passed.add(id); + for (Actor actor : actors) { + Set ns = actors.copySet(); + ns.remove(actor); - // also update relations - relationProviders.stream() - .map(r -> r.relation(id, key).orElse(null)) + update(new Actor.ActorSet(ns), key, updater); + } + } + + @Override + public void update(@NotNull Actor actor, @NotNull StatsKey key, @NotNull IntFunction updater) { + Set actors = relationProviders.stream() + .map(r -> r.provide(actor, key)) .filter(Objects::nonNull) - .filter(r -> !passed.contains(r)) - .distinct() - .forEach(r -> update(r, id, key, updater)); + .collect(Collectors.toSet()); + actors.add(actor); + + update(new Actor.ActorSet(actors), key, updater); } + // + @Override - public StatsContainer readAll(@NotNull UUID id) { - return find(id); + public List select(@NotNull String actorType, @NotNull StatsKey key, int limit, boolean asc) { + Comparator comp = Comparator.comparingInt(Record::value); + if (!asc) { + comp = comp.reversed(); + } + + List records = containers.keySet().stream() + .filter(a -> a.size() == 1) + .filter(a -> a.iterator().next().type().equals(actorType)) + .map(a -> find(a).find(key).orElse(null)) + .filter(Objects::nonNull) + .sorted(comp) + .toList(); + + if (records.size() > limit) { + records = records.subList(0, limit); + } + return records; } + // + @Override - public void registerRelationProvider(@NotNull RelationProvider relationProvider) { + public void registerRelations(@NotNull RelationProvider relationProvider) { relationProviders.add(relationProvider); } - // + @Override + public void unregisterRelations(@NotNull RelationProvider relationProvider) { + relationProviders.remove(relationProvider); + } - private final Set subscriptions = new HashSet<>(); + // - private void handleUpdate(@NotNull StatsKey key, @NotNull StatsRecord record, int previousValue) { - Event event = new Event(key, record, previousValue); + public void handleUpdate(@NotNull Record record, int previousValue) { + Event event = new Event(record, previousValue); new HashSet<>(subscriptions).forEach(sub -> sub.execute(event)); + + savingQueue.add((DRecord) record); } @Override diff --git a/common/src/main/java/com/guflimc/brick/stats/common/container/BrickStatsContainer.java b/common/src/main/java/com/guflimc/brick/stats/common/container/BrickStatsContainer.java index 7470991..8d9c0f3 100644 --- a/common/src/main/java/com/guflimc/brick/stats/common/container/BrickStatsContainer.java +++ b/common/src/main/java/com/guflimc/brick/stats/common/container/BrickStatsContainer.java @@ -1,57 +1,77 @@ package com.guflimc.brick.stats.common.container; -import com.guflimc.brick.stats.api.container.StatsContainer; +import com.guflimc.brick.stats.api.actor.Actor; +import com.guflimc.brick.stats.api.container.Container; +import com.guflimc.brick.stats.api.container.Record; import com.guflimc.brick.stats.api.key.StatsKey; -import com.guflimc.brick.stats.common.domain.DStatsRecord; +import com.guflimc.brick.stats.common.BrickStatsManager; +import com.guflimc.brick.stats.common.domain.DRecord; import org.jetbrains.annotations.NotNull; import java.util.Collection; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.IntFunction; +import java.util.stream.Collectors; -public class BrickStatsContainer implements StatsContainer { +public class BrickStatsContainer implements Container { - private final UUID id; - private final Map records = new ConcurrentHashMap<>(); + private final BrickStatsManager manager; - private record RecordKey(@NotNull String key, UUID relation) { + private final Actor.ActorSet actors; + private final Set records = new CopyOnWriteArraySet<>(); + + public BrickStatsContainer(BrickStatsManager manager, Actor.ActorSet actors) { + this.manager = manager; + this.actors = actors; } - public BrickStatsContainer(UUID id, Collection records) { - this.id = id; - for (DStatsRecord rec : records) { - if ( rec.id().equals(id) ) { - this.records.put(new RecordKey(rec.key(), rec.relation()), rec); - } else { - this.records.put(new RecordKey(rec.key(), rec.id()), rec); - } + public void add(DRecord record) { + if ( !record.actors().equals(actors) ) { + throw new IllegalArgumentException("Record actors does not match container actors."); } + records.add(record); } + // + @Override - public UUID id() { - return id; + public Actor.ActorSet actors() { + return actors; } @Override - public int read(@NotNull StatsKey key) { - return read(null, key); + public Collection stats() { + return records.stream().map(DRecord::key).collect(Collectors.toUnmodifiableSet()); } @Override - public int read(UUID relation, @NotNull StatsKey key) { - RecordKey recKey = new RecordKey(key.name(), relation); - return records.containsKey(recKey) ? records.get(recKey).value() : 0; + public Optional find(@NotNull StatsKey key) { + return records.stream() + .filter(r -> r.key().equals(key)) + .map(r -> (Record) r) + .findFirst(); } - public DStatsRecord find(@NotNull StatsKey key) { - return find(null, key); + @Override + public int read(@NotNull StatsKey key) { + return find(key).map(Record::value).orElse(0); } - public DStatsRecord find(UUID relation, @NotNull StatsKey key) { - RecordKey recKey = new RecordKey(key.name(), relation); - records.computeIfAbsent(recKey, k -> new DStatsRecord(id, key.name(), 0)); - return records.get(recKey); + @Override + public void update(@NotNull StatsKey key, @NotNull IntFunction updater) { + DRecord record = (DRecord) find(key).orElse(null); + if (record == null) { + record = new DRecord(key, actors); + records.add(record); + } + + int previousValue = record.value(); + record.setValue(updater.apply(previousValue)); + + manager.handleUpdate(record, previousValue); +// manager.handlePermutations(record, updater); } + } diff --git a/common/src/main/java/com/guflimc/brick/stats/common/domain/DActor.java b/common/src/main/java/com/guflimc/brick/stats/common/domain/DActor.java new file mode 100644 index 0000000..3dc09cc --- /dev/null +++ b/common/src/main/java/com/guflimc/brick/stats/common/domain/DActor.java @@ -0,0 +1,58 @@ +package com.guflimc.brick.stats.common.domain; + +import com.guflimc.brick.stats.api.actor.Actor; +import org.jetbrains.annotations.NotNull; + +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@Entity +@Table(name = "actors") +public class DActor implements Actor { + + @Embeddable + public static class ActorPK { + + private UUID id; + + private String type; + + @Override + public int hashCode() { + return id.hashCode() + type.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if ( !(obj instanceof ActorPK pk) ) { + return false; + } + return id.equals(pk.id) && type.equals(pk.type); + } + } + + @EmbeddedId + private ActorPK pk; + + public DActor() {} + + public DActor(@NotNull UUID id, @NotNull String type) { + this.pk = new ActorPK(); + this.pk.id = id; + this.pk.type = type; + } + + @Override + public UUID id() { + return pk.id; + } + + @Override + public String type() { + return pk.type; + } + +} diff --git a/common/src/main/java/com/guflimc/brick/stats/common/domain/DRecord.java b/common/src/main/java/com/guflimc/brick/stats/common/domain/DRecord.java new file mode 100644 index 0000000..48a3763 --- /dev/null +++ b/common/src/main/java/com/guflimc/brick/stats/common/domain/DRecord.java @@ -0,0 +1,70 @@ +package com.guflimc.brick.stats.common.domain; + +import com.guflimc.brick.stats.api.actor.Actor; +import com.guflimc.brick.stats.api.container.Record; +import com.guflimc.brick.stats.api.key.StatsKey; +import io.ebean.annotation.DbDefault; +import io.ebean.annotation.WhenCreated; +import io.ebean.annotation.WhenModified; +import org.jetbrains.annotations.NotNull; + +import javax.persistence.*; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "records") +public class DRecord implements Record { + + @Id + @GeneratedValue + private UUID id; + + @Column(nullable = false) + @ManyToMany(targetEntity = DActor.class, cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinTable(name = "records_actors", + joinColumns = @JoinColumn(name = "record_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "actor_id", referencedColumnName = "id")) + private List actors; + + @Column(name = "keyname", nullable = false) + private String key; + + @DbDefault("0") + private int value = 0; + + @WhenCreated + private Instant createdAt; + + @WhenModified + private Instant updatedAt; + + public DRecord() { + } + + public DRecord(@NotNull StatsKey key, @NotNull Actor.ActorSet actors) { + this.key = key.name(); + this.actors = actors.stream().map(a -> (DActor) a).toList(); + } + + @Override + public Actor.ActorSet actors() { + return new Actor.ActorSet(actors.stream().map(a -> (Actor) a).toList()); + } + + @Override + public StatsKey key() { + return StatsKey.of(key); + } + + @Override + public int value() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + +} diff --git a/common/src/main/java/com/guflimc/brick/stats/common/domain/DStatsRecord.java b/common/src/main/java/com/guflimc/brick/stats/common/domain/DStatsRecord.java deleted file mode 100644 index 7a5c189..0000000 --- a/common/src/main/java/com/guflimc/brick/stats/common/domain/DStatsRecord.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.guflimc.brick.stats.common.domain; - -import com.guflimc.brick.stats.api.container.StatsRecord; -import io.ebean.annotation.DbDefault; -import io.ebean.annotation.Index; -import io.ebean.annotation.WhenCreated; -import io.ebean.annotation.WhenModified; -import org.jetbrains.annotations.NotNull; - -import javax.persistence.*; -import java.time.Instant; -import java.util.UUID; - -@Entity -@Table(name = "stats") -@Index(columnNames = {"entity_id", "relation", "keyname"}, unique = true) -public class DStatsRecord implements StatsRecord { - - private final static UUID EMPTY_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); - - @Id - @GeneratedValue - private UUID id; - - @Column(nullable = false) - private UUID entityId; - - @Column(nullable = false) - @DbDefault("00000000-0000-0000-0000-000000000000") - private UUID relation = EMPTY_UUID; - - @Column(name = "keyname", nullable = false) - private String key; - - @DbDefault("0") - private int value; - - @WhenCreated - private Instant createdAt; - - @WhenModified - private Instant updatedAt; - - public DStatsRecord() { - } - - public DStatsRecord(@NotNull UUID entityId, UUID relation, @NotNull String key, int value) { - this.entityId = entityId; - this.key = key; - this.value = value; - - if ( relation != null ) { - this.relation = relation; - } - } - - public DStatsRecord(@NotNull UUID entityId, @NotNull String key, int value) { - this(entityId, null, key, value); - } - - @Override - public UUID id() { - return entityId; - } - - @Override - public UUID relation() { - return relation.equals(EMPTY_UUID) ? null : relation; - } - - public String key() { - return key; - } - - @Override - public int value() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - -} diff --git a/common/src/main/resources/dbmigrations/h2/1.0__initial.sql b/common/src/main/resources/dbmigrations/h2/1.0__initial.sql index 199ca0c..71bf0c0 100644 --- a/common/src/main/resources/dbmigrations/h2/1.0__initial.sql +++ b/common/src/main/resources/dbmigrations/h2/1.0__initial.sql @@ -1,13 +1,29 @@ -- apply changes -create table stats ( +create table actors ( + id uuid not null, + type varchar(255) not null, + constraint pk_actors primary key (id,type) +); + +create table records ( id uuid not null, - entity_id uuid not null, - relation uuid default '00000000-0000-0000-0000-000000000000' not null, keyname varchar(255) not null, value integer default 0 not null, created_at timestamp not null, updated_at timestamp not null, - constraint uq_stats_entity_id_relation_keyname unique (entity_id,relation,keyname), - constraint pk_stats primary key (id) + constraint pk_records primary key (id) ); +create table records_actors ( + record_id uuid not null, + actor_id uuid not null, + constraint pk_records_actors primary key (record_id,actor_id) +); + +-- foreign keys and indices +create index ix_records_actors_records on records_actors (record_id); +alter table records_actors add constraint fk_records_actors_records foreign key (record_id) references records (id) on delete restrict on update restrict; + +create index ix_records_actors_actors on records_actors (actor_id); +alter table records_actors add constraint fk_records_actors_actors foreign key (actor_id) references actors (id) on delete restrict on update restrict; + diff --git a/common/src/main/resources/dbmigrations/model/1.0__initial.model.xml b/common/src/main/resources/dbmigrations/model/1.0__initial.model.xml index 7d2d402..4d2f459 100644 --- a/common/src/main/resources/dbmigrations/model/1.0__initial.model.xml +++ b/common/src/main/resources/dbmigrations/model/1.0__initial.model.xml @@ -1,15 +1,22 @@ - + + + + + - - - + + + + + + \ No newline at end of file diff --git a/common/src/main/resources/dbmigrations/mysql/1.0__initial.sql b/common/src/main/resources/dbmigrations/mysql/1.0__initial.sql index ffaf266..70dec8e 100644 --- a/common/src/main/resources/dbmigrations/mysql/1.0__initial.sql +++ b/common/src/main/resources/dbmigrations/mysql/1.0__initial.sql @@ -1,13 +1,29 @@ -- apply changes -create table stats ( +create table actors ( + id varchar(40) not null, + type varchar(255) not null, + constraint pk_actors primary key (id,type) +); + +create table records ( id varchar(40) not null, - entity_id varchar(40) not null, - relation varchar(40) default '00000000-0000-0000-0000-000000000000' not null, keyname varchar(255) not null, value integer default 0 not null, created_at datetime(6) not null, updated_at datetime(6) not null, - constraint uq_stats_entity_id_relation_keyname unique (entity_id,relation,keyname), - constraint pk_stats primary key (id) + constraint pk_records primary key (id) ); +create table records_actors ( + record_id varchar(40) not null, + actor_id varchar(40) not null, + constraint pk_records_actors primary key (record_id,actor_id) +); + +-- foreign keys and indices +create index ix_records_actors_records on records_actors (record_id); +alter table records_actors add constraint fk_records_actors_records foreign key (record_id) references records (id) on delete restrict on update restrict; + +create index ix_records_actors_actors on records_actors (actor_id); +alter table records_actors add constraint fk_records_actors_actors foreign key (actor_id) references actors (id) on delete restrict on update restrict; + diff --git a/spigot/src/main/java/com/guflimc/brick/stats/spigot/SpigotBrickStats.java b/spigot/src/main/java/com/guflimc/brick/stats/spigot/SpigotBrickStats.java index 4883e84..636d6cd 100644 --- a/spigot/src/main/java/com/guflimc/brick/stats/spigot/SpigotBrickStats.java +++ b/spigot/src/main/java/com/guflimc/brick/stats/spigot/SpigotBrickStats.java @@ -6,8 +6,8 @@ import com.guflimc.brick.stats.common.BrickStatsConfig; import com.guflimc.brick.stats.common.BrickStatsDatabaseContext; import com.guflimc.brick.stats.common.BrickStatsManager; -import com.guflimc.brick.stats.spigot.jobs.MovementJob; -import com.guflimc.brick.stats.spigot.jobs.PlayTimeJob; +import com.guflimc.brick.stats.spigot.tasks.MovementTask; +import com.guflimc.brick.stats.spigot.tasks.PlayTimeTask; import com.guflimc.brick.stats.spigot.listeners.BlockListener; import com.guflimc.brick.stats.spigot.listeners.DeathListener; import org.bukkit.plugin.PluginManager; @@ -53,8 +53,8 @@ public void onEnable() { pm.registerEvents(new DeathListener(), this); // start jobs - scheduler.syncRepeating(new PlayTimeJob(), 1, TimeUnit.SECONDS); - scheduler.syncRepeating(new MovementJob(), 1, TimeUnit.SECONDS); + scheduler.syncRepeating(new PlayTimeTask(), 1, TimeUnit.SECONDS); + scheduler.syncRepeating(new MovementTask(), 1, TimeUnit.SECONDS); getLogger().info("Enabled " + nameAndVersion() + "."); } diff --git a/spigot/src/main/java/com/guflimc/brick/stats/spigot/listeners/BlockListener.java b/spigot/src/main/java/com/guflimc/brick/stats/spigot/listeners/BlockListener.java index d8834a5..8e4b34c 100644 --- a/spigot/src/main/java/com/guflimc/brick/stats/spigot/listeners/BlockListener.java +++ b/spigot/src/main/java/com/guflimc/brick/stats/spigot/listeners/BlockListener.java @@ -1,6 +1,7 @@ package com.guflimc.brick.stats.spigot.listeners; import com.guflimc.brick.stats.api.StatsAPI; +import com.guflimc.brick.stats.api.actor.Actor; import com.guflimc.brick.stats.api.key.Keys; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -13,7 +14,7 @@ public class BlockListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onBreak(BlockBreakEvent event) { StatsAPI.get().update( - event.getPlayer().getUniqueId(), + Actor.player(event.getPlayer().getUniqueId()), Keys.BLOCKS_BROKEN.with(event.getBlock().getType()), x -> x + 1 ); @@ -22,7 +23,7 @@ public void onBreak(BlockBreakEvent event) { @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onPlace(BlockPlaceEvent event) { StatsAPI.get().update( - event.getPlayer().getUniqueId(), + Actor.player(event.getPlayer().getUniqueId()), Keys.BLOCKS_PLACED.with(event.getBlock().getType()), x -> x + 1 ); diff --git a/spigot/src/main/java/com/guflimc/brick/stats/spigot/listeners/DeathListener.java b/spigot/src/main/java/com/guflimc/brick/stats/spigot/listeners/DeathListener.java index c2cad78..d644cfb 100644 --- a/spigot/src/main/java/com/guflimc/brick/stats/spigot/listeners/DeathListener.java +++ b/spigot/src/main/java/com/guflimc/brick/stats/spigot/listeners/DeathListener.java @@ -1,6 +1,7 @@ package com.guflimc.brick.stats.spigot.listeners; import com.guflimc.brick.stats.api.StatsAPI; +import com.guflimc.brick.stats.api.actor.Actor; import com.guflimc.brick.stats.api.key.Keys; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -12,14 +13,14 @@ public class DeathListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onDeath(PlayerDeathEvent event) { - StatsAPI.get().update(event.getEntity().getUniqueId(), Keys.DEATHS, x -> x + 1); + StatsAPI.get().update(Actor.player(event.getEntity().getUniqueId()), Keys.DEATHS, x -> x + 1); Player killer = event.getEntity().getKiller(); if ( killer == null ) { return; } - StatsAPI.get().update(killer.getUniqueId(), Keys.KILLS, x -> x + 1); + StatsAPI.get().update(Actor.player(killer.getUniqueId()), Keys.KILLS, x -> x + 1); } } diff --git a/spigot/src/main/java/com/guflimc/brick/stats/spigot/jobs/MovementJob.java b/spigot/src/main/java/com/guflimc/brick/stats/spigot/tasks/MovementTask.java similarity index 84% rename from spigot/src/main/java/com/guflimc/brick/stats/spigot/jobs/MovementJob.java rename to spigot/src/main/java/com/guflimc/brick/stats/spigot/tasks/MovementTask.java index adc8fff..3260bc5 100644 --- a/spigot/src/main/java/com/guflimc/brick/stats/spigot/jobs/MovementJob.java +++ b/spigot/src/main/java/com/guflimc/brick/stats/spigot/tasks/MovementTask.java @@ -1,6 +1,7 @@ -package com.guflimc.brick.stats.spigot.jobs; +package com.guflimc.brick.stats.spigot.tasks; import com.guflimc.brick.stats.api.StatsAPI; +import com.guflimc.brick.stats.api.actor.Actor; import com.guflimc.brick.stats.api.key.Keys; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -8,7 +9,7 @@ import java.util.*; -public class MovementJob implements Runnable { +public class MovementTask implements Runnable { private final Map locations = new HashMap<>(); private final Map cache = new HashMap<>(); @@ -35,7 +36,7 @@ public void run() { if (current > 1) { int floor = (int) Math.floor(current); current -= floor; - StatsAPI.get().update(id, Keys.DISTANCE_MOVED, x -> x + floor); + StatsAPI.get().update(Actor.player(id), Keys.DISTANCE_MOVED, x -> x + floor); } cache.put(id, current); diff --git a/spigot/src/main/java/com/guflimc/brick/stats/spigot/jobs/PlayTimeJob.java b/spigot/src/main/java/com/guflimc/brick/stats/spigot/tasks/PlayTimeTask.java similarity index 61% rename from spigot/src/main/java/com/guflimc/brick/stats/spigot/jobs/PlayTimeJob.java rename to spigot/src/main/java/com/guflimc/brick/stats/spigot/tasks/PlayTimeTask.java index 117b946..8372900 100644 --- a/spigot/src/main/java/com/guflimc/brick/stats/spigot/jobs/PlayTimeJob.java +++ b/spigot/src/main/java/com/guflimc/brick/stats/spigot/tasks/PlayTimeTask.java @@ -1,15 +1,16 @@ -package com.guflimc.brick.stats.spigot.jobs; +package com.guflimc.brick.stats.spigot.tasks; import com.guflimc.brick.stats.api.StatsAPI; +import com.guflimc.brick.stats.api.actor.Actor; import com.guflimc.brick.stats.api.key.Keys; import org.bukkit.Bukkit; -public class PlayTimeJob implements Runnable { +public class PlayTimeTask implements Runnable { @Override public void run() { Bukkit.getOnlinePlayers().forEach(p -> StatsAPI.get().update( - p.getUniqueId(), + Actor.player(p.getUniqueId()), Keys.PLAY_TIME, x -> x + 1 ));