Compare commits

...

81 Commits

Author SHA1 Message Date
Sheldan
3cafc95ceb [maven-release-plugin] prepare release abstracto-application-1.5.0 2023-07-09 23:13:48 +02:00
Sheldan
6409bbaa1d [AB-98] adding twitch support
upgrading to java 17
upgrade of dependencies
2023-07-09 23:06:52 +02:00
Sheldan
346e462185 [AB-xxx] fixing exact temperatures not leading to an appropriate color for weather service 2023-06-22 22:49:47 +02:00
Sheldan
46baa79d3e [maven-release-plugin] prepare for next development iteration 2023-06-04 21:17:29 +02:00
Sheldan
650d062808 [maven-release-plugin] prepare release abstracto-application-1.4.26 2023-06-04 21:17:25 +02:00
Sheldan
bac9832819 [AB-90] adding poll functionality
adding select menu functionality
not automatically acknowledging button interactions
adding ability to define positions for components
adding method to remove components to channel service
always replacing message contents with edit message in a channel
adding ability to reply a modal to a button interaction
moving post target specific methods from server management service to post target management
2023-06-04 21:12:46 +02:00
Sheldan
efbcb5c84b [maven-release-plugin] prepare for next development iteration 2023-05-19 00:27:06 +02:00
Sheldan
fd70e6ac90 [maven-release-plugin] prepare release abstracto-application-1.4.25 2023-05-19 00:27:02 +02:00
Sheldan
29bde70796 [AB-93] fixing setting attached files to null 2023-05-19 00:24:45 +02:00
Sheldan
ecd4feabb2 [maven-release-plugin] prepare for next development iteration 2023-05-18 23:45:59 +02:00
Sheldan
abf60409f1 [maven-release-plugin] prepare release abstracto-application-1.4.24 2023-05-18 23:45:55 +02:00
Sheldan
080733957f [AB-93] fixing not initializing the attached files 2023-05-18 23:43:34 +02:00
Sheldan
8a41f366ae [maven-release-plugin] prepare for next development iteration 2023-05-18 22:53:56 +02:00
Sheldan
dbf478c44c [maven-release-plugin] prepare release abstracto-application-1.4.23 2023-05-18 22:53:52 +02:00
Sheldan
3df688571f [AB-93] changing how MessageToSend handles attached plaintext files 2023-05-18 22:32:56 +02:00
Sheldan
ca530949c6 [AB-92] fixing not providing member display object for modmail thread already exists embed 2023-05-18 01:23:42 +02:00
Sheldan
724930e5a4 [AB-xxx] updating readme
upgrading JDA version to 5.0.0-beta.5
2023-03-20 23:16:52 +01:00
Sheldan
2875da117a [AB-91] adding location id to the available information for weather model 2023-03-19 10:54:54 +01:00
Sheldan
f95ba6c28f [maven-release-plugin] prepare for next development iteration 2023-03-19 00:14:21 +01:00
Sheldan
1b2e7654f9 [maven-release-plugin] prepare release abstracto-application-1.4.22 2023-03-19 00:14:16 +01:00
Sheldan
54976ed1d4 [AB-89] adding command to retrieve weather data 2023-03-19 00:09:43 +01:00
Sheldan
735816f5dd [AB-xxx] adding feature to split field value by configurable amount
using larger images for member avatar
2023-03-05 13:49:32 +01:00
Sheldan
a984bdb84e [AB-86] fixing exception in case of a re-joining user without experience role 2023-02-26 14:16:27 +01:00
Sheldan
21add6585d [maven-release-plugin] prepare for next development iteration 2023-02-26 10:58:01 +01:00
Sheldan
99e72245f3 [maven-release-plugin] prepare release abstracto-application-1.4.21 2023-02-26 10:57:57 +01:00
Sheldan
9d184ff560 [AB-85] adding feature mode to automatically create a thread for suggestions
reworking post target service to function with optionals
fixing trying to add reactions for suggestions if there was no message created
not showing votes in case the buttons feature mode is active
2023-02-26 10:50:36 +01:00
Sheldan
97895f5c56 [AB-84] fixing incorrect calculation for agreement percentage 2023-02-25 18:33:02 +01:00
Sheldan
f091559c49 [AB-84] adding necessary information to suggestion update message 2023-02-25 18:19:01 +01:00
Sheldan
fa7730975e [maven-release-plugin] prepare for next development iteration 2023-02-15 00:05:59 +01:00
Sheldan
03d7b9e2e2 [maven-release-plugin] prepare release abstracto-application-1.4.20 2023-02-15 00:05:51 +01:00
Sheldan
a0bff12263 [AB-xxx] adding default value for user experience object 2023-02-14 23:54:42 +01:00
Sheldan
70e708601e [AB-xxx] exception logging improvements for experience tracking 2023-02-14 21:14:18 +01:00
Sheldan
f01418d0de [AB-xxx] upgrading JDA version to version 5.0.0-beta.3 2023-02-04 20:57:02 +01:00
Sheldan
ad539adb2a [maven-release-plugin] prepare for next development iteration 2023-02-04 18:26:38 +01:00
Sheldan
3c9fec989f [maven-release-plugin] prepare release abstracto-application-1.4.19 2023-02-04 18:26:33 +01:00
Sheldan
21db3e3ee5 [AB-xxx] fixing compatible versions for installer 2023-02-04 18:22:06 +01:00
Sheldan
5b1ad2e075 [maven-release-plugin] prepare for next development iteration 2023-02-04 17:23:34 +01:00
Sheldan
63b3f68bdb [maven-release-plugin] prepare release abstracto-application-1.4.18 2023-02-04 17:23:29 +01:00
Sheldan
7c1537c4a7 Revert "[maven-release-plugin] prepare release abstracto-application-1.4.14"
This reverts commit 9836998087.
2023-02-04 17:21:15 +01:00
Sheldan
9836998087 [maven-release-plugin] prepare release abstracto-application-1.4.14 2023-02-04 17:13:57 +01:00
Sheldan
8a1bb4cad8 [AB-xxx] small installer logging improvement 2023-02-04 17:11:57 +01:00
Sheldan
a9dadec8ef [maven-release-plugin] prepare for next development iteration 2023-02-04 14:52:26 +01:00
Sheldan
3eaffaef87 [maven-release-plugin] prepare release abstracto-application-1.4.17 2023-02-04 14:52:20 +01:00
Sheldan
b3a943e155 [AB-20] adding level up notification configuration possibility 2023-01-28 13:13:38 +01:00
Sheldan
06dd4af131 [maven-release-plugin] prepare for next development iteration 2023-01-12 01:37:16 +01:00
Sheldan
0d51469975 [maven-release-plugin] prepare release abstracto-application-1.4.16 2023-01-12 01:37:12 +01:00
Sheldan
aa10c88588 [AB-81] adding support for choices in slash command parameters 2023-01-12 01:34:30 +01:00
Sheldan
db27f64832 [AB-80] fixing not cleaning up emote record when changing emote 2023-01-10 19:15:27 +01:00
Sheldan
3903039aac [AB-79] fixing test 2022-12-27 13:12:10 +01:00
Sheldan
41f42ee110 [AB-79] fix not applying role for re-joining users upon sync 2022-12-27 13:03:59 +01:00
Sheldan
74f54e1257 [maven-release-plugin] prepare for next development iteration 2022-12-21 13:27:21 +01:00
Sheldan
a72e48f690 [maven-release-plugin] prepare release abstracto-application-1.4.15 2022-12-21 13:27:16 +01:00
Sheldan
a813af8b1f [AB-xxx] fixing issue of not providing percentage to rank correctly 2022-12-21 01:34:32 +01:00
Sheldan
abee7b2732 [AB-xxx] providing more information in the rank model 2022-12-20 22:15:05 +01:00
Sheldan
4c71ffbb7e [maven-release-plugin] prepare for next development iteration 2022-12-10 16:54:36 +01:00
Sheldan
18050e2a8a [maven-release-plugin] prepare release abstracto-application-1.4.14 2022-12-10 16:54:32 +01:00
Sheldan
a0d83763d4 [AB-78] fixing users with previous experience not awarding the experience role if they join and are pending 2022-12-10 15:44:09 +01:00
Sheldan
0461c8e4ec [AB-77] deferring the reply for report with context interactions
do not defer the interaction all the time for modal interactions
2022-12-10 12:59:47 +01:00
Sheldan
072f32975a [maven-release-plugin] prepare for next development iteration 2022-12-03 20:17:11 +01:00
Sheldan
6810d28c50 [maven-release-plugin] prepare release abstracto-application-1.4.13 2022-12-03 20:16:57 +01:00
Sheldan
650a9099c4 [AB-66] fixing not reducing the bid on mines
fixing reaction report sometimes running into a timeout
2022-12-03 20:14:18 +01:00
Sheldan
1217e03725 [maven-release-plugin] prepare for next development iteration 2022-12-02 21:20:57 +01:00
Sheldan
2e837d8738 [maven-release-plugin] prepare release abstracto-application-1.4.12 2022-12-02 21:20:53 +01:00
Sheldan
9ddd386c6f [AB-66] adding min mines ratio and other input restrictions for mines game which should avoid some too easy setups 2022-12-02 21:16:32 +01:00
Sheldan
4e1db26df7 [AB-xxx] fixing java version in build file 2022-12-02 01:58:48 +01:00
Sheldan
de335a1e2a [AB-xxx] updating versions of GitHub actions in release/build job 2022-12-02 01:55:36 +01:00
Sheldan
9ba0ed711e [maven-release-plugin] prepare for next development iteration 2022-12-02 01:36:51 +01:00
Sheldan
b7376bf522 [maven-release-plugin] prepare release abstracto-application-1.4.11 2022-12-02 01:36:46 +01:00
Sheldan
0d6182c5d5 [AB-66] adding mines game
fixing suggestion evaluation job not getting the correct parameters
adding feature dependent parameters
2022-12-02 01:33:38 +01:00
Sheldan
96c38763b1 [maven-release-plugin] prepare for next development iteration 2022-11-20 23:22:21 +01:00
Sheldan
97b3099156 [maven-release-plugin] prepare release abstracto-application-1.4.10 2022-11-20 23:22:16 +01:00
Sheldan
a0b2fc9c31 [AB-25] artificial delay 2022-11-20 23:20:07 +01:00
Sheldan
2db36ef96b [maven-release-plugin] prepare for next development iteration 2022-11-20 22:39:40 +01:00
Sheldan
08b3913b04 [maven-release-plugin] prepare release abstracto-application-1.4.9 2022-11-20 22:39:36 +01:00
Sheldan
7a7ec5654f [AB-25] changing member loading to be partitioned 2022-11-20 22:37:10 +01:00
Sheldan
74679c2ccc [maven-release-plugin] prepare for next development iteration 2022-11-20 19:53:42 +01:00
Sheldan
063581ba1a [maven-release-plugin] prepare release abstracto-application-1.4.8 2022-11-20 19:53:38 +01:00
Sheldan
5c7b018b2a [AB-25] refactoring experience collection to work instantly instead of delayed job
adding level up notification for experience
2022-11-20 19:49:34 +01:00
Sheldan
d315113395 [AB-76] adding evaluation job for suggestions 2022-11-11 00:06:16 +01:00
Sheldan
ea2f62b721 [AB-74] adding more information to output of transfer credits command 2022-11-09 22:53:36 +01:00
Sheldan
1a91275a2d [maven-release-plugin] prepare for next development iteration 2022-10-27 20:56:03 +02:00
486 changed files with 10891 additions and 2800 deletions

View File

@@ -1,7 +1,7 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Execute build and Sonar
name: Execute Build
on:
push:
@@ -19,26 +19,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 1.8
distribution: 'corretto'
java-version: 17
- name: Build with Maven
run: mvn -B install --file abstracto-application/pom.xml
- name: Setup sonarqube
uses: warchant/setup-sonar-scanner@v3
- name: Run sonarqube
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: sonar-scanner
-Dsonar.login=${{ secrets.SONAR_TOKEN }}
-Dsonar.organization=sheldan
-Dsonar.host.url=https://sonarcloud.io/
-Dsonar.projectKey=abstracto-core
-Dsonar.java.binaries=**/target/classes
-Dsonar.coverage.jacoco.xmlReportPaths=abstracto-application/coverage/target/site/jacoco-aggregate/jacoco.xml
-Dsonar.coverage.exclusions=**/*Test.java
- uses: actions/setup-ruby@v1
- name: Send Webhook Notification
if: always()

View File

@@ -6,13 +6,14 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
persist-credentials: false
- name: Set up Java for publishing to GitHub Packages
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 1.8
distribution: 'corretto'
java-version: 17
- name: Load current version
id: version
run: echo "version=$(mvn --file abstracto-application/pom.xml -q -Dexec.executable="echo" -Dexec.args='${project.version}' --non-recursive exec:exec)" >> $GITHUB_ENV
@@ -21,7 +22,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy documentation to GitHub pages
uses: JamesIves/github-pages-deploy-action@4.1.0
uses: JamesIves/github-pages-deploy-action@v4
with:
repository-name: Sheldan/abstracto-docs
target-folder: docs/${{ env.version }}
@@ -29,7 +30,7 @@ jobs:
ssh-key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
folder: abstracto-application/documentation/target/generated-docs
- name: Deploy documentation to GitHub pages latest
uses: JamesIves/github-pages-deploy-action@4.1.0
uses: JamesIves/github-pages-deploy-action@v4
with:
repository-name: Sheldan/abstracto-docs
target-folder: docs/current
@@ -37,7 +38,7 @@ jobs:
ssh-key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
folder: abstracto-application/documentation/target/generated-docs
- name: Login to GitHub Packages Docker Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: docker.pkg.github.com
username: ${{ github.repository_owner }}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Sheldan
Copyright (c) 2023 Sheldan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,8 +1,6 @@
# Abstracto
![Build](https://github.com/Sheldan/abstracto/workflows/Execute%20build%20and%20Sonar/badge.svg)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=abstracto-core&metric=coverage)](https://sonarcloud.io/dashboard?id=abstracto-core)
[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-white.svg)](https://sonarcloud.io/dashboard?id=abstracto-core)
![Build](https://github.com/Sheldan/abstracto/workflows/Execute%20Build/badge.svg)
[![GitHub license](https://img.shields.io/github/license/Sheldan/abstracto)](https://github.com/Sheldan/abstracto/blob/master/LICENSE)
@@ -14,22 +12,20 @@ An example implementation of this bot can be seen [here](https://github.com/Shel
## Technologies
* [JDA](https://github.com/DV8FromTheWorld/JDA/) The Discord API Wrapper in the version 5.0.0-alpha.21
* [Spring boot](https://github.com/spring-projects/spring-boot) is used as a framework to create standalone application in Java with Java EE methods. (including Dependency injection and more)
* [JDA](https://github.com/DV8FromTheWorld/JDA/) The Discord API Wrapper in the version 5.0.0-beta.5
* [Spring boot](https://github.com/spring-projects/spring-boot) is used as a framework to create standalone application in Java with Java EE methods. (including dependency injection and more)
* [Hibernate](https://github.com/hibernate/hibernate-orm) is used as a reference implementation of JPA.
* [Freemarker](https://github.com/apache/freemarker) is used as a templating engine. This is used to provide internationalization for user facing text and enable dynamic embed configuration.
* [Ehcache](https://github.com/ehcache/ehcache3) is used as a caching implementation.
* [Lombok](https://github.com/rzwitserloot/lombok) is used as a framework in order to speed up creation of container classes and builders.
* [Quartz](https://github.com/quartz-scheduler/quartz) is used as a scheduling framework in order to provide functionalities which either require a delayed or cronjob behaviour.
* [Docker](https://github.com/docker) is used to package the application into a container and [Docker Compose](https://github.com/docker/compose) is used to connect the required containers together.
* [Quartz](https://github.com/quartz-scheduler/quartz) is used as a scheduling framework in order to provide functionalities which either require a scheduled or cronjob behaviour.
* [Docker](https://github.com/docker) is used to package the application into an image and [Docker Compose](https://github.com/docker/compose) is used to build the images
* [Liquibase](https://github.com/liquibase/liquibase) is used to manage changes to the database
## Documentation
A detailed documentation of the pure form of Abstracto including the terminology and commands in HTML form is available [here](https://sheldan.github.io/abstracto-docs/current). The PDF is available [here](https://sheldan.github.io/abstracto-docs/current/documentation.pdf)
If you want to view the documentation to an earlier released version you need to append the desired version to the URL. The current version will be available aforementioned URL, but it is not right now, because Abstracto has not been released yet.
## Customization documentation
TBD when Abstracto is released, as the current version is still being adapted, because of findings from the example customization in Crimson.
## Issues
If you find any issue, feel free to create a GitHub issue.

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>anti-raid</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>anti-raid</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>assignable-roles</artifactId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>assignable-roles</artifactId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>assignable-roles-int</artifactId>

View File

@@ -6,7 +6,7 @@ import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;

View File

@@ -3,7 +3,7 @@ package dev.sheldan.abstracto.assignableroles.model.database;
import dev.sheldan.abstracto.assignableroles.model.condition.AssignableRoleConditionType;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
@Entity
@Table(name = "assignable_role_condition")

View File

@@ -4,7 +4,7 @@ import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;

View File

@@ -3,7 +3,7 @@ package dev.sheldan.abstracto.assignableroles.model.database;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>abstracto-modules</artifactId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>custom-command</artifactId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>custom-command</artifactId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@ package dev.sheldan.abstracto.customcommand.model.database;
import dev.sheldan.abstracto.core.models.database.*;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>dynamic-activity</artifactId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>dynamic-activity</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -2,7 +2,7 @@ package dev.sheldan.abstracto.activity.models;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.time.Instant;
@Builder

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>entertainment</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -89,14 +89,14 @@ public class Slots extends AbstractConditionableCommand {
.templated(true)
.build();
Parameter lowParameter = Parameter
Parameter bidParameter = Parameter
.builder()
.name(BID_PARAMETER)
.type(Integer.class)
.templated(true)
.validators(Arrays.asList(MinIntegerValueValidator.min(0L)))
.build();
parameters.add(lowParameter);
parameters.add(bidParameter);
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()

View File

@@ -11,10 +11,14 @@ import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentSlashCommandNames;
import dev.sheldan.abstracto.entertainment.model.command.TransferCreditsModel;
import dev.sheldan.abstracto.entertainment.service.EconomyService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
@@ -44,15 +48,25 @@ public class TransferCredits extends AbstractConditionableCommand {
@Autowired
private EconomyService economyService;
@Autowired
private ChannelService channelService;
@Override
public CommandResult execute(CommandContext commandContext) {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
Member targetMember = (Member) parameters.get(0);
Integer amount = (Integer) parameters.get(1);
AUserInAServer targetUser = userInServerManagementService.loadOrCreateUser(targetMember);
AUserInAServer sourceUser = userInServerManagementService.loadOrCreateUser(commandContext.getAuthor());
economyService.transferCredits(sourceUser, targetUser, amount.longValue());
return CommandResult.fromSuccess();
TransferCreditsModel responseModel = TransferCreditsModel
.builder()
.sourceMember(MemberDisplay.fromMember(commandContext.getAuthor()))
.targetMember(MemberDisplay.fromMember(targetMember))
.credits(amount)
.build();
return FutureUtils.toSingleFutureGeneric(channelService.sendEmbedTemplateInTextChannelList(TRANSFER_CREDITS_RESPONSE, responseModel, commandContext.getChannel()))
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
@@ -61,8 +75,14 @@ public class TransferCredits extends AbstractConditionableCommand {
Integer amount = slashCommandParameterService.getCommandOption(AMOUNT_PARAMETER, event, Integer.class);
AUserInAServer targetUser = userInServerManagementService.loadOrCreateUser(targetMember);
AUserInAServer sourceUser = userInServerManagementService.loadOrCreateUser(event.getMember());
TransferCreditsModel responseModel = TransferCreditsModel
.builder()
.sourceMember(MemberDisplay.fromMember(event.getMember()))
.targetMember(MemberDisplay.fromMember(targetMember))
.credits(amount)
.build();
economyService.transferCredits(sourceUser, targetUser, amount.longValue());
return interactionService.replyEmbed(TRANSFER_CREDITS_RESPONSE, event)
return interactionService.replyEmbed(TRANSFER_CREDITS_RESPONSE, responseModel, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@@ -105,7 +125,8 @@ public class TransferCredits extends AbstractConditionableCommand {
.templated(true)
.supportsEmbedException(true)
.parameters(parameters)
.causesReaction(true)
.causesReaction(false)
.async(true)
.help(helpInfo)
.build();
}

View File

@@ -0,0 +1,230 @@
package dev.sheldan.abstracto.entertainment.command.games;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.HelpInfo;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentSlashCommandNames;
import dev.sheldan.abstracto.entertainment.exception.NotEnoughCreditsException;
import dev.sheldan.abstracto.entertainment.model.command.games.MineBoard;
import dev.sheldan.abstracto.entertainment.model.database.EconomyUser;
import dev.sheldan.abstracto.entertainment.service.GameService;
import dev.sheldan.abstracto.entertainment.service.management.EconomyUserManagementService;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Component
public class Mines extends AbstractConditionableCommand {
private static final String MINES_COMMAND_NAME = "mines";
private static final String WIDTH_PARAMETER = "width";
private static final String HEIGHT_PARAMETER = "height";
private static final String MINES_PARAMETER = "mines";
private static final String CREDITS_PARAMETER = "credits";
public static final String MINE_BOARD_TEMPLATE_KEY = "mines_board_response";
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private FeatureFlagService featureFlagService;
@Autowired
private GameService gameService;
@Autowired
private TemplateService templateService;
@Autowired
private EconomyUserManagementService economyUserManagementService;
@Autowired
private ChannelService channelService;
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Integer width = 5;
if(slashCommandParameterService.hasCommandOption(WIDTH_PARAMETER, event)) {
width = slashCommandParameterService.getCommandOption(WIDTH_PARAMETER, event, Integer.class);
}
Integer height = 5;
if(slashCommandParameterService.hasCommandOption(HEIGHT_PARAMETER, event)) {
height = slashCommandParameterService.getCommandOption(HEIGHT_PARAMETER, event, Integer.class);
}
Integer mines = 5;
if(slashCommandParameterService.hasCommandOption(MINES_PARAMETER, event)) {
mines = slashCommandParameterService.getCommandOption(MINES_PARAMETER, event, Integer.class);
}
Integer credit = null;
long serverId = event.getGuild().getIdLong();
boolean economyEnabled = featureFlagService.getFeatureFlagValue(EntertainmentFeatureDefinition.ECONOMY, serverId);
if(economyEnabled){
credit = 50;
if(slashCommandParameterService.hasCommandOption(CREDITS_PARAMETER, event)) {
credit = slashCommandParameterService.getCommandOption(CREDITS_PARAMETER, event, Integer.class);
}
Optional<EconomyUser> userOptional = economyUserManagementService.getUser(ServerUser.fromMember(event.getMember()));
if(!userOptional.isPresent()) {
throw new NotEnoughCreditsException();
}
EconomyUser user = userOptional.get();
if(user.getCredits() < credit) {
throw new NotEnoughCreditsException();
}
}
MineBoard board = gameService.createBoard(width, height, mines, serverId);
board.setCreditsEnabled(economyEnabled);
board.setUserId(event.getMember().getIdLong());
board.setServerId(serverId);
board.setCredits(credit);
MessageToSend messageToSend = templateService.renderEmbedTemplate(MINE_BOARD_TEMPLATE_KEY, board);
return interactionService.replyMessageToSend(messageToSend, event)
.thenCompose(interactionHook -> interactionHook.retrieveOriginal().submit())
.thenApply(message -> {
gameService.persistMineBoardMessage(board, message);
return CommandResult.fromSuccess();
});
}
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Integer width = 5;
List<Object> parameters = commandContext.getParameters().getParameters();
if(!parameters.isEmpty()) {
width = (Integer) parameters.get(0);
}
Integer height = 5;
if(parameters.size() >= 2) {
height = (Integer) parameters.get(1);
}
Integer mines = 5;
if(parameters.size() >= 3) {
mines = (Integer) parameters.get(2);
}
Integer credit = null;
long serverId = commandContext.getGuild().getIdLong();
boolean economyEnabled = featureFlagService.getFeatureFlagValue(EntertainmentFeatureDefinition.ECONOMY, serverId);
if(economyEnabled){
credit = 50;
if(parameters.size() == 4) {
credit = (Integer) parameters.get(3);
}
Optional<EconomyUser> userOptional = economyUserManagementService.getUser(ServerUser.fromMember(commandContext.getAuthor()));
if(!userOptional.isPresent()) {
throw new NotEnoughCreditsException();
}
EconomyUser user = userOptional.get();
if(user.getCredits() < credit) {
throw new NotEnoughCreditsException();
}
}
MineBoard board = gameService.createBoard(width, height, mines, serverId);
board.setCreditsEnabled(economyEnabled);
board.setUserId(commandContext.getAuthor().getIdLong());
board.setServerId(serverId);
board.setCredits(credit);
MessageToSend messageToSend = templateService.renderEmbedTemplate(MINE_BOARD_TEMPLATE_KEY, board);
List<CompletableFuture<Message>> futures = channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel());
return FutureUtils.toSingleFutureGeneric(futures)
.thenAccept(unused -> gameService.persistMineBoardMessage(board, futures.get(0).join()))
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
Parameter widthParameter = Parameter
.builder()
.name(WIDTH_PARAMETER)
.type(Integer.class)
.optional(true)
.templated(true)
.build();
parameters.add(widthParameter);
Parameter heightParameter = Parameter
.builder()
.name(HEIGHT_PARAMETER)
.type(Integer.class)
.optional(true)
.templated(true)
.build();
parameters.add(heightParameter);
Parameter minesParameter = Parameter
.builder()
.name(MINES_PARAMETER)
.type(Integer.class)
.optional(true)
.templated(true)
.build();
parameters.add(minesParameter);
Parameter creditsParameter = Parameter
.builder()
.name(CREDITS_PARAMETER)
.type(Integer.class)
.optional(true)
.templated(true)
.dependentFeatures(Arrays.asList(EntertainmentFeatureDefinition.ECONOMY.getKey()))
.build();
parameters.add(creditsParameter);
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(EntertainmentSlashCommandNames.GAME)
.commandName(MINES_COMMAND_NAME)
.build();
return CommandConfiguration.builder()
.name(MINES_COMMAND_NAME)
.slashCommandConfig(slashCommandConfig)
.async(true)
.module(EntertainmentModuleDefinition.ENTERTAINMENT)
.templated(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.GAMES;
}
}

View File

@@ -0,0 +1,79 @@
package dev.sheldan.abstracto.entertainment.listener.interaction;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListener;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerModel;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.entertainment.command.games.Mines;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.model.command.games.MineBoard;
import dev.sheldan.abstracto.entertainment.model.command.games.MineBoardPayload;
import dev.sheldan.abstracto.entertainment.service.GameService;
import dev.sheldan.abstracto.entertainment.service.GameServiceBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class MinesButtonClickedListener implements ButtonClickedListener {
@Autowired
private GameService gameService;
@Autowired
private TemplateService templateService;
@Autowired
private InteractionService interactionService;
@Autowired
private FeatureFlagService featureFlagService;
@Override
public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
MineBoardPayload payload = (MineBoardPayload) model.getDeserializedPayload();
if(model.getEvent().getUser().getIdLong() != payload.getMineBoard().getUserId()) {
return ButtonClickedListenerResult.IGNORED;
}
MineBoard mineBoard = payload.getMineBoard();
if(!mineBoard.getState().equals(GameService.MineResult.CONTINUE)) {
return ButtonClickedListenerResult.IGNORED;
}
GameService.MineResult mineResult = gameService.uncoverField(mineBoard, payload.getX(), payload.getY());
mineBoard.setState(mineResult);
if(mineBoard.getState() != GameService.MineResult.CONTINUE) {
if(featureFlagService.getFeatureFlagValue(EntertainmentFeatureDefinition.ECONOMY, model.getServerId())){
gameService.evaluateCreditChanges(mineBoard);
}
gameService.uncoverBoard(mineBoard);
}
MessageToSend messageToSend = templateService.renderEmbedTemplate(Mines.MINE_BOARD_TEMPLATE_KEY, mineBoard);
interactionService.editOriginal(messageToSend, model.getEvent().getHook()).thenAccept(message -> {
gameService.updateMineBoard(mineBoard);
log.info("Updated original mineboard for board {}.", mineBoard.getBoardId());
});
return ButtonClickedListenerResult.ACKNOWLEDGED;
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.GAMES;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
@Override
public Boolean handlesEvent(ButtonClickedListenerModel model) {
return model.getOrigin().equals(GameServiceBean.MINES_BUTTON_ORIGIN);
}
}

View File

@@ -16,6 +16,7 @@ import java.util.Optional;
@Repository
public interface EconomyUserRepository extends JpaRepository<EconomyUser, Long> {
Optional<EconomyUser> findByUser(AUserInAServer aUserInAServer);
Optional<EconomyUser> findByServer_IdAndUser_UserReference_Id(Long serverId, Long userId);
@Query(value = "WITH economy_user_ranked AS" +
"( " +

View File

@@ -0,0 +1,312 @@
package dev.sheldan.abstracto.entertainment.service;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadService;
import dev.sheldan.abstracto.core.interaction.ComponentService;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.entertainment.exception.InvalidGameBoardException;
import dev.sheldan.abstracto.entertainment.model.command.games.MineBoard;
import dev.sheldan.abstracto.entertainment.model.command.games.MineBoardField;
import dev.sheldan.abstracto.entertainment.model.command.games.MineBoardPayload;
import dev.sheldan.abstracto.entertainment.model.command.games.MineBoardRow;
import dev.sheldan.abstracto.entertainment.model.database.EconomyUser;
import dev.sheldan.abstracto.entertainment.service.management.EconomyUserManagementService;
import net.dv8tion.jda.api.entities.Message;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.util.*;
import java.util.stream.Collectors;
import static dev.sheldan.abstracto.entertainment.config.GamesFeatureConfig.MINES_CREDITS_FACTOR;
import static dev.sheldan.abstracto.entertainment.config.GamesFeatureConfig.MINES_MINIMUM_MINES_RATIO;
@Component
public class GameServiceBean implements GameService {
public static final String MINES_BUTTON_ORIGIN = "MINES_BUTTON";
@Autowired
private SecureRandom secureRandom;
@Autowired
private ComponentService componentService;
@Autowired
private ComponentPayloadService componentPayloadService;
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private EconomyService economyService;
@Autowired
private EconomyUserManagementService economyUserManagementService;
@Autowired
private ConfigService configService;
@Override
public MineBoard createBoard(Integer width, Integer height, Integer mines, Long serverId) {
double minMinesRatio = configService.getDoubleValueOrConfigDefault(MINES_MINIMUM_MINES_RATIO, serverId);
if(mines >= width * height || width > 5 || height > 5 || mines <= 1 || height <= 1 || width <= 1) {
throw new InvalidGameBoardException(minMinesRatio);
}
if((double) mines / (width * height) < minMinesRatio) {
throw new InvalidGameBoardException(minMinesRatio);
}
MineBoard mineBoard = generateEmptyBoard(width, height);
mineBoard.setMineCount(mines);
fillWithMines(mineBoard);
evaluateCounters(mineBoard);
return mineBoard;
}
@Override
@Transactional
public void persistMineBoardMessage(MineBoard mineBoard, Message message) {
mineBoard.setMessageId(message.getIdLong());
mineBoard.setChannelId(message.getChannel().getIdLong());
AServer server = serverManagementService.loadServer(message.getGuild());
mineBoard.getFields().forEach(mineBoardField -> {
MineBoardPayload payload = MineBoardPayload
.builder()
.x(mineBoardField.getX())
.y(mineBoardField.getY())
.mineBoard(mineBoard)
.build();
String componentId = mineBoard.getBoardId() + "_" + mineBoardField.getX() + "_" + mineBoardField.getY();
componentPayloadService.createButtonPayload(componentId, payload, MINES_BUTTON_ORIGIN, server);
});
}
@Override
@Transactional
public void updateMineBoard(MineBoard mineBoard) {
mineBoard.getFields().forEach(mineBoardField -> {
MineBoardPayload newPayload = MineBoardPayload
.builder()
.x(mineBoardField.getX())
.y(mineBoardField.getY())
.mineBoard(mineBoard)
.build();
String componentId = mineBoard.getBoardId() + "_" + mineBoardField.getX() + "_" + mineBoardField.getY();
componentPayloadService.updateButtonPayload(componentId, newPayload);
});
}
@Override
public void uncoverBoard(MineBoard mineBoard) {
mineBoard.getFields().forEach(mineBoardField -> {
if(mineBoardField.getType().equals(MineBoardField.MineBoardFieldType.COVERED)) {
mineBoardField.setType(MineBoardField.MineBoardFieldType.UNCOVERED);
}
});
}
@Override
public void evaluateCreditChanges(MineBoard mineBoard) {
Long credits = mineBoard.getCredits().longValue();
Integer mineCount = mineBoard.getMineCount();
List<MineBoardField> allFields = mineBoard.getFields();
Integer leftFields = (int) allFields
.stream()
.filter(mineBoardField -> mineBoardField.getType().equals(MineBoardField.MineBoardFieldType.COVERED)
|| mineBoardField.getType().equals(MineBoardField.MineBoardFieldType.MINE))
.count();
Integer fieldCount = allFields.size();
Integer uncoveredFields = fieldCount - leftFields;
Long creditChange = (long) (calculateCreditFactor(fieldCount, mineCount, uncoveredFields) * credits);
if(mineBoard.getState().equals(MineResult.WON)) {
Double factor = configService.getDoubleValueOrConfigDefault(MINES_CREDITS_FACTOR, mineBoard.getServerId());
creditChange = (long) (creditChange * factor);
}
ServerUser serverUser = ServerUser
.builder()
.serverId(mineBoard.getServerId())
.userId(mineBoard.getUserId())
.build();
Optional<EconomyUser> economyUserOptional = economyUserManagementService.getUser(serverUser);
if(economyUserOptional.isPresent()) {
economyService.addCredits(economyUserOptional.get(), -credits);
economyService.addCredits(economyUserOptional.get(), creditChange);
}
mineBoard.setCreditChange(creditChange);
}
private double calculateCreditFactor(int totalFields, int mines, int uncovered) {
return ((double) uncovered / (totalFields - mines)) + totalFields / 25.0 - ((totalFields - mines) / 25.0);
}
@Override
public MineResult uncoverField(MineBoard board, Integer x, Integer y) {
return uncoverFieldOnBoard(board, x, y);
}
public GameService.MineResult uncoverFieldOnBoard(MineBoard board, Integer x, Integer y) {
MineBoardField field = board.getField(x, y);
if(!MineBoardField.canInteract(field.getType())) {
return GameService.MineResult.CONTINUE;
}
if(field.getType().equals(MineBoardField.MineBoardFieldType.MINE)) {
field.setType(MineBoardField.MineBoardFieldType.EXPLODED);
return GameService.MineResult.LOST;
}
if(field.getType().equals(MineBoardField.MineBoardFieldType.COVERED)) {
if(field.getCounterValue() == 0) {
Set<String> alreadyConsidered = new HashSet<>();
Queue<MineBoardField> toUncover = new LinkedList<>();
toUncover.add(field);
while(!toUncover.isEmpty()) {
MineBoardField fieldToHandle = toUncover.poll();
fieldToHandle.setType(MineBoardField.MineBoardFieldType.UNCOVERED);
alreadyConsidered.add(fieldToHandle.getX() + "_" + fieldToHandle.getY());
// only when we actually got a free field, we should add its neighbors to the next one to uncover
if(fieldToHandle.getCounterValue() == 0) {
List<MineBoardField> neighbors = getNeighbors(board, fieldToHandle.getX(), fieldToHandle.getY(), false);
List<MineBoardField> uncoverableNeighbors = neighbors
.stream().filter(mineBoardField -> mineBoardField.getType().equals(MineBoardField.MineBoardFieldType.COVERED))
.collect(Collectors.toList());
uncoverableNeighbors.forEach(mineBoardField -> {
if(!alreadyConsidered.contains(mineBoardField.getX() + "_" + mineBoardField.getY())) {
mineBoardField.setType(MineBoardField.MineBoardFieldType.UNCOVERED);
// only if t he newly found neighbor is a free field, we should discover its neighbors
if(mineBoardField.getCounterValue() == 0) {
toUncover.addAll(uncoverableNeighbors);
}
}
});
}
}
} else {
field.setType(MineBoardField.MineBoardFieldType.UNCOVERED);
}
if(hasWon(board)) {
return GameService.MineResult.WON;
}
return GameService.MineResult.CONTINUE;
}
throw new IllegalStateException("Did not find correct type of field.");
}
private List<MineBoardField> getNeighbors(MineBoard mineBoard, int xPosition, int yPosition) {
return getNeighbors(mineBoard, xPosition, yPosition, false);
}
private List<MineBoardField> getNeighbors(MineBoard board, int xPosition, int yPosition, boolean directOnly) {
List<MineBoardField> neighbors = new ArrayList<>();
boolean isFirstRow = yPosition == 0;
boolean isLastRow = yPosition == board.getRowCount() - 1;
boolean isFirstColumn = xPosition == 0;
boolean isLastColumn = xPosition == board.getColumnCount() - 1;
if(!isFirstColumn) {
if(!isFirstRow && !directOnly) {
neighbors.add(board.getField(xPosition - 1, yPosition - 1));
}
neighbors.add(board.getField(xPosition - 1, yPosition));
if(!isLastRow && !directOnly) {
neighbors.add(board.getField(xPosition - 1, yPosition + 1));
}
}
if(!isFirstRow && !directOnly) {
neighbors.add(board.getField(xPosition, yPosition - 1));
}
if(!isLastRow && !directOnly) {
neighbors.add(board.getField(xPosition, yPosition + 1));
}
if(!isLastColumn) {
if(!isFirstRow && !directOnly) {
neighbors.add(board.getField(xPosition + 1, yPosition - 1));
}
neighbors.add(board.getField(xPosition + 1, yPosition));
if(!isLastRow && !directOnly) {
neighbors.add(board.getField(xPosition + 1, yPosition + 1));
}
}
return neighbors;
}
public MineBoard generateEmptyBoard(Integer width, Integer height) {
List<MineBoardRow> rows = new ArrayList<>();
for (int y = 0; y < height; y++) {
List<MineBoardField> fields = new ArrayList<>();
for (int x = 0; x < width; x++) {
MineBoardField field = MineBoardField
.builder()
.y(y)
.x(x)
.counterValue(0)
.type(MineBoardField.MineBoardFieldType.COVERED)
.build();
fields.add(field);
}
MineBoardRow row = MineBoardRow
.builder()
.fields(fields)
.build();
rows.add(row);
}
return MineBoard
.builder()
.rows(rows)
.state(MineResult.CONTINUE)
.columnCount(width)
.creditChange(0L)
.rowCount(height)
.boardId(UUID.randomUUID().toString())
.build();
}
public void fillWithMines(MineBoard board) {
Set<String> usedKeys = new HashSet<>();
int maxIterations = 1_0000_000;
int iterations = 0;
List<Pair<Integer, Integer>> foundPositions = new ArrayList<>();
do {
int x = secureRandom.nextInt(board.getColumnCount());
int y = secureRandom.nextInt(board.getRowCount());
String positionKey = x + "_" + y;
if(!usedKeys.contains(positionKey)) {
foundPositions.add(Pair.of(x, y));
usedKeys.add(positionKey);
iterations = 0;
}
iterations++;
} while(foundPositions.size() < board.getMineCount() && iterations < maxIterations);
foundPositions.forEach(xYPair -> board.getRows().get(xYPair.getRight()).getFields().get(xYPair.getLeft()).setType(MineBoardField.MineBoardFieldType.MINE));
}
public void evaluateCounters(MineBoard board) {
board.getRows().forEach(mineBoardRow -> mineBoardRow.getFields().forEach(mineBoardField -> {
if(!mineBoardField.getType().equals(MineBoardField.MineBoardFieldType.MINE)) {
long mineCounts = getMineCounts(board, mineBoardField.getX(), mineBoardField.getY());
mineBoardField.setCounterValue((int) mineCounts);
}
}));
}
private long getMineCounts(MineBoard board, int xPosition, int yPosition) {
List<MineBoardField> neighbors = getNeighbors(board, xPosition, yPosition);
return neighbors
.stream()
.filter(mineBoardField -> mineBoardField.getType().equals(MineBoardField.MineBoardFieldType.MINE))
.count();
}
private boolean hasWon(MineBoard board) {
return board
.getFields()
.stream()
.noneMatch(mineBoardField ->
mineBoardField.getType().equals(MineBoardField.MineBoardFieldType.COVERED)
&& !mineBoardField.getType().equals(MineBoardField.MineBoardFieldType.MINE));
}
}

View File

@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.entertainment.service.management;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.EconomyLeaderboardResult;
@@ -39,6 +40,11 @@ public class EconomyUserManagementServiceBean implements EconomyUserManagementSe
return repository.findByUser(aUserInAServer);
}
@Override
public Optional<EconomyUser> getUser(ServerUser serverUser) {
return repository.findByServer_IdAndUser_UserReference_Id(serverUser.getServerId(), serverUser.getUserId());
}
@Override
public EconomyLeaderboardResult getRankOfUserInServer(AUserInAServer aUserInAServer) {
return repository.getRankOfUserInServer(aUserInAServer.getUserInServerId(), aUserInAServer.getServerReference().getId());

View File

@@ -20,6 +20,15 @@ abstracto.systemConfigs.slotsCooldown.longValue=30
abstracto.featureFlags.economy.featureName=economy
abstracto.featureFlags.economy.enabled=false
abstracto.featureFlags.games.featureName=games
abstracto.featureFlags.games.enabled=false
abstracto.systemConfigs.minesCreditsFactor.name=minesCreditsFactor
abstracto.systemConfigs.minesCreditsFactor.doubleValue=5
abstracto.systemConfigs.minesMinMineRatio.name=minesMinMineRatio
abstracto.systemConfigs.minesMinMineRatio.doubleValue=0.2
# for now this is fine
abstracto.systemConfigs.creditGambleJackpot.name=creditGambleJackpot
abstracto.systemConfigs.creditGambleJackpot.longValue=1000

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,20 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<property name="entertainmentModule" value="(SELECT id FROM module WHERE name = 'entertainment')"/>
<property name="gamesFeature" value="(SELECT id FROM feature WHERE key = 'games')"/>
<changeSet author="Sheldan" id="mines-commands">
<insert tableName="command">
<column name="name" value="mines"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${gamesFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="feature.xml" relativeToChangelogFile="true"/>
<include file="command.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="games_feature-insertion">
<insert tableName="feature">
<column name="key" value="games"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -11,4 +11,5 @@
<include file="1.2.9-entertainment/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.0/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.3/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.11/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>entertainment</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@ import lombok.Getter;
@Getter
public enum EntertainmentFeatureDefinition implements FeatureDefinition {
ENTERTAINMENT("entertainment"), ECONOMY("economy");
ENTERTAINMENT("entertainment"), ECONOMY("economy"), GAMES("games");
private String key;

View File

@@ -4,4 +4,5 @@ public class EntertainmentSlashCommandNames {
public static final String ENTERTAINMENT = "entertainment";
public static final String UTILITY = "utility";
public static final String ECONOMY = "economy";
public static final String GAME = "game";
}

View File

@@ -0,0 +1,26 @@
package dev.sheldan.abstracto.entertainment.config;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class GamesFeatureConfig implements FeatureConfig {
public static final String MINES_CREDITS_FACTOR = "minesCreditsFactor";
public static final String MINES_MINIMUM_MINES_RATIO = "minesMinMineRatio";
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.GAMES;
}
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(MINES_CREDITS_FACTOR, MINES_MINIMUM_MINES_RATIO);
}
}

View File

@@ -0,0 +1,27 @@
package dev.sheldan.abstracto.entertainment.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
import dev.sheldan.abstracto.entertainment.model.exception.InvalidGameBoardExceptionModel;
public class InvalidGameBoardException extends AbstractoTemplatableException {
private final InvalidGameBoardExceptionModel model;
public InvalidGameBoardException(Double minRatio) {
super();
this.model = InvalidGameBoardExceptionModel
.builder()
.minMinesRatio(minRatio)
.build();
}
@Override
public String getTemplateName() {
return "invalid_mine_board_config_exception";
}
@Override
public Object getTemplateModel() {
return model;
}
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class TransferCreditsModel {
private Integer credits;
private MemberDisplay sourceMember;
private MemberDisplay targetMember;
}

View File

@@ -0,0 +1,44 @@
package dev.sheldan.abstracto.entertainment.model.command.games;
import dev.sheldan.abstracto.entertainment.service.GameService;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@Builder
public class MineBoard {
private String boardId;
private Long userId;
private Long serverId;
private Long messageId;
private Long channelId;
private Integer rowCount;
private Integer columnCount;
private Integer credits;
private Long creditChange;
private boolean creditsEnabled;
private Integer mineCount;
private List<MineBoardRow> rows;
private GameService.MineResult state;
public MineBoardField getField(int x, int y) {
if(x > columnCount || y > rowCount) {
throw new IllegalArgumentException("Out of bounds access to board.");
}
MineBoardRow mineBoardRow = rows.get(y);
return mineBoardRow.getFields().get(x);
}
public List<MineBoardField> getFields() {
List<MineBoardField> fields = new ArrayList<>();
rows.forEach(mineBoardRow -> fields.addAll(mineBoardRow.getFields()));
return fields;
}
}

View File

@@ -0,0 +1,25 @@
package dev.sheldan.abstracto.entertainment.model.command.games;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Builder
@Setter
public class MineBoardField {
private MineBoardField.MineBoardFieldType type;
private Integer x;
private Integer y;
private Integer counterValue;
public enum MineBoardFieldType {
MINE, UNCOVERED, COVERED, EXPLODED
}
public static boolean canInteract(MineBoardFieldType currentType) {
return currentType != MineBoardFieldType.UNCOVERED && currentType != MineBoardFieldType.EXPLODED;
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.entertainment.model.command.games;
import dev.sheldan.abstracto.core.interaction.button.ButtonPayload;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Builder
@Setter
public class MineBoardPayload implements ButtonPayload {
private MineBoard mineBoard;
private Integer x;
private Integer y;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.entertainment.model.command.games;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Getter
@Builder
public class MineBoardRow {
private List<MineBoardField> fields;
}

View File

@@ -4,7 +4,7 @@ import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.entertainment.model.exception;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class InvalidGameBoardExceptionModel {
private Double minMinesRatio;
}

View File

@@ -0,0 +1,18 @@
package dev.sheldan.abstracto.entertainment.service;
import dev.sheldan.abstracto.entertainment.model.command.games.MineBoard;
import net.dv8tion.jda.api.entities.Message;
public interface GameService {
MineBoard createBoard(Integer width, Integer height, Integer mines, Long serverId);
void persistMineBoardMessage(MineBoard mineBoard, Message message);
void updateMineBoard(MineBoard mineBoard);
void uncoverBoard(MineBoard mineBoard);
void evaluateCreditChanges(MineBoard mineBoard);
MineResult uncoverField(MineBoard board, Integer x, Integer y);
enum MineResult {
WON, LOST, CONTINUE
}
}

View File

@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.entertainment.service.management;
import dev.sheldan.abstracto.core.models.ServerUser;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.EconomyLeaderboardResult;
@@ -11,6 +12,7 @@ import java.util.Optional;
public interface EconomyUserManagementService {
EconomyUser createUser(AUserInAServer aUserInAServer);
Optional<EconomyUser> getUser(AUserInAServer aUserInAServer);
Optional<EconomyUser> getUser(ServerUser serverUser);
EconomyLeaderboardResult getRankOfUserInServer(AUserInAServer aUserInAServer);
List<EconomyUser> getRanksInServer(AServer server, Integer page, Integer size);
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>abstracto-modules</artifactId>
<groupId>dev.sheldan.abstracto.modules</groupId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>experience-tracking</artifactId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -0,0 +1,103 @@
package dev.sheldan.abstracto.experience.command;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.HelpInfo;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.config.ExperienceSlashCommandNames;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class ExpLevelUpNotification extends AbstractConditionableCommand {
private static final String FLAG_PARAMETER = "newValue";
private static final String EXP_LEVEL_UP_NOTIFICATION_COMMAND = "expLevelUpNotification";
private static final String EXP_LEVEL_UP_NOTIFICATION_RESPONSE = "expLevelUpNotification_response";
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private AUserExperienceService aUserExperienceService;
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Override
public CommandResult execute(CommandContext commandContext) {
Boolean newValue = (Boolean) commandContext.getParameters().getParameters().get(0);
updateExpLevelNotification(commandContext.getAuthor(), newValue);
return CommandResult.fromSuccess();
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Boolean newValue = slashCommandParameterService.getCommandOption(FLAG_PARAMETER, event, Boolean.class);
updateExpLevelNotification(event.getMember(), newValue);
return interactionService.replyEmbed(EXP_LEVEL_UP_NOTIFICATION_RESPONSE, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
private void updateExpLevelNotification(Member member, Boolean newValue) {
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
aUserExperienceService.setLevelUpNotification(aUserInAServer, newValue);
}
@Override
public FeatureDefinition getFeature() {
return ExperienceFeatureDefinition.EXPERIENCE;
}
@Override
public CommandConfiguration getConfiguration() {
Parameter memberParameter = Parameter
.builder()
.name(FLAG_PARAMETER)
.templated(true)
.type(Boolean.class)
.build();
List<Parameter> parameters = Arrays.asList(memberParameter);
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(ExperienceSlashCommandNames.EXPERIENCE)
.commandName(EXP_LEVEL_UP_NOTIFICATION_COMMAND)
.build();
return CommandConfiguration.builder()
.name(EXP_LEVEL_UP_NOTIFICATION_COMMAND)
.module(ExperienceModuleDefinition.EXPERIENCE)
.slashCommandConfig(slashCommandConfig)
.causesReaction(true)
.supportsEmbedException(true)
.templated(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
}

View File

@@ -104,7 +104,17 @@ public class Rank extends AbstractConditionableCommand {
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(toRender);
AUserExperience experienceObj = userExperienceManagementService.findUserInServer(aUserInAServer);
log.info("Rendering rank for user {} in server {}.", toRender.getId(), toRender.getGuild().getId());
rankModel.setExperienceToNextLevel(experienceLevelService.calculateExperienceToNextLevel(experienceObj.getCurrentLevel().getLevel(), experienceObj.getExperience()));
Long currentExpNeeded = experienceObj.getCurrentLevel().getExperienceNeeded();
Long experienceNeededToNextLevel = experienceLevelService.calculateExperienceToNextLevel(experienceObj.getCurrentLevel().getLevel(), experienceObj.getExperience());
Long nextLevelExperience = experienceLevelService.calculateNextLevel(experienceObj.getCurrentLevel().getLevel()).getExperienceNeeded();
Long levelExperience = nextLevelExperience - currentExpNeeded;
Long inLevelExperience = experienceObj.getExperience() - currentExpNeeded;
rankModel.setExperienceForCurrentLevel(currentExpNeeded);
rankModel.setCurrentLevelPercentage(((float) inLevelExperience / levelExperience) * 100);
rankModel.setLevelExperience(levelExperience);
rankModel.setExperienceToNextLevel(experienceNeededToNextLevel);
rankModel.setInLevelExperience(inLevelExperience);
rankModel.setNextLevelExperience(nextLevelExperience);
return templateService.renderEmbedTemplate(RANK_POST_EMBED_TEMPLATE, rankModel, toRender.getGuild().getIdLong());
}

View File

@@ -10,8 +10,6 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.exception.EntityGuildMismatchException;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.service.ExperienceRoleService;
import lombok.extern.slf4j.Slf4j;
@@ -34,12 +32,6 @@ public class SetExpRole extends AbstractConditionableCommand {
@Autowired
private ExperienceRoleService experienceRoleService;
@Autowired
private RoleService roleService;
@Autowired
private RoleManagementService roleManagementService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Integer level = (Integer) commandContext.getParameters().getParameters().get(0);
@@ -48,7 +40,7 @@ public class SetExpRole extends AbstractConditionableCommand {
throw new EntityGuildMismatchException();
}
log.info("Setting role {} to be used for level {} on server {}", role.getId(), level, role.getGuild().getId());
return experienceRoleService.setRoleToLevel(role, level, commandContext.getChannel().getIdLong())
return experienceRoleService.setRoleToLevel(role, level, commandContext.getChannel())
.thenApply(aVoid -> CommandResult.fromSuccess());
}
@@ -56,8 +48,21 @@ public class SetExpRole extends AbstractConditionableCommand {
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
List<ParameterValidator> levelValidators = Arrays.asList(MinIntegerValueValidator.min(0L));
parameters.add(Parameter.builder().name("level").validators(levelValidators).templated(true).type(Integer.class).build());
parameters.add(Parameter.builder().name("role").templated(true).type(Role.class).build());
Parameter level = Parameter
.builder()
.name("level")
.validators(levelValidators)
.templated(true)
.type(Integer.class)
.build();
parameters.add(level);
Parameter role = Parameter
.builder()
.name("role")
.templated(true)
.type(Role.class)
.build();
parameters.add(role);
HelpInfo helpInfo = HelpInfo.builder().templated(true).hasExample(true).build();
return CommandConfiguration.builder()
.name("setExpRole")

View File

@@ -39,7 +39,7 @@ public class SyncRoles extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
AServer server = serverManagementService.loadServer(commandContext.getGuild());
log.info("Synchronizing roles on server {}", server.getId());
return userExperienceService.syncUserRolesWithFeedback(server, commandContext.getChannel().getIdLong())
return userExperienceService.syncUserRolesWithFeedback(server, commandContext.getChannel())
.thenApply(aVoid -> CommandResult.fromIgnored());
}

View File

@@ -52,7 +52,7 @@ public class UnSetExpRole extends AbstractConditionableCommand {
if(!experienceRole.isPresent()) {
throw new ExperienceRoleNotFoundException();
}
return experienceRoleService.unsetRoles(actualRole, commandContext.getChannel().getIdLong())
return experienceRoleService.unsetRoles(actualRole, commandContext.getChannel())
.thenApply(aVoid -> CommandResult.fromSuccess());
}

View File

@@ -1,6 +1,5 @@
package dev.sheldan.abstracto.experience.job;
import dev.sheldan.abstracto.experience.model.ServerExperience;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.RunTimeExperienceService;
import lombok.extern.slf4j.Slf4j;
@@ -12,10 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.List;
import java.util.Map;
/**
* This {@link QuartzJobBean job} is executed regularly and calls the the {@link AUserExperienceService service}
@@ -26,10 +21,7 @@ import java.util.Map;
@DisallowConcurrentExecution
@Component
@PersistJobDataAfterExecution
public class ExperiencePersistingJob extends QuartzJobBean {
@Autowired
private AUserExperienceService userExperienceService;
public class ExperienceCleanupJob extends QuartzJobBean {
@Autowired
private RunTimeExperienceService runTimeExperienceService;
@@ -37,19 +29,9 @@ public class ExperiencePersistingJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
runTimeExperienceService.takeLock();
log.info("Cleaning up experience runtime storage.");
try {
Map<Long, List<ServerExperience>> runtimeExperience = runTimeExperienceService.getRuntimeExperience();
log.info("Running experience persisting job.");
Long pastMinute = (Instant.now().getEpochSecond() / 60) - 1;
if(runtimeExperience.containsKey(pastMinute)) {
List<ServerExperience> foundServers = runtimeExperience.get(pastMinute);
log.info("Found experience from {} servers to persist.", foundServers.size());
userExperienceService.handleExperienceGain(foundServers).thenAccept(aVoid -> {
runTimeExperienceService.takeLock();
runTimeExperienceService.getRuntimeExperience().remove(pastMinute);
runTimeExperienceService.releaseLock();
});
}
runTimeExperienceService.cleanupRunTimeStorage();
} finally {
runTimeExperienceService.releaseLock();
}

View File

@@ -4,9 +4,7 @@ import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncMessageReceivedListener;
import dev.sheldan.abstracto.core.listener.sync.jda.MessageReceivedListener;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import lombok.extern.slf4j.Slf4j;
@@ -25,9 +23,6 @@ public class ExperienceTrackerListener implements AsyncMessageReceivedListener {
@Autowired
private AUserExperienceService userExperienceService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Override
public DefaultListenerResult execute(MessageReceivedModel model) {
Message message = model.getMessage();
@@ -35,8 +30,7 @@ public class ExperienceTrackerListener implements AsyncMessageReceivedListener {
return DefaultListenerResult.IGNORED;
}
if(userExperienceService.experienceGainEnabledInChannel(message.getChannel())) {
AUserInAServer cause = userInServerManagementService.loadOrCreateUser(model.getServerId(), model.getMessage().getAuthor().getIdLong());
userExperienceService.addExperience(cause);
userExperienceService.addExperience(message.getMember(), model.getMessage());
return DefaultListenerResult.PROCESSED;
} else {
return DefaultListenerResult.IGNORED;

View File

@@ -37,12 +37,16 @@ public class JoiningUserRoleListener implements AsyncJoinListener {
@Override
public DefaultListenerResult execute(MemberJoinModel model) {
if(model.getMember().isPending()) {
log.info("Joining member {} in guild {} is still pending - ignoring for experience role assignment.", model.getJoiningUser().getUserId(), model.getJoiningUser().getServerId());
return DefaultListenerResult.IGNORED;
}
Optional<AUserInAServer> userInAServerOptional = userInServerManagementService.loadUserOptional(model.getServerId(), model.getJoiningUser().getUserId());
userInAServerOptional.ifPresent(aUserInAServer -> {
Optional<AUserExperience> userExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(aUserInAServer.getUserInServerId());
if(userExperienceOptional.isPresent()) {
log.info("User {} joined {} with previous experience. Setting up experience role again (if necessary).", model.getJoiningUser().getUserId(), model.getServerId());
userExperienceService.syncForSingleUser(userExperienceOptional.get()).thenAccept(result ->
userExperienceService.syncForSingleUser(userExperienceOptional.get(), model.getMember(), true).thenAccept(result ->
log.info("Finished re-assigning experience for re-joining user {} in server {}.", model.getJoiningUser().getUserId(), model.getServerId())
);
} else {

View File

@@ -0,0 +1,60 @@
package dev.sheldan.abstracto.experience.listener;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncUpdatePendingListener;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.MemberUpdatePendingModel;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* If a {@link Member member} updates the pending status, this {@link AsyncUpdatePendingListener listener} retrieves the previously stored {@link AUserExperience experience} and gives the
* member the necessary {@link net.dv8tion.jda.api.entities.Role role} according to the current configuration, if any
*/
@Component
@Slf4j
public class MemberPendingRoleListener implements AsyncUpdatePendingListener {
@Autowired
private UserExperienceManagementService userExperienceManagementService;
@Autowired
private AUserExperienceService userExperienceService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Override
public DefaultListenerResult execute(MemberUpdatePendingModel model) {
Optional<AUserInAServer> userInAServerOptional = userInServerManagementService.loadUserOptional(model.getServerId(), model.getUser().getUserId());
userInAServerOptional.ifPresent(aUserInAServer -> {
Optional<AUserExperience> userExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(aUserInAServer.getUserInServerId());
if(userExperienceOptional.isPresent()) {
log.info("User {} updated pending status {} with previous experience. Setting up experience role again (if necessary).", model.getUser().getUserId(), model.getServerId());
userExperienceService.syncForSingleUser(userExperienceOptional.get(), model.getMember(), true).thenAccept(result ->
log.info("Finished re-assigning experience for update pending user {} in server {}.", model.getUser().getUserId(), model.getServerId())
);
} else {
log.info("Member updating pending {} in server {} does not have any previous experience. Not setting up anything.", model.getUser().getUserId(), model.getServerId());
}
});
return DefaultListenerResult.PROCESSED;
}
@Override
public FeatureDefinition getFeature() {
return ExperienceFeatureDefinition.EXPERIENCE;
}
}

View File

@@ -21,6 +21,7 @@ public interface ExperienceRoleRepository extends JpaRepository<AExperienceRole,
* @return The {@link AExperienceRole experienceRole} found or null if the query did not return any results
*/
Optional<AExperienceRole> findByRole(ARole role);
List<AExperienceRole> findByRole_IdIn(List<Long> role);
/**
* Finds a list of {@link AExperienceRole experienceRoles} (if there are multiple ones, because of misconfiguration) of the given

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@@ -52,4 +53,8 @@ public interface UserExperienceRepository extends JpaRepository<AUserExperience
"WHERE rank.id = :userInServerId", nativeQuery = true)
LeaderBoardEntryResult getRankOfUserInServer(@Param("userInServerId") Long id, @Param("serverId") Long serverId);
@Modifying(clearAutomatically = true)
@Query("update AUserExperience u set u.currentExperienceRole = null where u.currentExperienceRole.id = :roleId")
void removeExperienceRoleFromUsers(@Param("roleId") Long experienceRoleId);
}

View File

@@ -1,36 +1,51 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.models.database.*;
import dev.sheldan.abstracto.core.models.property.SystemConfigProperty;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.*;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureConfig;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureMode;
import dev.sheldan.abstracto.experience.exception.NoExperienceTrackedException;
import dev.sheldan.abstracto.experience.model.*;
import dev.sheldan.abstracto.experience.model.LeaderBoard;
import dev.sheldan.abstracto.experience.model.LeaderBoardEntry;
import dev.sheldan.abstracto.experience.model.RoleCalculationResult;
import dev.sheldan.abstracto.experience.model.database.*;
import dev.sheldan.abstracto.experience.model.template.LevelUpNotificationModel;
import dev.sheldan.abstracto.experience.model.template.UserSyncStatusModel;
import dev.sheldan.abstracto.experience.service.management.DisabledExpRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.apache.commons.collections4.ListUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static dev.sheldan.abstracto.experience.config.ExperienceFeatureConfig.EXP_COOLDOWN_SECONDS_KEY;
@Component
@Slf4j
public class AUserExperienceServiceBean implements AUserExperienceService {
@@ -81,38 +96,52 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
private ChannelGroupService channelGroupService;
@Autowired
private DefaultConfigManagementService defaultConfigManagementService;
private SecureRandom secureRandom;
@Autowired
private ChannelService channelService;
@Autowired
private FeatureModeService featureModeService;
@Autowired
private AUserExperienceServiceBean self;
@Autowired
@Qualifier("experienceUpdateExecutor")
private TaskExecutor experienceUpdateExecutor;
@Override
public void addExperience(AUserInAServer userInAServer) {
public void addExperience(Member member, Message message) {
runTimeExperienceService.takeLock();
try {
Long minute = Instant.now().getEpochSecond() / 60;
Map<Long, List<ServerExperience>> runtimeExperience = runTimeExperienceService.getRuntimeExperience();
Long serverId = userInAServer.getServerReference().getId();
Long userInServerId = userInAServer.getUserInServerId();
if(runtimeExperience.containsKey(minute)) {
log.debug("Minute {} already tracked, adding user {} in server {}.",
minute, userInAServer.getUserReference().getId(), serverId);
List<ServerExperience> existing = runtimeExperience.get(minute);
for (ServerExperience server : existing) {
if (server.getServerId().equals(serverId) && server.getUserInServerIds().stream().noneMatch(userInServerId::equals)) {
server.getUserInServerIds().add(userInServerId);
break;
Map<Long, Map<Long, Instant>> runtimeExperience = runTimeExperienceService.getRuntimeExperience();
Long serverId = member.getGuild().getIdLong();
Long userId = member.getIdLong();
boolean receivesNewExperience = false;
if(!runtimeExperience.containsKey(serverId)) {
runtimeExperience.put(serverId, new HashMap<>());
receivesNewExperience = true;
} else {
Map<Long, Instant> serverExperience = runtimeExperience.get(serverId);
if(!serverExperience.containsKey(userId)) {
receivesNewExperience = true;
} else {
Instant latestExperience = serverExperience.get(userId);
if(latestExperience.isBefore(Instant.now())) {
receivesNewExperience = true;
}
}
} else {
log.debug("Minute {} did not exist yet. Creating new entry for user {} in server {}.", minute, userInAServer.getUserReference().getId(), serverId);
ServerExperience serverExperience = ServerExperience
.builder()
.serverId(serverId)
.build();
serverExperience.getUserInServerIds().add(userInServerId);
runtimeExperience.put(minute, new ArrayList<>(Arrays.asList(serverExperience)));
}
if(receivesNewExperience) {
Map<Long, Instant> serverExperience = runtimeExperience.get(serverId);
// we store when the user is eligible for experience _again_
Long maxSeconds = configService.getLongValueOrConfigDefault(EXP_COOLDOWN_SECONDS_KEY, serverId);
serverExperience.put(userId, Instant.now().plus(maxSeconds, ChronoUnit.SECONDS));
CompletableFuture.runAsync(() -> self.addExperienceToMember(member, message), experienceUpdateExecutor).exceptionally(throwable -> {
log.error("Failed to add experience to member {} in server {}.", message.getAuthor().getId(), message.getGuild().getIdLong(), throwable);
return null;
});
}
} finally {
runTimeExperienceService.releaseLock();
@@ -148,317 +177,220 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
return false;
}
@Transactional
@Override
public CompletableFuture<Void> handleExperienceGain(List<ServerExperience> servers) {
List<ExperienceGainResult> resultFutures = new ArrayList<>();
List<CompletableFuture<RoleCalculationResult>> futures = new ArrayList<>();
CompletableFuture<Void> experienceFuture = new CompletableFuture<>();
// TODO what if there are a lot in here...., transaction size etc
servers.forEach(serverExp -> {
List<CompletableFuture<Member>> memberFutures = new ArrayList<>();
serverExp.getUserInServerIds().forEach(userInAServerId -> {
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(userInAServerId);
CompletableFuture<Member> memberFuture = memberService.getMemberInServerAsync(userInAServer);
memberFutures.add(memberFuture);
});
public CompletableFuture<Void> syncUserRolesWithFeedback(AServer server, MessageChannel messageChannel) {
List<AUserExperience> aUserExperiences = userExperienceManagementService.loadAllUsers(server);
List<Long> userIds = aUserExperiences
.stream()
.map(aUserExperience -> aUserExperience.getUser().getUserReference().getId())
.collect(Collectors.toList());
log.info("Synchronizing experience roles for {} users.", userIds.size());
CompletableFuture<Void> returnFuture = new CompletableFuture<>();
Long serverId = server.getId();
int supposedUserCount = userIds.size();
FutureUtils.toSingleFutureGeneric(memberFutures).whenComplete((unused, throwable) -> {
self.updateFoundMembers(memberFutures, serverExp.getServerId(), resultFutures, futures);
experienceFuture.complete(null);
List<List<Long>> partitionedUserIds = ListUtils.partition(userIds, 100);
List<CompletableFuture<List<Member>>> memberLoadingFutures = new ArrayList<>();
for (List<Long> userIdsPart : partitionedUserIds) {
memberLoadingFutures.add(memberService.getMembersInServerAsync(server.getId(), userIdsPart));
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
log.error("Failed to sleep.", e);
}
}
CompletableFutureList<List<Member>> listCompletableFutureList = new CompletableFutureList<>(memberLoadingFutures);
listCompletableFutureList.getMainFuture().whenComplete((result, throwable) -> {
List<Member> members = new ArrayList<>();
listCompletableFutureList.getFutures().forEach(listCompletableFuture -> members.addAll(listCompletableFuture.join()));
if(throwable != null) {
log.warn("Failed to load all members in server {} for syncing experience. We started with {} and got {}.",
serverId, supposedUserCount, members.size(), throwable);
}
self.syncUsers(members, serverId, messageChannel).thenAccept(unused -> {
log.info("Finished syncing users for experience roles.");
returnFuture.complete(null);
}).exceptionally(throwable1 -> {
log.error("Failed to sync members.", throwable);
returnFuture.complete(null);
return null;
});
}).exceptionally(throwable -> {
experienceFuture.completeExceptionally(throwable);
log.error("Failed to load members.", throwable);
returnFuture.complete(null);
return null;
});
});
return experienceFuture
.thenCompose(unused -> FutureUtils.toSingleFutureGeneric(futures))
.whenComplete((unused, throwable) -> self.persistExperienceChanges(resultFutures));
return returnFuture;
}
@Transactional
public void updateFoundMembers(List<CompletableFuture<Member>> memberFutures, Long serverId, List<ExperienceGainResult> resultFutures, List<CompletableFuture<RoleCalculationResult>> futures) {
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
SystemConfigProperty defaultExpMultiplier = defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY);
SystemConfigProperty defaultMinExp = defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.MIN_EXP_KEY);
SystemConfigProperty defaultMaxExp = defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.MAX_EXP_KEY);
AServer server = serverManagementService.loadOrCreate(serverId);
int minExp = configService.getLongValue(ExperienceFeatureConfig.MIN_EXP_KEY, serverId, defaultMinExp.getLongValue()).intValue();
int maxExp = configService.getLongValue(ExperienceFeatureConfig.MAX_EXP_KEY, serverId, defaultMaxExp.getLongValue()).intValue();
Double multiplier = configService.getDoubleValue(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, serverId, defaultExpMultiplier.getDoubleValue());
PrimitiveIterator.OfInt iterator = new Random().ints(memberFutures.size(), minExp, maxExp + 1).iterator();
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
public CompletableFuture<Void> syncUsers(List<Member> members, Long serverId, MessageChannel messageChannel) {
AtomicInteger currentCount = new AtomicInteger();
MessageToSend status = getUserSyncStatusUpdateModel(0, members.size(), serverId);
Message statusMessage = messageService.createStatusMessage(status, messageChannel).join();
AServer server = serverManagementService.loadServer(serverId);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
List<ADisabledExpRole> disabledExpRoles = disabledExpRoleManagementService.getDisabledRolesForServer(server);
List<ARole> disabledRoles = disabledExpRoles.stream().map(ADisabledExpRole::getRole).collect(Collectors.toList());
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
log.info("Handling {} experiences for server {}. Using {} roles.", memberFutures.size(), serverId, roles.size());
memberFutures.forEach(future -> {
if(!future.isCompletedExceptionally()) {
Integer gainedExperience = iterator.next();
gainedExperience = (int) Math.floor(gainedExperience * multiplier);
Member member = future.join();
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
Long userInServerId = userInAServer.getUserInServerId();
if(!roleService.hasAnyOfTheRoles(member, disabledRoles)) {
log.debug("Handling {}. The user might gain {}.", userInServerId, gainedExperience);
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInAServer.getUserInServerId());
if(aUserExperienceOptional.isPresent()) {
AUserExperience aUserExperience = aUserExperienceOptional.get();
if(Boolean.FALSE.equals(aUserExperience.getExperienceGainDisabled())) {
log.debug("User {} will gain experience.", userInServerId);
Long newExperienceCount = aUserExperience.getExperience() + gainedExperience.longValue();
AExperienceLevel newLevel = calculateLevel(levels, newExperienceCount);
CompletableFuture<RoleCalculationResult> resultFuture = updateUserRole(aUserExperience, roles, newLevel.getLevel());
Long newMessageCount = aUserExperience.getMessageCount() + 1L;
ExperienceGainResult calculationResult =
ExperienceGainResult
.builder()
.calculationResult(resultFuture)
.newExperience(newExperienceCount)
.newMessageCount(newMessageCount)
.newLevel(newLevel.getLevel())
.serverId(serverId)
.userInServerId(userInAServer.getUserInServerId())
.build();
resultFutures.add(calculationResult);
futures.add(resultFuture);
} else {
log.debug("Experience gain was disabled. User did not gain any experience.");
}
} else {
log.info("User experience for user {} was not found. Planning to create new instance.", userInAServer.getUserInServerId());
Long newExperience = gainedExperience.longValue();
AExperienceLevel newLevel = calculateLevel(levels, newExperience);
Long newMessageCount = 1L;
CompletableFuture<RoleCalculationResult> resultFuture = applyInitialRole(userInAServer, roles, newLevel.getLevel());
ExperienceGainResult calculationResult =
ExperienceGainResult
.builder()
.calculationResult(resultFuture)
.newExperience(newExperience)
.newMessageCount(newMessageCount)
.newLevel(newLevel.getLevel())
.createUserExperience(true)
.userInServerId(userInAServer.getUserInServerId())
.build();
resultFutures.add(calculationResult);
futures.add(resultFuture);
}
} else {
log.debug("User {} has a role which makes the user unable to gain experience.", userInAServer.getUserInServerId());
}
}
List<CompletableFuture<Void>> futures = members
.stream()
.map(member -> this.syncUser(member, roles)
.thenAccept(unused -> {
currentCount.set(currentCount.get() + 1);
log.debug("Finished synchronizing {} users.", currentCount.get());
if(currentCount.get() % 50 == 0) {
log.info("Notifying for {} current users with synchronize.", currentCount.get());
MessageToSend newStatus = getUserSyncStatusUpdateModel(currentCount.get(), members.size(), serverId);
messageService.updateStatusMessage(messageChannel, statusMessage.getIdLong(), newStatus);
}
}))
.collect(Collectors.toList());
return FutureUtils.toSingleFutureGeneric(futures).thenAccept(unused -> {
MessageToSend newStatus = getUserSyncStatusUpdateModel(currentCount.get(), members.size(), serverId);
messageService.updateStatusMessage(messageChannel, statusMessage.getIdLong(), newStatus);
});
}
/**
* Calculates the appropriate {@link AExperienceRole experienceRole} based on the current level and awards the referenced the {@link AUserInAServer userinAServer}
* the {@link net.dv8tion.jda.api.entities.Role role}. If the user already left the guild, this will not award a {@link net.dv8tion.jda.api.entities.Role}, but just
* return the instance in order to be persisted
* @param aUserInAServer The {@link AUserInAServer userInAServer} which does not have a {@link AUserExperience userExperience} object,
* therefore we need to calculate the appropriate role and award the role
* @param roles A list of {@link AExperienceRole experienceRoles} representing the configuration which is used to calculate the appropriate
* {@link AExperienceRole experienceRole}
* @param currentLevel The current level of the user which was reached.
* @return A {@link CompletableFuture future} containing the {@link RoleCalculationResult result} of the role calculation, which completes after the user has been awarded the role.
*/
private CompletableFuture<RoleCalculationResult> applyInitialRole(AUserInAServer aUserInAServer, List<AExperienceRole> roles, Integer currentLevel) {
AExperienceRole role = experienceRoleService.calculateRole(roles, currentLevel);
if(role == null) {
log.debug("No experience role calculated. Applying none to user {} in server {}.",
aUserInAServer.getUserReference().getId(), aUserInAServer.getServerReference().getId());
return CompletableFuture.completedFuture(RoleCalculationResult
.builder()
.userInServerId(aUserInAServer.getUserInServerId())
.experienceRoleId(null)
.build());
}
Long experienceRoleId = role.getId();
Long userInServerId = aUserInAServer.getUserInServerId();
log.debug("Applying {} as the first experience role for user {} in server {}.",
experienceRoleId, aUserInAServer.getUserReference().getId(), aUserInAServer.getServerReference().getId());
return roleService.addRoleToUserAsync(aUserInAServer, role.getRole()).thenApply(aVoid -> RoleCalculationResult
.builder()
.experienceRoleId(experienceRoleId)
.userInServerId(userInServerId)
.build());
public CompletableFuture<Void> syncUser(Member member, List<AExperienceRole> roles) {
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
AUserExperience userExperience = userExperienceManagementService.findByUserInServerId(aUserInAServer.getUserInServerId());
return calculateAndApplyExperienceRole(userExperience, member, roles, false);
}
/**
* Persists the list of {@link ExperienceGainResult results} in the database. If the creation of {@link AUserExperience userExperience} object was requested,
* this will happen here, also the correct level is selected
* @param resultFutures A list of {@link ExperienceGainResult results} which define what should be changed for the given {@link AUserExperience userExperience} object:
* The level, experience, experienceRole, message account could change, or the object could not even exist
*/
@Transactional
public void persistExperienceChanges(List<ExperienceGainResult> resultFutures) {
// we do have the _value_ of the level, but we require the actual instance
Map<Integer, AExperienceLevel> levels = experienceLevelManagementService.getLevelConfigAsMap();
log.info("Storing {} experience gain results.", resultFutures.size());
HashMap<Long, List<AExperienceRole>> serverRoleMapping = new HashMap<>();
resultFutures.forEach(experienceGainResult -> {
AUserInAServer user = userInServerManagementService.loadOrCreateUser(experienceGainResult.getUserInServerId());
AUserExperience userExperience;
if(experienceGainResult.isCreateUserExperience()) {
userExperience = userExperienceManagementService.createUserInServer(user);
log.info("Creating new experience user for user in server {}.", experienceGainResult.getUserInServerId());
@Override
public CompletableFuture<Void> syncForSingleUser(AUserExperience userExperience, Member member, boolean forceRoles) {
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(userExperience.getServer());
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
return calculateAndApplyExperienceRole(userExperience, member, roles, forceRoles);
}
private CompletableFuture<Void> calculateAndApplyExperienceRole(AUserExperience userExperience, Member member, List<AExperienceRole> roles, boolean forceRoles) {
AExperienceRole calculatedNewRole = experienceRoleService.calculateRole(roles, userExperience.getCurrentLevel().getLevel());
Long oldRoleId = userExperience.getCurrentExperienceRole() != null && userExperience.getCurrentExperienceRole().getRole() != null ? userExperience.getCurrentExperienceRole().getRole().getId() : null;
Long newRoleId = calculatedNewRole != null ? calculatedNewRole.getRole().getId() : null;
userExperience.setCurrentExperienceRole(calculatedNewRole);
CompletableFuture<Void> returningFuture;
if(!Objects.equals(oldRoleId, newRoleId) || forceRoles) {
CompletableFuture<Void> addingFuture;
if(oldRoleId != null) {
addingFuture = roleService.removeRoleFromMemberAsync(member, oldRoleId);
} else {
userExperience = userExperienceManagementService.findByUserInServerId(experienceGainResult.getUserInServerId());
addingFuture = CompletableFuture.completedFuture(null);
}
userExperience.setMessageCount(experienceGainResult.getNewMessageCount());
userExperience.setExperience(experienceGainResult.getNewExperience());
// only search the levels if the level changed, or if there is no level currently set
AExperienceLevel currentLevel = userExperience.getCurrentLevel();
boolean userExperienceHasLevel = currentLevel != null;
if(!userExperienceHasLevel || !currentLevel.getLevel().equals(experienceGainResult.getNewLevel())) {
AExperienceLevel foundLevel = levels.get(experienceGainResult.getNewLevel());
if(foundLevel != null) {
log.info("User {} in server {} changed the level. Old level {}. New level {}.", experienceGainResult.getUserInServerId(), experienceGainResult.getServerId(), currentLevel.getLevel(), experienceGainResult.getNewLevel());
userExperience.setCurrentLevel(foundLevel);
} else {
log.warn("User {} was present, but no level matching the calculation result {} could be found.", userExperience.getUser().getUserReference().getId(), experienceGainResult.getNewLevel());
}
CompletableFuture<Void> removingFeature;
if(newRoleId != null) {
removingFeature = roleService.addRoleToMemberAsync(member, newRoleId);
} else {
removingFeature = CompletableFuture.completedFuture(null);
}
AServer server = user.getServerReference();
// "Caching" the experience roles for this server
if(!serverRoleMapping.containsKey(server.getId())) {
serverRoleMapping.put(server.getId(), experienceRoleManagementService.getExperienceRolesForServer(server));
}
RoleCalculationResult roleCalculationResult = experienceGainResult.getCalculationResult().join();
if(roleCalculationResult.getExperienceRoleId() != null) {
AExperienceRole role = experienceRoleManagementService.getExperienceRoleById(roleCalculationResult.getExperienceRoleId());
userExperience.setCurrentExperienceRole(role);
}
if(experienceGainResult.isCreateUserExperience()) {
userExperienceManagementService.saveUser(userExperience);
}
});
}
@Override
public CompletableFuture<RoleCalculationResult> updateUserRole(AUserExperience userExperience, List<AExperienceRole> roles, Integer currentLevel) {
AUserInAServer user = userExperience.getUser();
Function<Void, RoleCalculationResult> returnNullRole = aVoid -> RoleCalculationResult
.builder()
.userInServerId(user.getUserInServerId())
.experienceRoleId(null)
.build();
Long userInServerId = user.getUserInServerId();
Long serverId = user.getServerReference().getId();
log.debug("Updating experience role for user {} in server {}", user.getUserReference().getId(), serverId);
AExperienceRole role = experienceRoleService.calculateRole(roles, currentLevel);
boolean currentlyHasNoExperienceRole = userExperience.getCurrentExperienceRole() == null;
// if calculation results in no role, do not add a role
if(role == null) {
log.debug("User {} in server {} does not have an experience role, according to new calculation.",
user.getUserReference().getId(), serverId);
// if the user has a experience role currently, remove it
if(!currentlyHasNoExperienceRole && !userExperience.getCurrentExperienceRole().getRole().getDeleted()){
return roleService.removeRoleFromUserAsync(user, userExperience.getCurrentExperienceRole().getRole())
.thenApply(returnNullRole);
}
return CompletableFuture.completedFuture(returnNullRole.apply(null));
returningFuture = CompletableFuture.allOf(addingFuture, removingFeature);
} else {
returningFuture = CompletableFuture.completedFuture(null);
}
Long experienceRoleId = role.getId();
Long roleId = role.getRole().getId();
// if the new role is already the one configured in the database
Long userId = user.getUserReference().getId();
Long oldUserExperienceRoleId = currentlyHasNoExperienceRole ? 0L : userExperience.getCurrentExperienceRole().getRole().getId();
return memberService.getMemberInServerAsync(user).thenCompose(member -> {
boolean userHasRoleAlready = roleService.memberHasRole(member, roleId);
boolean userHasOldRole = false;
boolean rolesChanged = true;
if(!currentlyHasNoExperienceRole) {
userHasOldRole = roleService.memberHasRole(member, oldUserExperienceRoleId);
rolesChanged = !roleId.equals(oldUserExperienceRoleId);
}
Function<Void, RoleCalculationResult> fullResult = aVoid -> RoleCalculationResult
.builder()
.experienceRoleId(experienceRoleId)
.userInServerId(userInServerId)
.build();
// if the roles changed or
// the user does not have the new target role already
// the user still has the old role
if(!userHasRoleAlready || userHasOldRole) {
CompletableFuture<Void> removalFuture;
if(userHasOldRole && rolesChanged) {
log.info("User {} in server {} loses experience role {}.", userId, serverId, oldUserExperienceRoleId);
removalFuture = roleService.removeRoleFromMemberAsync(member, oldUserExperienceRoleId);
} else {
removalFuture = CompletableFuture.completedFuture(null);
}
CompletableFuture<Void> addRoleFuture;
if(!userHasRoleAlready) {
log.info("User {} in server {} gets a new role {} because of experience.", userId, serverId, roleId);
addRoleFuture = roleService.addRoleToMemberAsync(member, roleId);
} else {
addRoleFuture = CompletableFuture.completedFuture(null);
}
return CompletableFuture.allOf(removalFuture, addRoleFuture).thenApply(fullResult);
}
// we are turning the full calculation result regardless
return CompletableFuture.completedFuture(fullResult.apply(null));
});
return returningFuture;
}
@Override
public List<CompletableFuture<RoleCalculationResult>> syncUserRoles(AServer server) {
List<CompletableFuture<RoleCalculationResult>> results = new ArrayList<>();
List<AUserExperience> aUserExperiences = userExperienceManagementService.loadAllUsers(server);
log.info("Found {} users to synchronize", aUserExperiences.size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
for (int i = 0; i < aUserExperiences.size(); i++) {
AUserExperience userExperience = aUserExperiences.get(i);
log.info("Synchronizing {} out of {}. User in Server {}.", i, aUserExperiences.size(), userExperience.getUser().getUserInServerId());
results.add(updateUserRole(userExperience, roles, userExperience.getCurrentLevel().getLevel()));
}
return results;
}
@Override
public CompletableFuture<Void> syncUserRolesWithFeedback(AServer server, Long channelId) {
AChannel channel = channelManagementService.loadChannel(channelId);
List<AUserExperience> aUserExperiences = userExperienceManagementService.loadAllUsers(server);
log.info("Found {} users to synchronize", aUserExperiences.size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
CompletableFutureList<RoleCalculationResult> calculations = executeActionOnUserExperiencesWithFeedBack(aUserExperiences, channel, (AUserExperience experience) -> updateUserRole(experience, roles, experience.getLevelOrDefault()));
return calculations.getMainFuture().thenAccept(aVoid ->
self.syncRolesInStorage(calculations.getObjects())
);
}
/**
* Updates the actually stored experience roles in the database
* @param results The list of {@link RoleCalculationResult results} which should be applied
*/
@Transactional
public void syncRolesInStorage(List<RoleCalculationResult> results) {
HashMap<Long, AExperienceRole> experienceRoleHashMap = new HashMap<>();
results.forEach(result -> {
if(result != null) {
AUserInAServer user = userInServerManagementService.loadOrCreateUser(result.getUserInServerId());
AUserExperience userExperience = userExperienceManagementService.findUserInServer(user);
log.debug("Updating experience role for {} in server {} to {}", user.getUserInServerId(), user.getServerReference().getId(), result.getExperienceRoleId());
if(result.getExperienceRoleId() != null) {
log.debug("User experience {} gets new experience role with id {}.", userExperience.getId(), result.getExperienceRoleId());
AExperienceRole role;
if(!experienceRoleHashMap.containsKey(result.getExperienceRoleId())) {
role = experienceRoleManagementService.getExperienceRoleById(result.getExperienceRoleId());
experienceRoleHashMap.put(result.getExperienceRoleId(), role);
} else {
role = experienceRoleHashMap.get(result.getExperienceRoleId());
}
userExperience.setCurrentExperienceRole(role);
} else {
log.debug("User experience {} does not get a user experience role.", userExperience.getId());
userExperience.setCurrentExperienceRole(null);
public void addExperienceToMember(Member member, Message message) {
long serverId = member.getGuild().getIdLong();
AServer server = serverManagementService.loadOrCreate(serverId);
List<ADisabledExpRole> disabledExpRoles = disabledExpRoleManagementService.getDisabledRolesForServer(server);
List<ARole> disabledRoles = disabledExpRoles
.stream()
.map(ADisabledExpRole::getRole)
.collect(Collectors.toList());
if(roleService.hasAnyOfTheRoles(member, disabledRoles)) {
log.debug("User {} has a experience disable role in server {} - not giving any experience.", member.getIdLong(), serverId);
return;
}
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
Long minExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MIN_EXP_KEY, serverId);
Long maxExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MAX_EXP_KEY, serverId);
Double multiplier = configService.getDoubleValueOrConfigDefault(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, serverId);
Long experienceRange = maxExp - minExp + 1;
Long gainedExperience = (secureRandom.nextInt(experienceRange.intValue()) + minExp);
gainedExperience = (long) Math.floor(gainedExperience * multiplier);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
Long userInServerId = userInAServer.getUserInServerId();
log.debug("Handling {}. The user might gain {}.", userInServerId, gainedExperience);
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInAServer.getUserInServerId());
AUserExperience aUserExperience = aUserExperienceOptional.orElseGet(() -> userExperienceManagementService.createUserInServer(userInAServer));
if(Boolean.FALSE.equals(aUserExperience.getExperienceGainDisabled())) {
Long oldExperience = aUserExperience.getExperience();
Long newExperienceCount = oldExperience + gainedExperience;
aUserExperience.setExperience(newExperienceCount);
AExperienceLevel newLevel = calculateLevel(levels, newExperienceCount);
RoleCalculationResult result = RoleCalculationResult
.builder()
.build();
if(!Objects.equals(newLevel.getLevel(), aUserExperience.getCurrentLevel().getLevel())) {
Integer oldLevel = aUserExperience.getCurrentLevel() != null ? aUserExperience.getCurrentLevel().getLevel() : 0;
log.info("User {} in server {} changed level. New {}, Old {}.", member.getIdLong(),
member.getGuild().getIdLong(), newLevel.getLevel(),
oldLevel);
aUserExperience.setCurrentLevel(newLevel);
AExperienceRole calculatedNewRole = experienceRoleService.calculateRole(roles, newLevel.getLevel());
Long oldRoleId = aUserExperience.getCurrentExperienceRole() != null && aUserExperience.getCurrentExperienceRole().getRole() != null ? aUserExperience.getCurrentExperienceRole().getRole().getId() : null;
Long newRoleId = calculatedNewRole != null && calculatedNewRole.getRole() != null ? calculatedNewRole.getRole().getId() : null;
result.setOldRoleId(oldRoleId);
result.setNewRoleId(newRoleId);
if(message != null
&& aUserExperience.getLevelUpNotification()
&& featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, serverId, ExperienceFeatureMode.LEVEL_UP_NOTIFICATION)) {
LevelUpNotificationModel model = LevelUpNotificationModel
.builder()
.memberDisplay(MemberDisplay.fromMember(member))
.oldExperience(oldExperience)
.newExperience(newExperienceCount)
.newLevel(newLevel.getLevel())
.oldLevel(oldLevel)
.newRole(oldRoleId != null ? RoleDisplay.fromRole(oldRoleId) : null)
.newRole(newRoleId != null ? RoleDisplay.fromRole(newRoleId) : null)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("experience_level_up_notification", model);
FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, message.getChannel())).thenAccept(unused -> {
log.info("Sent level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to send level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
return null;
});
}
aUserExperience.setCurrentExperienceRole(calculatedNewRole);
}
aUserExperience.setMessageCount(aUserExperience.getMessageCount() + 1L);
if(!aUserExperienceOptional.isPresent()) {
userExperienceManagementService.saveUser(aUserExperience);
}
if(!Objects.equals(result.getOldRoleId(), result.getNewRoleId())) {
if(result.getOldRoleId() != null) {
roleService.removeRoleFromMemberAsync(member, result.getOldRoleId()).thenAccept(unused -> {
log.debug("Removed role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from {} member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
if(result.getNewRoleId() != null) {
roleService.addRoleToMemberAsync(member, result.getNewRoleId()).thenAccept(unused -> {
log.debug("Added role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to add role {} to {} member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
}
}
});
} else {
log.debug("Experience gain was disabled. User did not gain any experience.");
}
}
@Override
@@ -488,29 +420,6 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
return userExperience;
}
@Override
public CompletableFutureList<RoleCalculationResult> executeActionOnUserExperiencesWithFeedBack(List<AUserExperience> experiences, AChannel channel, Function<AUserExperience, CompletableFuture<RoleCalculationResult>> toExecute) {
List<CompletableFuture<RoleCalculationResult>> futures = new ArrayList<>();
Long serverId = channel.getServer().getId();
MessageToSend status = getUserSyncStatusUpdateModel(0, experiences.size(), serverId);
Message statusMessage = messageService.createStatusMessage(status, channel).join();
int interval = Math.min(Math.max(experiences.size() / 10, 1), 100);
for (int i = 0; i < experiences.size(); i++) {
if((i % interval) == 1) {
log.info("Updating feedback message with new index {} out of {}.", i, experiences.size());
status = getUserSyncStatusUpdateModel(i, experiences.size(), serverId);
messageService.updateStatusMessage(channel, statusMessage.getIdLong(), status);
}
AUserExperience userExperience = experiences.get(i);
futures.add(toExecute.apply(userExperience));
log.debug("Synchronizing {} out of {}. User in server ID {}.", i, experiences.size(), userExperience.getUser().getUserInServerId());
}
status = getUserSyncStatusUpdateModel(experiences.size(), experiences.size(), serverId);
messageService.updateStatusMessage(channel, statusMessage.getIdLong(), status);
return new CompletableFutureList<>(futures);
}
@Override
public void disableExperienceForUser(AUserInAServer userInAServer) {
log.info("Disabling experience gain for user {} in server {}.", userInAServer.getUserReference().getId(), userInAServer.getServerReference().getId());
@@ -525,6 +434,13 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
userExperience.setExperienceGainDisabled(false);
}
@Override
public void setLevelUpNotification(AUserInAServer aUserInAServer, Boolean newValue) {
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(aUserInAServer.getUserInServerId());
AUserExperience aUserExperience = aUserExperienceOptional.orElseGet(() -> userExperienceManagementService.createUserInServer(aUserInAServer));
aUserExperience.setLevelUpNotification(newValue);
}
/**
* Renders a {@link MessageToSend messageToSend} to be used as a status message for the ongoing user synchronization
* @param current The amount of users which have been synced
@@ -537,15 +453,6 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
return templateService.renderEmbedTemplate("user_sync_status_message", statusModel, serverId);
}
@Override
public CompletableFuture<RoleCalculationResult> syncForSingleUser(AUserExperience userExperience) {
AUserInAServer user = userExperience.getUser();
log.info("Synchronizing for user {} in server {}.", user.getUserReference().getId(), user.getServerReference().getId());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(user.getServerReference());
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
return updateUserRole(userExperience, roles, userExperience.getLevelOrDefault());
}
@Override
public LeaderBoard findLeaderBoardData(AServer server, Integer page) {
if(page <= 0) {

View File

@@ -57,9 +57,14 @@ public class ExperienceLevelServiceBean implements ExperienceLevelService {
@Override
public Long calculateExperienceToNextLevel(Integer level, Long currentExperience) {
AExperienceLevel nextLevel = experienceLevelManagementService.getLevelOptional(level + 1)
.orElseThrow(() -> new AbstractoRunTimeException(String.format("Could not find level %s", level)));
AExperienceLevel nextLevel = calculateNextLevel(level);
return nextLevel.getExperienceNeeded() - currentExperience;
}
@Override
public AExperienceLevel calculateNextLevel(Integer level) {
return experienceLevelManagementService.getLevelOptional(level + 1)
.orElseThrow(() -> new AbstractoRunTimeException(String.format("Could not find level %s", level)));
}
}

View File

@@ -1,27 +1,29 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.MessageService;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.experience.model.RoleCalculationResult;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.template.LevelRole;
import dev.sheldan.abstracto.experience.model.template.UserSyncStatusModel;
import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Component
@@ -34,89 +36,46 @@ public class ExperienceRoleServiceBean implements ExperienceRoleService {
@Autowired
private ExperienceLevelManagementService experienceLevelService;
@Autowired
private AUserExperienceService userExperienceService;
@Autowired
private ExperienceRoleServiceBean self;
@Autowired
private RoleManagementService roleManagementService;
@Autowired
private ChannelManagementService channelManagementService;
@Autowired
private RoleService roleService;
/**
* UnSets the current configuration for the passed level, and sets the {@link ARole} to be used for this level
* in the given {@link AServer}
* @param role The {@link ARole} to set the level to
* @param level The level the {@link ARole} should be awarded at
*/
@Autowired
private TemplateService templateService;
@Autowired
private MessageService messageService;
@Override
public CompletableFuture<Void> setRoleToLevel(Role role, Integer level, Long channelId) {
Long roleId = role.getIdLong();
ARole aRoleToSet = roleManagementService.findRole(roleId);
public CompletableFuture<Void> setRoleToLevel(Role role, Integer level, GuildMessageChannel messageChannel) {
ARole aRoleToSet = roleManagementService.findRole(role.getIdLong());
List<AExperienceRole> experienceRoles = getExperienceRolesAtLevel(level, aRoleToSet.getServer());
List<ARole> rolesToUnset = experienceRoles.stream().map(AExperienceRole::getRole).collect(Collectors.toList());
List<ARole> rolesToUnset = experienceRoles
.stream()
.map(AExperienceRole::getRole)
.collect(Collectors.toList());
if(rolesToUnset.size() == 1 && rolesToUnset.contains(aRoleToSet)) {
return CompletableFuture.completedFuture(null);
}
if(!rolesToUnset.contains(aRoleToSet)) {
rolesToUnset.add(aRoleToSet);
}
AExperienceLevel experienceLevel;
if(!experienceRoles.isEmpty()) {
experienceLevel = experienceRoles.get(0).getLevel();
} else {
experienceLevel = experienceLevelService.getLevel(level);
}
AExperienceRole newExperienceRole = experienceRoleManagementService.setLevelToRole(experienceLevel, aRoleToSet);
Long newlyCreatedExperienceRoleId = newExperienceRole.getId();
CompletableFuture<Void> future = new CompletableFuture<>();
unsetRoles(rolesToUnset, channelId, newExperienceRole).thenAccept(aVoid ->
self.unsetRoleInDb(level, roleId)
).thenAccept(unused -> future.complete(null)).exceptionally(throwable -> {
self.deleteExperienceRoleViaId(newlyCreatedExperienceRoleId);
future.completeExceptionally(throwable);
return null;
});
return future;
experienceRoleManagementService.setLevelToRole(experienceLevel, aRoleToSet);
if(!rolesToUnset.isEmpty()) {
return unsetRoles(rolesToUnset, messageChannel);
} else {
return CompletableFuture.completedFuture(null);
}
}
@Transactional
public void deleteExperienceRoleViaId(Long newlyCreatedExperienceRoleId) {
AExperienceRole reLoadedRole = experienceRoleManagementService.getExperienceRoleById(newlyCreatedExperienceRoleId);
experienceRoleManagementService.unsetRole(reLoadedRole);
}
/**
* Removes all previous defined {@link AExperienceRole experienceRoles} from the given level and sets the {@link ARole}
* (defined by its ID) to the level.
* @param level The level which the {@link ARole role} should be set to
* @param roleId The ID of the {@link Role} which should have its level set
*/
@Transactional
public void unsetRoleInDb(Integer level, Long roleId) {
log.info("Unsetting role {} from level {}.", roleId, level);
AExperienceLevel experienceLevel = experienceLevelService.getLevelOptional(level).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find level %s", level)));
ARole loadedRole = roleManagementService.findRole(roleId);
experienceRoleManagementService.removeAllRoleAssignmentsForLevelInServerExceptRole(experienceLevel, loadedRole.getServer(), loadedRole);
experienceRoleManagementService.setLevelToRole(experienceLevel, loadedRole);
}
/**
* Deletes the {@link AExperienceRole} and recalculates the experience for all users which currently had the associated
* {@link net.dv8tion.jda.api.entities.Role}.
* @param role The {@link ARole} to remove from the {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole}
* configuration
*/
@Override
public CompletableFuture<Void> unsetRoles(ARole role, Long feedbackChannelId) {
return unsetRoles(Arrays.asList(role), feedbackChannelId);
public CompletableFuture<Void> unsetRoles(ARole role, GuildMessageChannel messageChannel) {
return unsetRoles(Arrays.asList(role), messageChannel);
}
@Override
@@ -125,64 +84,56 @@ public class ExperienceRoleServiceBean implements ExperienceRoleService {
return experienceRoleManagementService.getExperienceRolesAtLevelInServer(levelObj, server);
}
@Override
public CompletableFuture<Void> unsetRoles(List<ARole> rolesToUnset, Long feedbackChannelId) {
return unsetRoles(rolesToUnset, feedbackChannelId, null);
}
@Override
public CompletableFuture<Void> unsetRoles(List<ARole> rolesToUnset, Long feedbackChannelId, AExperienceRole toAdd) {
if(rolesToUnset.isEmpty()) {
return CompletableFuture.completedFuture(null);
public CompletableFuture<Void> unsetRoles(List<ARole> rolesToUnset, GuildMessageChannel messageChannel) {
List<AExperienceRole> rolesInServer = experienceRoleManagementService.getRolesInServer(rolesToUnset);
Integer totalCount = 0;
for (AExperienceRole aExperienceRole : rolesInServer) {
totalCount += aExperienceRole.getUsers().size();
}
AServer server = rolesToUnset.get(0).getServer();
AChannel channel = channelManagementService.loadChannel(feedbackChannelId);
List<AExperienceRole> experienceRolesNecessaryToRemove = new ArrayList<>();
List<AUserExperience> usersToUpdate = new ArrayList<>();
rolesToUnset.forEach(role -> {
Optional<AExperienceRole> roleInServerOptional = experienceRoleManagementService.getRoleInServerOptional(role);
if(roleInServerOptional.isPresent()) {
AExperienceRole experienceRole = roleInServerOptional.get();
experienceRolesNecessaryToRemove.add(experienceRole);
usersToUpdate.addAll(experienceRole.getUsers());
} else {
log.info("Experience role {} is not defined in server {} - skipping unset.", role.getId(), server.getId());
AtomicInteger totalCountAtomic = new AtomicInteger(totalCount);
long serverId = messageChannel.getGuild().getIdLong();
MessageToSend status = getUserSyncStatusUpdateModel(0, totalCount, serverId);
Message statusMessage = messageService.createStatusMessage(status, messageChannel).join();
AtomicInteger atomicInteger = new AtomicInteger();
List<CompletableFuture<Void>> futures = new ArrayList<>();
rolesInServer.forEach(experienceRole -> {
experienceRole.getUsers().forEach(aUserExperience -> {
futures.add(roleService.removeRoleFromUserAsync(aUserExperience.getUser(), experienceRole.getRole()).thenAccept(unused -> {
atomicInteger.set(atomicInteger.get() + 1);
log.debug("Finished synchronizing {} users.", atomicInteger.get());
if(atomicInteger.get() % 50 == 0) {
log.info("Notifying for {} current users with synchronize.", atomicInteger.get());
MessageToSend newStatus = getUserSyncStatusUpdateModel(atomicInteger.get(), totalCountAtomic.get(), serverId);
messageService.updateStatusMessage(messageChannel, statusMessage.getIdLong(), newStatus);
}
}));
});
});
CompletableFuture<Void> returningFuture = new CompletableFuture<>();
experienceRoleManagementService.unsetRoles(rolesInServer);
FutureUtils.toSingleFutureGeneric(futures).whenComplete((unused, throwable) -> {
MessageToSend newStatus = getUserSyncStatusUpdateModel(atomicInteger.get(), totalCountAtomic.get(), serverId);
messageService.updateStatusMessage(messageChannel, statusMessage.getIdLong(), newStatus);
if(throwable != null) {
log.warn("Failed to unset role in server {}.", serverId, throwable);
}
returningFuture.complete(null);
});
log.info("Recalculating the roles for {} users, because their current role was removed from experience tracking.", usersToUpdate.size());
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.removeIf(role1 -> experienceRolesNecessaryToRemove.stream().anyMatch(aExperienceRole -> aExperienceRole.getId().equals(role1.getId())));
if(toAdd != null) {
roles.add(toAdd);
}
roles.sort(Comparator.comparing(innerRole -> innerRole.getLevel().getLevel()));
List<Long> roleIds = experienceRolesNecessaryToRemove.stream().map(AExperienceRole::getId).collect(Collectors.toList());
if(toAdd != null) {
roleIds.removeIf(aLong -> aLong.equals(toAdd.getRole().getId()));
}
CompletableFutureList<RoleCalculationResult> calculationResults = userExperienceService.executeActionOnUserExperiencesWithFeedBack(usersToUpdate, channel,
(AUserExperience ex) -> userExperienceService.updateUserRole(ex, roles, ex.getLevelOrDefault()));
return calculationResults.getMainFuture().thenAccept(aVoid -> self.persistData(calculationResults, roleIds));
return returningFuture;
}
/**
* Stores the changed experience roles for all of the {@link AUserExperience userExperiences} which are referenced in the list of
* {@link RoleCalculationResult results}. This is only executed after a role is being "unset", which means, we also
* have to remove the existing {@link AExperienceRole experienceRole}
* @param results A list of {@link CompletableFuture futures} which each contain a {@link RoleCalculationResult result}, for the members who got
* their {@link AExperienceRole experienceRole} removed
* @param roleIds The IDs of the {@link AExperienceRole experienceRoles} which were removed from the experience roles
*/
@Transactional
public void persistData(CompletableFutureList<RoleCalculationResult> results, List<Long> roleIds) {
log.info("Persisting {} role calculation results.", results.getFutures().size());
roleIds.forEach(roleId -> {
log.info("Deleting experience role {}.", roleId);
deleteExperienceRoleViaId(roleId);
});
userExperienceService.syncRolesInStorage(results.getObjects());
private MessageToSend getUserSyncStatusUpdateModel(Integer current, Integer total, Long serverId) {
UserSyncStatusModel statusModel = UserSyncStatusModel
.builder()
.currentCount(current)
.totalUserCount(total)
.build();
return templateService.renderEmbedTemplate("user_sync_status_message", statusModel, serverId);
}
@Override
public AExperienceRole calculateRole(List<AExperienceRole> roles, Integer currentLevel) {
if(roles == null || roles.isEmpty()) {

View File

@@ -1,12 +1,9 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.metric.service.CounterMetric;
import dev.sheldan.abstracto.core.metric.service.MetricService;
import dev.sheldan.abstracto.experience.model.ServerExperience;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -20,18 +17,9 @@ import java.util.concurrent.locks.ReentrantLock;
@Component
public class RunTimeExperienceService {
@Autowired
private MetricService metricService;
public static final String EXPERIENCE_RUNTIME_STORAGE = "experience.runtime.storage";
private static final CounterMetric EXPERIENCE_RUNTIME_STORAGE_METRIC = CounterMetric
.builder()
.name(EXPERIENCE_RUNTIME_STORAGE)
.build();
private Map<Long, List<ServerExperience>> runtimeExperience = new HashMap<>();
private Map<Long,Map<Long, Instant>> runtimeExperience = new HashMap<>();
private static final Lock lock = new ReentrantLock();
public Map<Long, List<ServerExperience>> getRuntimeExperience() {
public Map<Long, Map<Long, Instant>> getRuntimeExperience() {
return runtimeExperience;
}
@@ -49,12 +37,16 @@ public class RunTimeExperienceService {
lock.unlock();
}
@PostConstruct
public void postConstruct() {
metricService.registerGauge(EXPERIENCE_RUNTIME_STORAGE_METRIC, runtimeExperience, serverList -> serverList.values().stream()
.mapToInt(minuteEntry -> minuteEntry.stream()
.mapToInt(individualServerList -> individualServerList.getUserInServerIds().size()).sum()).sum(),
"Number of entries in runtime experience storage");
public void cleanupRunTimeStorage() {
Instant now = Instant.now();
runtimeExperience.forEach((serverId, userInstantMap) -> {
List<Long> userIdsToRemove = new ArrayList<>();
userInstantMap.forEach((userId, instant) -> {
if(instant.isBefore(now)) {
userIdsToRemove.add(userId);
}
});
userIdsToRemove.forEach(userInstantMap::remove);
});
}
}

View File

@@ -3,7 +3,6 @@ package dev.sheldan.abstracto.experience.service.management;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.repository.ExperienceRoleRepository;
@@ -13,6 +12,7 @@ import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@Slf4j
@@ -22,7 +22,7 @@ public class ExperienceRoleManagementServiceBean implements ExperienceRoleManage
private ExperienceRoleRepository experienceRoleRepository;
@Autowired
private RoleManagementService roleManagementService;
private UserExperienceManagementService userExperienceManagementService;
/**
* Removes *all* assignments of roles for the given level
@@ -46,12 +46,30 @@ public class ExperienceRoleManagementServiceBean implements ExperienceRoleManage
experienceRoleRepository.delete(role);
}
@Override
public void unsetRoles(List<AExperienceRole> roles) {
log.info("Deleting {} roles.", roles.size());
roles.forEach(experienceRole -> {
userExperienceManagementService.removeExperienceRoleFromUsers(experienceRole);
});
experienceRoleRepository.deleteAll(roles);
}
@Override
public AExperienceRole getRoleInServer(ARole role) {
// TODO throw different exception
return this.getRoleInServerOptional(role).orElseThrow(AbstractoRunTimeException::new);
}
@Override
public List<AExperienceRole> getRolesInServer(List<ARole> roles) {
List<Long> roleIds = roles
.stream()
.map(ARole::getId)
.collect(Collectors.toList());
return experienceRoleRepository.findByRole_IdIn(roleIds);
}
@Override
public Optional<AExperienceRole> getRoleInServerOptional(ARole role) {
return experienceRoleRepository.findByRole(role);

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.core.exception.UserInServerNotFoundException;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.repository.UserExperienceRepository;
@@ -32,6 +33,11 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
return byId.orElseGet(() -> createUserInServer(aUserInAServer));
}
@Override
public void removeExperienceRoleFromUsers(AExperienceRole experienceRole) {
repository.removeExperienceRoleFromUsers(experienceRole.getId());
}
@Override
public Optional<AUserExperience> findByUserInServerIdOptional(Long userInServerId) {
return repository.findById(userInServerId);
@@ -52,6 +58,7 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage
.messageCount(0L)
.server(aUserInAServer.getServerReference())
.experienceGainDisabled(false)
.levelUpNotification(true)
.user(aUserInAServer)
.id(aUserInAServer.getUserInServerId())
.currentLevel(startingLevel)

View File

@@ -6,4 +6,11 @@ abstracto.systemConfigs.expMultiplier.name=expMultiplier
abstracto.systemConfigs.expMultiplier.doubleValue=1
abstracto.featureFlags.experience.featureName=experience
abstracto.featureFlags.experience.enabled=false
abstracto.featureFlags.experience.enabled=false
abstracto.systemConfigs.expCooldownSeconds.name=expCooldownSeconds
abstracto.systemConfigs.expCooldownSeconds.longValue=60
abstracto.featureModes.levelUpNotification.featureName=experience
abstracto.featureModes.levelUpNotification.mode=levelUpNotification
abstracto.featureModes.levelUpNotification.enabled=false

View File

@@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,19 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<property name="experienceModule" value="(SELECT id FROM module WHERE name = 'experience')"/>
<property name="experienceFeature" value="(SELECT id FROM feature WHERE key = 'experience')"/>
<changeSet author="Sheldan" id="experience-expLevelUpNotification_command">
<insert tableName="command">
<column name="name" value="expLevelUpNotification"/>
<column name="module_id" valueComputed="${experienceModule}"/>
<column name="feature_id" valueComputed="${experienceFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="command.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,12 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="user_experience.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,15 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="user_experience-add_level_up_notification">
<addColumn tableName="user_experience">
<column name="level_up_notification" type="BOOLEAN" defaultValueBoolean="true"/>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,11 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
<include file="update/updates.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,12 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="user_experience.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,13 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="drop_user_experience_role_foreign_key">
<dropForeignKeyConstraint baseTableName="user_experience" constraintName="fk_user_experience_role" />
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,18 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="experience-job-update_schedule">
<update tableName="scheduler_job">
<column name="cron_expression" value="0 0 * * * ?"/>
<column name="clazz" value="dev.sheldan.abstracto.experience.job.ExperienceCleanupJob"/>
<where>name='experienceJob'</where>
</update>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,12 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="jobs.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -8,4 +8,6 @@
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="1.0-experience/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.15/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.8/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.17/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -1,57 +0,0 @@
package dev.sheldan.abstracto.experience.command;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.experience.service.ExperienceRoleService;
import net.dv8tion.jda.api.entities.Role;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class SetExpRoleTest {
@InjectMocks
private SetExpRole testUnit;
@Mock
private ExperienceRoleService experienceRoleService;
@Mock
private RoleService roleService;
@Mock
private RoleManagementService roleManagementService;
private static final Long CHANNEL_ID = 4L;
@Test
public void setExpRole() {
CommandContext noParameters = CommandTestUtilities.getNoParameters();
Role roleToChange = Mockito.mock(Role.class);
when(roleToChange.getGuild()).thenReturn(noParameters.getGuild());
Integer levelToSetTo = 4;
when(noParameters.getChannel().getIdLong()).thenReturn(CHANNEL_ID);
CommandContext context = CommandTestUtilities.enhanceWithParameters(noParameters, Arrays.asList(levelToSetTo, roleToChange));
when(experienceRoleService.setRoleToLevel(roleToChange, levelToSetTo, CHANNEL_ID)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<CommandResult> result = testUnit.executeAsync(context);
CommandTestUtilities.checkSuccessfulCompletionAsync(result);
}
@Test
public void validateCommand() {
CommandConfigValidator.validateCommandConfiguration(testUnit.getConfiguration());
}
}

View File

@@ -8,6 +8,8 @@ import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -28,21 +30,19 @@ public class SyncRolesTest {
@Mock
private AUserExperienceService userExperienceService;
@Mock
private ChannelManagementService channelManagementService;
@Mock
private ServerManagementService serverManagementService;
private static final Long CHANNEL_ID = 4L;
@Mock
private GuildMessageChannel messageChannel;
@Test
public void executeCommand() {
CommandContext context = CommandTestUtilities.getNoParameters();
CommandContext context = Mockito.mock(CommandContext.class);
AServer server = Mockito.mock(AServer.class);
when(serverManagementService.loadServer(context.getGuild())).thenReturn(server);
when(context.getChannel().getIdLong()).thenReturn(CHANNEL_ID);
when(userExperienceService.syncUserRolesWithFeedback(server, CHANNEL_ID)).thenReturn(CompletableFuture.completedFuture(null));
when(context.getChannel()).thenReturn(messageChannel);
when(userExperienceService.syncUserRolesWithFeedback(server, messageChannel)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<CommandResult> result = testUnit.executeAsync(context);
CommandTestUtilities.checkSuccessfulCompletionAsync(result);
}

View File

@@ -1,63 +0,0 @@
package dev.sheldan.abstracto.experience.command;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.management.RoleManagementService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.service.ExperienceRoleService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class UnSetExpRoleTest {
@InjectMocks
private UnSetExpRole testUnit;
@Mock
private ExperienceRoleService experienceRoleService;
@Mock
private RoleManagementService roleManagementService;
@Mock
private ExperienceRoleManagementService experienceRoleManagementService;
private static final Long CHANNEL_ID = 4L;
@Test
public void setUnSetExpRole() {
CommandContext noParameters = CommandTestUtilities.getNoParameters();
ARole changedRole = Mockito.mock(ARole.class);
CommandContext context = CommandTestUtilities.enhanceWithParameters(noParameters, Arrays.asList(changedRole));
when(context.getChannel().getIdLong()).thenReturn(CHANNEL_ID);
ARole actualRole = Mockito.mock(ARole.class);
AServer server = Mockito.mock(AServer.class);
when(actualRole.getServer()).thenReturn(server);
when(roleManagementService.findRole(changedRole.getId())).thenReturn(actualRole);
when(experienceRoleManagementService.getRoleInServerOptional(actualRole)).thenReturn(Optional.of(Mockito.mock(AExperienceRole.class)));
when(experienceRoleService.unsetRoles(actualRole, CHANNEL_ID)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<CommandResult> result = testUnit.executeAsync(context);
CommandTestUtilities.checkSuccessfulCompletionAsync(result);
}
@Test
public void validateCommand() {
CommandConfigValidator.validateCommandConfiguration(testUnit.getConfiguration());
}
}

View File

@@ -1,60 +0,0 @@
package dev.sheldan.abstracto.experience.listener;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageType;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class ExperienceTrackerListenerTest {
@InjectMocks
public ExperienceTrackerListener testUnit;
@Mock
private AUserExperienceService userExperienceService;
@Mock
private UserInServerManagementService userInServerManagementService;
@Mock
private MessageReceivedModel model;
@Mock
private User user;
private static final Long SERVER_ID = 4L;
private static final Long USER_ID = 5L;
@Test
public void testExperienceTracking() {
AUserInAServer userInAServer = Mockito.mock(AUserInAServer.class);
Message mockedMessage = Mockito.mock(Message.class);
when(mockedMessage.isFromGuild()).thenReturn(true);
when(mockedMessage.isWebhookMessage()).thenReturn(false);
MessageChannelUnion channel = Mockito.mock(MessageChannelUnion.class);
MessageType type = MessageType.DEFAULT;
when(mockedMessage.getType()).thenReturn(type);
when(userInServerManagementService.loadOrCreateUser(SERVER_ID, USER_ID)).thenReturn(userInAServer);
when(model.getMessage()).thenReturn(mockedMessage);
when(userExperienceService.experienceGainEnabledInChannel(channel)).thenReturn(true);
when(mockedMessage.getChannel()).thenReturn(channel);
when(model.getServerId()).thenReturn(SERVER_ID);
when(mockedMessage.getAuthor()).thenReturn(user);
when(user.getIdLong()).thenReturn(USER_ID);
testUnit.execute(model);
verify(userExperienceService, times(1)).addExperience(userInAServer);
}
}

View File

@@ -8,6 +8,7 @@ import dev.sheldan.abstracto.core.service.management.UserInServerManagementServi
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import net.dv8tion.jda.api.entities.Member;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -46,6 +47,9 @@ public class JoiningUserRoleListenerTest {
@Mock
private MemberJoinModel model;
@Mock
private Member member;
private static final Long SERVER_ID = 1L;
private static final Long USER_ID = 2L;
private static final Long USER_IN_SERVER_ID = 3L;
@@ -63,16 +67,26 @@ public class JoiningUserRoleListenerTest {
public void testUserWithExperienceRejoining() {
AUserExperience experience = Mockito.mock(AUserExperience.class);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(experience));
when(userExperienceService.syncForSingleUser(experience)).thenReturn(CompletableFuture.completedFuture(null));
when(userExperienceService.syncForSingleUser(experience, member, true)).thenReturn(CompletableFuture.completedFuture(null));
when(model.getMember()).thenReturn(member);
DefaultListenerResult result = testUnit.execute(model);
Assert.assertEquals(DefaultListenerResult.PROCESSED, result);
}
@Test
public void testUserWithOutExperienceRejoining() {
when(model.getMember()).thenReturn(member);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.empty());
testUnit.execute(model);
verify(userExperienceService, times(0)).syncForSingleUser(any());
verify(userExperienceService, times(0)).syncForSingleUser(any(), any(), eq(true));
}
@Test
public void testPendingUserJoining() {
when(member.isPending()).thenReturn(true);
when(model.getMember()).thenReturn(member);
testUnit.execute(model);
verify(userExperienceService, times(0)).syncForSingleUser(any(), any(), eq(true));
}
}

View File

@@ -1,830 +0,0 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.models.database.*;
import dev.sheldan.abstracto.core.models.property.SystemConfigProperty;
import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.ChannelManagementService;
import dev.sheldan.abstracto.core.service.management.DefaultConfigManagementService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.service.management.UserInServerManagementService;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureConfig;
import dev.sheldan.abstracto.experience.exception.NoExperienceTrackedException;
import dev.sheldan.abstracto.experience.model.*;
import dev.sheldan.abstracto.experience.model.database.AExperienceLevel;
import dev.sheldan.abstracto.experience.model.database.AExperienceRole;
import dev.sheldan.abstracto.experience.model.database.AUserExperience;
import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult;
import dev.sheldan.abstracto.experience.model.template.UserSyncStatusModel;
import dev.sheldan.abstracto.experience.service.management.DisabledExpRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class AUserExperienceServiceBeanTest {
@InjectMocks
private AUserExperienceServiceBean testUnit;
@Mock
private UserExperienceManagementService userExperienceManagementService;
@Mock
private ExperienceRoleService experienceRoleService;
@Mock
private ExperienceLevelManagementService experienceLevelManagementService;
@Mock
private ExperienceRoleManagementService experienceRoleManagementService;
@Mock
private ConfigService configService;
@Mock
private RoleService roleService;
@Mock
private MessageService messageService;
@Mock
private TemplateService templateService;
@Mock
private ChannelManagementService channelManagementService;
@Mock
private DisabledExpRoleManagementService disabledExpRoleManagementService;
@Mock
private MemberService memberService;
@Mock
private ServerManagementService serverManagementService;
@Mock
private UserInServerManagementService userInServerManagementService;
@Mock
private AUserExperienceServiceBean self;
@Mock
private DefaultConfigManagementService defaultConfigManagementService;
@Mock
private AUserExperience userExperience;
@Mock
private AUserExperience userExperience2;
@Mock
private AUserInAServer aUserInAServer;
@Mock
private AUserInAServer aUserInAServer2;
@Mock
private AUser user;
@Mock
private AUser user2;
@Mock
private ServerExperience serverExperience;
@Mock
private AServer server;
@Mock
private Member firstMember;
@Mock
private Member secondMember;
@Mock
private AExperienceLevel level0;
@Mock
private AExperienceLevel level1;
@Mock
private AExperienceLevel level2;
@Mock
private AExperienceLevel level3;
@Mock
private AExperienceRole experienceRole1;
@Mock
private ARole aRole1;
@Mock
private AExperienceRole experienceRole2;
@Mock
private ARole aRole2;
private List<AExperienceLevel> levels = new ArrayList<>();
private List<AExperienceRole> experienceRoles = new ArrayList<>();
private static final Long USER_IN_SERVER_ID = 4L;
private static final Long USER_ID = 8L;
private static final Long SERVER_ID = 9L;
private static final Long CHANNEL_ID = 7L;
private static final Long DEFAULT_MIN_EXP = 10L;
private static final Long DEFAULT_MAX_EXP = 25L;
private static final Double DEFAULT_EXP_MULTIPLIER = 1D;
private static final Long LOW_EXP = 50L;
private static final Long MID_EXP = 250L;
private static final Long HIGH_EXP = 500L;
private static final Long LVL_0_EXP = 0L;
private static final Long LVL_1_EXP = 100L;
private static final Long LVL_2_EXP = 200L;
private static final Long LVL_3_EXP = 300L;
private static final Integer ZERO_LVL = 0;
private static final Integer SECOND_LVL = 2;
private static final Long ROLE_ID = 4L;
private static final Long SECOND_ROLE_ID = 7L;
private static final Long MESSAGE_COUNT = 10L;
@Before
public void setup() {
this.levels = Arrays.asList(level0, level1, level2, level3);
this.experienceRoles = Arrays.asList(experienceRole1, experienceRole2);
}
@Test
public void testCalculateLevelTooLow() {
this.levels = Arrays.asList(level0, level1);
setupLevels(1);
AExperienceLevel calculatedLevel = testUnit.calculateLevel(levels, LOW_EXP);
Assert.assertEquals(level0, calculatedLevel);
}
@Test
public void testCalculateLevelBetweenLevels() {
this.levels = Arrays.asList(level0, level1, level2);
setupLevels(3);
AExperienceLevel calculatedLevel = testUnit.calculateLevel(levels, MID_EXP);
Assert.assertEquals(level2, calculatedLevel);
}
@Test
public void testCalculateLevelTooHigh() {
this.levels = Arrays.asList(level0, level1, level2, level3);
setupLevels(3);
AExperienceLevel calculatedLevel = testUnit.calculateLevel(levels, HIGH_EXP);
Assert.assertEquals(level3, calculatedLevel);
}
@Test
public void testUpdateUserExperienceLevelChanged() {
AUserExperience experienceToCalculate = Mockito.mock(AUserExperience.class);
when(experienceToCalculate.getUser()).thenReturn(aUserInAServer);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(experienceToCalculate.getCurrentLevel()).thenReturn(level0);
when(level0.getLevel()).thenReturn(ZERO_LVL);
when(level2.getLevel()).thenReturn(SECOND_LVL);
setupLevels(3);
Assert.assertTrue(testUnit.updateUserLevel(experienceToCalculate, levels, MID_EXP));
verify(experienceToCalculate, times(1)).setCurrentLevel(level2);
}
@Test
public void testUpdateUserExperienceLevelNotChanged() {
AUserExperience experienceToCalculate = Mockito.mock(AUserExperience.class);
when(experienceToCalculate.getCurrentLevel()).thenReturn(level2);
when(experienceToCalculate.getExperience()).thenReturn(MID_EXP);
setupLevels(3);
Assert.assertFalse(testUnit.updateUserLevel(experienceToCalculate, levels, experienceToCalculate.getExperience()));
}
@Test
public void testHandleExperienceGainSingleUser() {
when(serverExperience.getUserInServerIds()).thenReturn(Arrays.asList(USER_IN_SERVER_ID));
when(userInServerManagementService.loadOrCreateUser(USER_IN_SERVER_ID)).thenReturn(aUserInAServer);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(serverExperience.getServerId()).thenReturn(SERVER_ID);
testUnit.handleExperienceGain(Arrays.asList(serverExperience)).join();
ArgumentCaptor<List<CompletableFuture<Member>>> listArgumentCaptor = ArgumentCaptor.forClass(List.class);
verify(self, times(1)).updateFoundMembers(listArgumentCaptor.capture(), eq(SERVER_ID), anyList(), anyList());
Assert.assertEquals(firstMember, listArgumentCaptor.getValue().get(0).join());
}
@Test
public void testHandleExperienceMemberFailed() {
when(serverExperience.getUserInServerIds()).thenReturn(Arrays.asList(USER_IN_SERVER_ID));
when(userInServerManagementService.loadOrCreateUser(USER_IN_SERVER_ID)).thenReturn(aUserInAServer);
CompletableFuture<Member> future = new CompletableFuture<>();
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(future);
future.completeExceptionally(new RuntimeException());
when(serverExperience.getServerId()).thenReturn(SERVER_ID);
testUnit.handleExperienceGain(Arrays.asList(serverExperience)).join();
ArgumentCaptor<List<CompletableFuture<Member>>> listArgumentCaptor = ArgumentCaptor.forClass(List.class);
verify(self, times(1)).updateFoundMembers(listArgumentCaptor.capture(), eq(SERVER_ID), anyList(), anyList());
Assert.assertTrue(listArgumentCaptor.getValue().get(0).isCompletedExceptionally());
}
@Test
public void testGainExpSingleUserLvlUpOneServerWithoutRole() {
/*
* In this scenario, the user has a role before, but the config changed, and now there are no experience roles.
* Hence the user should lose the experience role.
*/
setupServerId();
setupLevels(3);
when(experienceRole1.getRole()).thenReturn(aRole1);
setExperienceRoleLevels();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setupUserInServer();
when(userExperience.getMessageCount()).thenReturn(MESSAGE_COUNT);
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(null);
when(userExperience.getExperience()).thenReturn(500L);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
when(userExperience.getCurrentExperienceRole()).thenReturn(experienceRole1);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(roleService.removeRoleFromUserAsync(aUserInAServer, aRole1)).thenReturn(CompletableFuture.completedFuture(null));
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
Assert.assertEquals(1, experienceResults.size());
ExperienceGainResult result = experienceResults.get(0);
Assert.assertEquals(MESSAGE_COUNT + 1, result.getNewMessageCount().longValue());
Assert.assertEquals(1, roleCalculationResults.size());
RoleCalculationResult roleCalcResult = roleCalculationResults.get(0).join();
Assert.assertNull(roleCalcResult.getExperienceRoleId());
Assert.assertEquals(USER_IN_SERVER_ID, roleCalcResult.getUserInServerId());
verify(roleService, times(1)).removeRoleFromUserAsync(aUserInAServer, aRole1);
verify(roleService, times(0)).addRoleToUserAsync(any(AUserInAServer.class), any());
}
@Test
public void testLevelUpGainingNewRoleButUserAlreadyHasRole() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
setupUserInServer();
when(userExperience.getExperience()).thenReturn(199L);
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getRole()).thenReturn(aRole2);
when(experienceRole2.getLevel()).thenReturn(level1);
AExperienceRole newRole = experienceRole2;
when(aRole2.getId()).thenReturn(ROLE_ID);
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(newRole);
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).addRoleToMember(any(AUserInAServer.class), any(ARole.class));
verify(roleService, times(0)).removeRoleFromUser(any(AUserInAServer.class), any(ARole.class));
}
@Test
public void testLevelUpNotGainingNewRole() {
setupServerId();
when(experienceRole1.getRole()).thenReturn(aRole1);
setExperienceRoleLevels();
when(aRole1.getId()).thenReturn(ROLE_ID);
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
setupUserInServer();
when(userExperience.getExperience()).thenReturn(500L);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
AExperienceRole newRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(newRole);
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(newRole);
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).addRoleToMember(any(AUserInAServer.class), any(ARole.class));
verify(roleService, times(0)).removeRoleFromUser(any(AUserInAServer.class), any(ARole.class));
}
@Test
public void testHandleExperienceForUserNotLevelingUpWithoutExistingRole() {
setupServerId();
when(userExperience.getCurrentExperienceRole()).thenReturn(null);
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
setupUserInServer();
when(userExperience.getExperience()).thenReturn(500L);
AExperienceRole newRole = experienceRole1;
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(newRole);
when(aRole1.getId()).thenReturn(ROLE_ID);
setupTwoExperienceRoles();
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(false);
when(roleService.addRoleToMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
when(roleService.addRoleToMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).addRoleToUserAsync(any(AUserInAServer.class), any(ARole.class));
verify(roleService, times(0)).removeRoleFromUserAsync(any(AUserInAServer.class), any());
}
@Test
public void handleExpGainWithTooLittleForRole() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setupUserInServer();
when(userExperience.getExperience()).thenReturn(50L);
setExperienceRoleLevels();
when(userExperience.getCurrentExperienceRole()).thenReturn(null);
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(null);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).removeRoleFromUserAsync(any(AUserInAServer.class), any());
verify(roleService, times(0)).addRoleToUserAsync(any(AUserInAServer.class), any());
}
@Test
public void testUserHasExperienceRoleButNotAnymore() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setupUserInServer();
when(userExperience.getExperience()).thenReturn(50L);
AExperienceRole previousExperienceRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(previousExperienceRole);
when(experienceRole1.getRole()).thenReturn(aRole1);
setExperienceRoleLevels();
when(experienceRoleService.calculateRole(eq(experienceRoles), any())).thenReturn(null);
when(roleService.removeRoleFromUserAsync(eq(aUserInAServer), any())).thenReturn(CompletableFuture.completedFuture(null));
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).addRoleToUserAsync(eq(aUserInAServer), any());
verify(roleService, times(1)).removeRoleFromUserAsync(eq(aUserInAServer), any());
}
@Test
public void testHandleExperienceGainForGainDisabledForUser() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setExperienceRoleLevels();
when(userExperience.getExperienceGainDisabled()).thenReturn(true);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userInServerManagementService.loadOrCreateUser(firstMember)).thenReturn(aUserInAServer);
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).removeRoleFromUserAsync(eq(aUserInAServer), any());
verify(roleService, times(0)).addRoleToUserAsync(eq(aUserInAServer), any());
}
@Test
public void testHandleExperienceGainForGainDisabledForRole() {
setupServerId();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
setExperienceRoleLevels();
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
when(roleService.hasAnyOfTheRoles(eq(firstMember), anyList())).thenReturn(true);
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userInServerManagementService.loadOrCreateUser(firstMember)).thenReturn(aUserInAServer);
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).removeRoleFromUser(aUserInAServer, aRole1);
verify(roleService, times(0)).addRoleToMember(eq(aUserInAServer), any(ARole.class));
}
@Test
public void testHandleExperienceForUserNotLevelingUpWithExistingRole() {
setupServerId();
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(roleService.hasAnyOfTheRoles(eq(firstMember), anyList())).thenReturn(false);
when(user.getId()).thenReturn(USER_ID);
setExperienceRoleLevels();
setupServerConfig();
setupDefaultConfig();
setupLevelsAndRolesAndNoDisallowed();
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userInServerManagementService.loadOrCreateUser(firstMember)).thenReturn(aUserInAServer);
ArrayList<ExperienceGainResult> experienceResults = new ArrayList<>();
ArrayList<CompletableFuture<RoleCalculationResult>> roleCalculationResults = new ArrayList<>();
List<CompletableFuture<Member>> memberFutures = Arrays.asList(CompletableFuture.completedFuture(firstMember));
testUnit.updateFoundMembers(memberFutures, SERVER_ID, experienceResults, roleCalculationResults);
verify(roleService, times(0)).removeRoleFromUser(aUserInAServer, aRole1);
verify(roleService, times(0)).addRoleToMember(eq(aUserInAServer), any(ARole.class));
}
@Test
public void testSyncNoRoleUserGettingRole2() {
userExperience.setCurrentExperienceRole(null);
AExperienceRole afterRole = experienceRole1;
when(experienceRole1.getRole()).thenReturn(aRole1);
when(experienceRole1.getId()).thenReturn(ROLE_ID);
when(aRole1.getId()).thenReturn(ROLE_ID);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(afterRole);
when(memberService.getMemberInServerAsync(userExperience.getUser())).thenReturn(CompletableFuture.completedFuture(firstMember));
when(roleService.addRoleToMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<RoleCalculationResult> calculationFuture = testUnit.syncForSingleUser(userExperience);
RoleCalculationResult result = calculationFuture.join();
Assert.assertEquals(ROLE_ID, result.getExperienceRoleId());
}
@Test
public void testSyncUserLosingRole() {
AExperienceRole beforeRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(beforeRole);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(experienceRole1.getRole()).thenReturn(aRole1);
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(null);
when(roleService.removeRoleFromUserAsync(aUserInAServer, aRole1)).thenReturn(CompletableFuture.completedFuture(null));
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
CompletableFuture<RoleCalculationResult> calculationFuture = testUnit.syncForSingleUser(userExperience);
RoleCalculationResult result = calculationFuture.join();
Assert.assertNull(result.getExperienceRoleId());
}
@Test
public void testSyncUserKeepingRole() {
AExperienceRole beforeRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(beforeRole);
AExperienceRole afterRole = experienceRole1;
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(aRole1.getId()).thenReturn(ROLE_ID);
when(experienceRole1.getId()).thenReturn(ROLE_ID);
when(experienceRole1.getRole()).thenReturn(aRole1);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(afterRole);
when(memberService.getMemberInServerAsync(userExperience.getUser())).thenReturn(CompletableFuture.completedFuture(firstMember));
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
CompletableFuture<RoleCalculationResult> calculationFuture = testUnit.syncForSingleUser(userExperience);
RoleCalculationResult result = calculationFuture.join();
Assert.assertEquals(ROLE_ID, result.getExperienceRoleId());
}
@Test
public void testSyncUserChangingRole() {
AExperienceRole beforeRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(beforeRole);
AExperienceRole afterRole = experienceRole2;
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(aRole1.getId()).thenReturn(ROLE_ID);
when(aRole2.getId()).thenReturn(SECOND_ROLE_ID);
when(experienceRole1.getRole()).thenReturn(aRole1);
when(experienceRole2.getRole()).thenReturn(aRole2);
when(experienceRole2.getId()).thenReturn(SECOND_ROLE_ID);
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(afterRole);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
when(roleService.memberHasRole(firstMember, SECOND_ROLE_ID)).thenReturn(false);
when(roleService.removeRoleFromMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
when(roleService.addRoleToMemberAsync(firstMember, SECOND_ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<RoleCalculationResult> calculationFuture = testUnit.syncForSingleUser(userExperience);
RoleCalculationResult result = calculationFuture.join();
Assert.assertEquals(SECOND_ROLE_ID, result.getExperienceRoleId());
}
@Test
public void testDisablingExperienceForUser() {
AUserExperience experience = Mockito.mock(AUserExperience.class);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperienceManagementService.findUserInServer(aUserInAServer)).thenReturn(experience);
testUnit.disableExperienceForUser(aUserInAServer);
verify(experience, times(1)).setExperienceGainDisabled(true);
}
@Test
public void testEnablingExpForUser() {
AUserExperience experience = Mockito.mock(AUserExperience.class);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperienceManagementService.findUserInServer(aUserInAServer)).thenReturn(experience);
testUnit.enableExperienceForUser(aUserInAServer);
verify(experience, times(1)).setExperienceGainDisabled(false);
}
@Test
public void testFindLeaderBoardData() {
executeLeaderBoardTest(server, 1);
}
@Test
public void testFindLeaderBoardDataSecondPage() {
executeLeaderBoardTest(server, 2);
}
@Test(expected = IllegalArgumentException.class)
public void testIllegalLeaderBoardPage() {
testUnit.findLeaderBoardData(server, -1);
}
@Test
public void testSyncAllUsers() {
AExperienceRole beforeRole = experienceRole1;
when(userExperience.getCurrentExperienceRole()).thenReturn(beforeRole);
AExperienceRole afterRole = experienceRole2;
when(aUserInAServer.getUserReference()).thenReturn(user);
when(user.getId()).thenReturn(8L);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(userExperience.getCurrentLevel()).thenReturn(level0);
when(aUserInAServer2.getUserReference()).thenReturn(user2);
when(user2.getId()).thenReturn(9L);
when(aUserInAServer2.getServerReference()).thenReturn(server);
when(userExperience2.getUser()).thenReturn(aUserInAServer2);
when(userExperience2.getCurrentLevel()).thenReturn(level0);
when(userExperience2.getCurrentExperienceRole()).thenReturn(beforeRole);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(memberService.getMemberInServerAsync(aUserInAServer2)).thenReturn(CompletableFuture.completedFuture(secondMember));
when(aRole1.getId()).thenReturn(ROLE_ID);
when(aRole2.getId()).thenReturn(SECOND_ROLE_ID);
when(experienceRole1.getRole()).thenReturn(aRole1);
when(experienceRole2.getRole()).thenReturn(aRole2);
when(experienceRole2.getId()).thenReturn(SECOND_ROLE_ID);
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(experienceRoleService.calculateRole(experienceRoles, userExperience.getLevelOrDefault())).thenReturn(afterRole);
when(experienceRoleService.calculateRole(experienceRoles, userExperience2.getLevelOrDefault())).thenReturn(afterRole);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(memberService.getMemberInServerAsync(aUserInAServer2)).thenReturn(CompletableFuture.completedFuture(secondMember));
when(roleService.memberHasRole(firstMember, SECOND_ROLE_ID)).thenReturn(false);
when(roleService.memberHasRole(firstMember, ROLE_ID)).thenReturn(true);
when(roleService.memberHasRole(secondMember, SECOND_ROLE_ID)).thenReturn(true);
when(roleService.removeRoleFromMemberAsync(firstMember, ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
when(roleService.addRoleToMemberAsync(firstMember,SECOND_ROLE_ID)).thenReturn(CompletableFuture.completedFuture(null));
List<AUserExperience> experiences = Arrays.asList(userExperience, userExperience2);
when(userExperienceManagementService.loadAllUsers(server)).thenReturn(experiences);
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
List<CompletableFuture<RoleCalculationResult>> calculationFutures = testUnit.syncUserRoles(server);
verify(roleService, times(0)).removeRoleFromMemberAsync(secondMember, ROLE_ID);
verify(roleService, times(0)).addRoleToMemberAsync(secondMember, SECOND_ROLE_ID);
RoleCalculationResult firstResult = calculationFutures.get(0).join();
Assert.assertEquals(SECOND_ROLE_ID, firstResult.getExperienceRoleId());
RoleCalculationResult secondResult = calculationFutures.get(1).join();
Assert.assertEquals(SECOND_ROLE_ID, secondResult.getExperienceRoleId());
}
@Test
public void testGetRankForUser() {
int rank = 1;
AUserExperience experienceObj = Mockito.mock(AUserExperience.class);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(experienceObj));
LeaderBoardEntryResult leaderBoardEntryTest = Mockito.mock(LeaderBoardEntryResult.class);
when(leaderBoardEntryTest.getRank()).thenReturn(rank);
when(userExperienceManagementService.getRankOfUserInServer(experienceObj)).thenReturn(leaderBoardEntryTest);
LeaderBoardEntry rankOfUserInServer = testUnit.getRankOfUserInServer(aUserInAServer);
Assert.assertEquals(experienceObj, rankOfUserInServer.getExperience());
Assert.assertEquals(rank, rankOfUserInServer.getRank().intValue());
}
@Test(expected = NoExperienceTrackedException.class)
public void testGetRankForUserNoExperienceFound() {
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.empty());
testUnit.getRankOfUserInServer(aUserInAServer);
}
@Test
public void testGetRankWhenRankReturnsNull() {
AUserExperience experienceObj = Mockito.mock(AUserExperience.class);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(experienceObj));
when(userExperienceManagementService.getRankOfUserInServer(experienceObj)).thenReturn(null);
LeaderBoardEntry rankOfUserInServer = testUnit.getRankOfUserInServer(aUserInAServer);
Assert.assertEquals(experienceObj, rankOfUserInServer.getExperience());
Assert.assertEquals(0, rankOfUserInServer.getRank().intValue());
}
@Test
public void testSyncRolesWithFeedBack() {
AChannel channel = Mockito.mock(AChannel.class);
when(channel.getServer()).thenReturn(server);
when(server.getId()).thenReturn(SERVER_ID);
List<AUserExperience> experiences = getUserExperiences(25);
checkStatusMessages(server, channel, experiences, 13);
}
@Test
public void testSyncRolesWithNoUsers() {
AChannel channel = Mockito.mock(AChannel.class);
List<AUserExperience> experiences = new ArrayList<>();
when(channel.getServer()).thenReturn(server);
when(server.getId()).thenReturn(SERVER_ID);
checkStatusMessages(server, channel, experiences, 1);
}
private void checkStatusMessages(AServer server, AChannel channel, List<AUserExperience> experiences, int messageCount) {
when(userExperienceManagementService.loadAllUsers(server)).thenReturn(experiences);
MessageToSend statusMessage = Mockito.mock(MessageToSend.class);
when(templateService.renderEmbedTemplate(eq("user_sync_status_message"), any(UserSyncStatusModel.class), eq(SERVER_ID))).thenReturn(statusMessage);
long messageId = 5L;
Message statusMessageJDA = Mockito.mock(Message.class);
when(statusMessageJDA.getIdLong()).thenReturn(messageId);
when(messageService.createStatusMessage(statusMessage, channel)).thenReturn(CompletableFuture.completedFuture(statusMessageJDA));
when(channelManagementService.loadChannel(CHANNEL_ID)).thenReturn(channel);
testUnit.syncUserRolesWithFeedback(server, CHANNEL_ID);
verify(messageService, times(messageCount)).updateStatusMessage(channel, messageId, statusMessage);
}
private void setupUserInServer() {
when(userExperienceManagementService.findByUserInServerIdOptional(USER_IN_SERVER_ID)).thenReturn(Optional.of(userExperience));
when(userInServerManagementService.loadOrCreateUser(firstMember)).thenReturn(aUserInAServer);
when(aUserInAServer.getUserReference()).thenReturn(user);
when(aUserInAServer.getUserInServerId()).thenReturn(USER_IN_SERVER_ID);
when(memberService.getMemberInServerAsync(aUserInAServer)).thenReturn(CompletableFuture.completedFuture(firstMember));
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(user.getId()).thenReturn(USER_ID);
}
private void setupTwoExperienceRoles() {
when(experienceRole1.getRole()).thenReturn(aRole1);
setExperienceRoleLevels();
}
private void setupServerId() {
when(server.getId()).thenReturn(SERVER_ID);
when(serverManagementService.loadOrCreate(SERVER_ID)).thenReturn(server);
}
private void setupServerConfig() {
when(configService.getLongValue(ExperienceFeatureConfig.MIN_EXP_KEY, SERVER_ID, DEFAULT_MIN_EXP)).thenReturn(20L);
when(configService.getLongValue(ExperienceFeatureConfig.MAX_EXP_KEY, SERVER_ID, DEFAULT_MAX_EXP)).thenReturn(50L);
when(configService.getDoubleValue(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, SERVER_ID, DEFAULT_EXP_MULTIPLIER)).thenReturn(1.2);
}
private void executeLeaderBoardTest(AServer server, Integer page) {
int pageSize = 10;
List<AUserExperience> experiences = Arrays.asList(userExperience, userExperience2);
when(userExperience.getExperience()).thenReturn(LOW_EXP);
when(userExperience.getCurrentLevel()).thenReturn(level0);
when(userExperience.getUser()).thenReturn(aUserInAServer);
when(userExperience2.getExperience()).thenReturn(MID_EXP);
when(userExperience2.getCurrentLevel()).thenReturn(level1);
when(userExperience2.getUser()).thenReturn(aUserInAServer2);
when(userExperienceManagementService.findLeaderBoardUsersPaginated(server, page - 1, pageSize)).thenReturn(experiences);
LeaderBoard leaderBoardData = testUnit.findLeaderBoardData(server, page);
page--;
List<LeaderBoardEntry> entries = leaderBoardData.getEntries();
LeaderBoardEntry firstEntry = entries.get(0);
Assert.assertEquals(LOW_EXP, firstEntry.getExperience().getExperience());
Assert.assertEquals(level0, firstEntry.getExperience().getCurrentLevel());
Assert.assertEquals(aUserInAServer, firstEntry.getExperience().getUser());
Assert.assertEquals((page * pageSize) + 1, firstEntry.getRank().intValue());
LeaderBoardEntry secondEntry = entries.get(1);
Assert.assertEquals(MID_EXP, secondEntry.getExperience().getExperience());
Assert.assertEquals(level1, secondEntry.getExperience().getCurrentLevel());
Assert.assertEquals(aUserInAServer2, secondEntry.getExperience().getUser());
Assert.assertEquals((page * pageSize) + 2, secondEntry.getRank().intValue());
Assert.assertEquals(2, entries.size());
}
private void setExperienceRoleLevels() {
when(experienceRole1.getLevel()).thenReturn(level0);
when(experienceRole2.getLevel()).thenReturn(level1);
}
private void setupLevelsAndRolesAndNoDisallowed() {
when(experienceLevelManagementService.getLevelConfig()).thenReturn(levels);
when(experienceRoleManagementService.getExperienceRolesForServer(server)).thenReturn(experienceRoles);
when(disabledExpRoleManagementService.getDisabledRolesForServer(server)).thenReturn(new ArrayList<>());
}
private void setupDefaultConfig() {
SystemConfigProperty minExpProperty = Mockito.mock(SystemConfigProperty.class);
when(minExpProperty.getLongValue()).thenReturn(DEFAULT_MIN_EXP);
when(defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.MIN_EXP_KEY)).thenReturn(minExpProperty);
SystemConfigProperty maxExpProperty = Mockito.mock(SystemConfigProperty.class);
when(maxExpProperty.getLongValue()).thenReturn(DEFAULT_MAX_EXP);
when(defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.MAX_EXP_KEY)).thenReturn(maxExpProperty);
SystemConfigProperty expMultiplierProperty = Mockito.mock(SystemConfigProperty.class);
when(expMultiplierProperty.getDoubleValue()).thenReturn(DEFAULT_EXP_MULTIPLIER);
when(defaultConfigManagementService.getDefaultConfig(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY)).thenReturn(expMultiplierProperty);
}
protected List<AUserExperience> getUserExperiences(int count) {
List<AUserExperience> experiences = new ArrayList<>();
for (int i = 0; i < count; i++) {
AUserExperience experience = Mockito.mock(AUserExperience.class);
when(experience.getUser()).thenReturn(aUserInAServer);
when(aUserInAServer.getServerReference()).thenReturn(server);
when(aUserInAServer.getUserReference()).thenReturn(user);
experiences.add(experience);
}
return experiences;
}
private void setupLevels(int count) {
if(count >= 0) {
when(level0.getExperienceNeeded()).thenReturn(LVL_0_EXP);
}
if(count >= 1) {
when(level1.getExperienceNeeded()).thenReturn(LVL_1_EXP);
}
if(count >= 2) {
when(level2.getExperienceNeeded()).thenReturn(LVL_2_EXP);
}
if(count >= 3) {
when(level3.getExperienceNeeded()).thenReturn(LVL_3_EXP);
}
}
}

View File

@@ -53,21 +53,6 @@ public class ExperienceRoleServiceBeanTest {
private static final Long CHANNEL_ID = 4L;
private static final Long ROLE_ID = 5L;
@Test
public void testUnsetRoleInDb() {
Integer levelCount = 10;
AExperienceLevel level = Mockito.mock(AExperienceLevel.class);
ARole roleToChange = Mockito.mock(ARole.class);
when(roleToChange.getServer()).thenReturn(server);
when(experienceLevelService.getLevelOptional(levelCount)).thenReturn(Optional.of(level));
when(roleManagementService.findRole(roleToChange.getId())).thenReturn(roleToChange);
testingUnit.unsetRoleInDb(levelCount, roleToChange.getId());
verify(experienceRoleManagementService, times(1)).removeAllRoleAssignmentsForLevelInServerExceptRole(level, server, roleToChange);
verify(experienceRoleManagementService, times(1)).setLevelToRole(level, roleToChange);
verify(experienceRoleManagementService, times(0)).getExperienceRolesForServer(server);
}
@Test
public void testCalculateRoleForLevelInBetween() {
List<AExperienceRole> roles = getExperienceRoles();

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>dev.sheldan.abstracto.modules</groupId>
<artifactId>experience-tracking</artifactId>
<version>1.4.7</version>
<version>1.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -0,0 +1,19 @@
package dev.sheldan.abstracto.experience.config;
import dev.sheldan.abstracto.core.service.ExecutorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
@Configuration
public class ExperienceExecutorConfig {
@Autowired
private ExecutorService executorService;
@Bean(name = "experienceUpdateExecutor")
public TaskExecutor experienceUpdateExecutor() {
return executorService.setupExecutorFor("experienceUpdateExecutor");
}
}

View File

@@ -2,11 +2,14 @@ package dev.sheldan.abstracto.experience.config;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.FeatureMode;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import static dev.sheldan.abstracto.experience.config.ExperienceFeatureMode.LEVEL_UP_NOTIFICATION;
/**
* {@link FeatureConfig} instance containing the required configuration concerning system config and post targets for
* the {@link ExperienceFeatureDefinition} feature.
@@ -26,6 +29,7 @@ public class ExperienceFeatureConfig implements FeatureConfig {
* The multiplier which is applied to each calculated gained experience
*/
public static final String EXP_MULTIPLIER_KEY = "expMultiplier";
public static final String EXP_COOLDOWN_SECONDS_KEY = "expCooldownSeconds";
@Override
public FeatureDefinition getFeature() {
@@ -37,6 +41,11 @@ public class ExperienceFeatureConfig implements FeatureConfig {
*/
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(EXP_MULTIPLIER_KEY, MIN_EXP_KEY, MAX_EXP_KEY);
return Arrays.asList(EXP_MULTIPLIER_KEY, MIN_EXP_KEY, MAX_EXP_KEY, EXP_COOLDOWN_SECONDS_KEY);
}
@Override
public List<FeatureMode> getAvailableModes() {
return Arrays.asList(LEVEL_UP_NOTIFICATION);
}
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.experience.config;
import dev.sheldan.abstracto.core.config.FeatureMode;
import lombok.Getter;
@Getter
public enum ExperienceFeatureMode implements FeatureMode {
LEVEL_UP_NOTIFICATION("levelUpNotification");
private final String key;
ExperienceFeatureMode(String key) {
this.key = key;
}
}

View File

@@ -15,10 +15,6 @@ import java.util.concurrent.CompletableFuture;
@Setter
@Builder
public class ExperienceGainResult {
/**
* The calculation result contained in a {@link CompletableFuture future}. The future is necessary, because the calculation both calculates the new role
* and removes/adds {@link net.dv8tion.jda.api.entities.Role role} to the {@link net.dv8tion.jda.api.entities.Member member}
*/
private CompletableFuture<RoleCalculationResult> calculationResult;
/**
* The ID of the {@link dev.sheldan.abstracto.core.models.database.AUserInAServer user} for which this is the result

View File

@@ -4,20 +4,10 @@ import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* The result of calculating the appropriate {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole role} for a {@link dev.sheldan.abstracto.experience.model.database.AUserExperience user}
* in a server.
*/
@Getter
@Setter
@Builder
public class RoleCalculationResult {
/**
* The ID of the {@link dev.sheldan.abstracto.experience.model.database.AExperienceRole role} which was given to the user. Can be null, in case no role is given.
*/
private Long experienceRoleId;
/**
* The ID of a {@link dev.sheldan.abstracto.core.models.database.AUserInAServer user} for who the role was calculated for.
*/
private Long userInServerId;
private Long oldRoleId;
private Long newRoleId;
}

View File

@@ -1,27 +0,0 @@
package dev.sheldan.abstracto.experience.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/**
* Container object to store the experience in runtime and group it together. This basically is just a list of users who were tracked by experience.
* The actual calculation of the appropriate experience amount is done later.
*/
@Getter
@Setter
@Builder
public class ServerExperience {
/**
* The ID of the {@link dev.sheldan.abstracto.core.models.database.AServer} for which this experience were collected
*/
private Long serverId;
/**
* A list of IDs of the {@link dev.sheldan.abstracto.core.models.database.AUserInAServer} which should be given experience
*/
@Builder.Default
private List<Long> userInServerIds = new ArrayList<>();
}

View File

@@ -3,7 +3,7 @@ package dev.sheldan.abstracto.experience.model.database;
import dev.sheldan.abstracto.core.models.database.ARole;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;

View File

@@ -2,7 +2,7 @@ package dev.sheldan.abstracto.experience.model.database;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;

View File

@@ -4,7 +4,7 @@ import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;

View File

@@ -4,7 +4,7 @@ import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;
@@ -60,6 +60,9 @@ public class AUserExperience implements Serializable {
@Column(name = "experience_gain_disabled", nullable = false)
private Boolean experienceGainDisabled = false;
@Column(name = "level_up_notification")
private Boolean levelUpNotification;
/**
* The {@link AExperienceLevel level} which the user currently has.
*/

View File

@@ -0,0 +1,20 @@
package dev.sheldan.abstracto.experience.model.template;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class LevelUpNotificationModel {
private MemberDisplay memberDisplay;
private Integer oldLevel;
private Integer newLevel;
private RoleDisplay oldRole;
private RoleDisplay newRole;
private Long oldExperience;
private Long newExperience;
}

Some files were not shown because too many files have changed in this diff Show More