[AB-53] adding economy related commands

adding runtime exception to root command handling
adding simple custom command module
disabling slash command for show emote the utility is not given (the bot cannot show any emotes from servers its not in)
fixing post target setup not allowing threads
fixing post targets not supporting threads
fixing interactions not considering the actual amount of embeds
fixing some cases for events which are not in a guild
This commit is contained in:
Sheldan
2022-07-21 00:43:04 +02:00
parent 68cae74819
commit 9954515db0
81 changed files with 3717 additions and 108 deletions

View File

@@ -0,0 +1,102 @@
package dev.sheldan.abstracto.entertainment.command.economy;
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.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.ChannelService;
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.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.dto.CreditGambleResult;
import dev.sheldan.abstracto.entertainment.model.command.CreditGambleResultModel;
import dev.sheldan.abstracto.entertainment.service.EconomyService;
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.List;
import java.util.concurrent.CompletableFuture;
@Component
public class CreditGamble extends AbstractConditionableCommand {
private static final String CREDIT_GAMBLE_COMMAND = "creditGamble";
private static final String CREDIT_GAMBLE_RESPONSE = "creditGamble_response";
@Autowired
private EconomyService economyService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private InteractionService interactionService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(commandContext.getAuthor());
CreditGambleResult result = economyService.triggerCreditGamble(aUserInAServer);
CreditGambleResultModel model = CreditGambleResultModel.fromCreditGambleResult(result);
MessageToSend messageToSend = templateService.renderEmbedTemplate(CREDIT_GAMBLE_RESPONSE, model, commandContext.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(event.getMember());
CreditGambleResult result = economyService.triggerCreditGamble(aUserInAServer);
CreditGambleResultModel model = CreditGambleResultModel.fromCreditGambleResult(result);
return interactionService.replyEmbed(CREDIT_GAMBLE_RESPONSE, model, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(EntertainmentSlashCommandNames.ECONOMY)
.commandName("creditgamble")
.build();
return CommandConfiguration.builder()
.name(CREDIT_GAMBLE_COMMAND)
.slashCommandConfig(slashCommandConfig)
.async(true)
.module(EntertainmentModuleDefinition.ENTERTAINMENT)
.templated(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ECONOMY;
}
}

View File

@@ -0,0 +1,179 @@
package dev.sheldan.abstracto.entertainment.command.economy;
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.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.MemberService;
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.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentSlashCommandNames;
import dev.sheldan.abstracto.entertainment.model.command.CreditsLeaderboardEntry;
import dev.sheldan.abstracto.entertainment.model.command.CreditsLeaderboardResponseModel;
import dev.sheldan.abstracto.entertainment.service.EconomyService;
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.*;
import java.util.concurrent.CompletableFuture;
@Component
public class CreditLeaderboard extends AbstractConditionableCommand {
private static final String CREDIT_LEADERBOARD_COMMAND_NAME = "creditLeaderboard";
private static final String CREDIT_LEADERBOARD_RESPONSE = "creditLeaderboard_response";
private static final String PAGE_PARAMETER = "page";
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private EconomyService economyService;
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private InteractionService interactionService;
@Autowired
private ChannelService channelService;
@Autowired
private TemplateService templateService;
@Autowired
private MemberService memberService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
AServer server = serverManagementService.loadServer(commandContext.getGuild());
List<Object> parameters = commandContext.getParameters().getParameters();
// parameter is optional, in case its not present, we default to the 0th page
Integer page = !parameters.isEmpty() ? (Integer) parameters.get(0) : 1;
List<CreditsLeaderboardEntry> creditLeaderboard = economyService.getCreditLeaderboard(server, page);
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(commandContext.getAuthor());
CreditsLeaderboardEntry ownRank = economyService.getRankOfUser(aUserInAServer);
CreditsLeaderboardResponseModel model = CreditsLeaderboardResponseModel
.builder()
.entries(creditLeaderboard)
.ownRank(ownRank)
.build();
return enrichModelWithMembers(model, commandContext.getGuild().getIdLong())
.thenCompose(model1 -> {
MessageToSend message = templateService.renderEmbedTemplate(CREDIT_LEADERBOARD_RESPONSE, model1, commandContext.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(message, commandContext.getChannel()));
})
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
event.deferReply().queue();
Integer page;
if(slashCommandParameterService.hasCommandOption(PAGE_PARAMETER, event)) {
page = slashCommandParameterService.getCommandOption(PAGE_PARAMETER, event, Integer.class);
} else {
page = 1;
}
AServer server = serverManagementService.loadServer(event.getGuild());
List<CreditsLeaderboardEntry> creditLeaderboard = economyService.getCreditLeaderboard(server, page);
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(event.getMember());
CreditsLeaderboardEntry ownRank = economyService.getRankOfUser(aUserInAServer);
CreditsLeaderboardResponseModel model = CreditsLeaderboardResponseModel
.builder()
.entries(creditLeaderboard)
.ownRank(ownRank)
.build();
return enrichModelWithMembers(model, event.getGuild().getIdLong())
.thenCompose(model1 -> FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(CREDIT_LEADERBOARD_RESPONSE, model1, event.getHook())))
.thenApply(unused -> CommandResult.fromSuccess());
}
private CompletableFuture<CreditsLeaderboardResponseModel> enrichModelWithMembers(CreditsLeaderboardResponseModel model, Long serverId) {
List<CompletableFuture<Member>> memberFutures = new ArrayList<>();
model.getEntries().forEach(creditsLeaderboardEntry -> {
memberFutures.add(memberService.getMemberInServerAsync(serverId, creditsLeaderboardEntry.getMemberDisplay().getUserId()));
});
memberFutures.add(memberService.getMemberInServerAsync(serverId, model.getOwnRank().getMemberDisplay().getUserId()));
CompletableFuture<CreditsLeaderboardResponseModel> modelFuture = new CompletableFuture<>();
CompletableFutureList<Member> futureList = new CompletableFutureList<>(memberFutures);
futureList.getMainFuture().whenComplete((unused, throwable) -> {
Map<Long, Member> memberMap = new HashMap<>();
futureList.getObjects().forEach(member -> memberMap.put(member.getIdLong(), member));
model.getEntries().forEach(creditsLeaderboardEntry -> {
if(memberMap.containsKey(creditsLeaderboardEntry.getMemberDisplay().getUserId())) {
creditsLeaderboardEntry.setMember(memberMap.get(creditsLeaderboardEntry.getMemberDisplay().getUserId()));
}
});
if(memberMap.containsKey(model.getOwnRank().getMemberDisplay().getUserId())) {
model.getOwnRank().setMember(memberMap.get(model.getOwnRank().getMemberDisplay().getUserId()));
}
modelFuture.complete(model);
});
return modelFuture;
}
@Override
public CommandConfiguration getConfiguration() {
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
Parameter pageParameter = Parameter
.builder()
.name(PAGE_PARAMETER)
.optional(true)
.templated(true)
.type(Integer.class)
.build();
List<Parameter> parameters = Arrays.asList(pageParameter);
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(EntertainmentSlashCommandNames.ECONOMY)
.commandName("creditboard")
.build();
return CommandConfiguration.builder()
.name(CREDIT_LEADERBOARD_COMMAND_NAME)
.module(EntertainmentModuleDefinition.ENTERTAINMENT)
.templated(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.supportsEmbedException(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ECONOMY;
}
}

View File

@@ -0,0 +1,111 @@
package dev.sheldan.abstracto.entertainment.command.economy;
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.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.ChannelService;
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.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.CreditsLeaderboardEntry;
import dev.sheldan.abstracto.entertainment.model.command.CreditsModel;
import dev.sheldan.abstracto.entertainment.service.EconomyService;
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.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Credits extends AbstractConditionableCommand {
private static final String CREDITS_COMMAND = "credits";
private static final String CREDITS_RESPONSE = "credits_response";
@Autowired
private EconomyService economyService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private InteractionService interactionService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
AUserInAServer targetUser = userInServerManagementService.loadOrCreateUser(commandContext.getAuthor());
CreditsLeaderboardEntry rankEntry = economyService.getRankOfUser(targetUser);
rankEntry.setMember(commandContext.getAuthor());
CreditsModel model = CreditsModel
.builder()
.entry(rankEntry)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(CREDITS_RESPONSE, model, commandContext.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
AUserInAServer targetUser = userInServerManagementService.loadOrCreateUser(event.getMember());
CreditsLeaderboardEntry rankEntry = economyService.getRankOfUser(targetUser);
rankEntry.setMember(event.getMember());
CreditsModel model = CreditsModel
.builder()
.entry(rankEntry)
.build();
return interactionService.replyEmbed(CREDITS_RESPONSE, model, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(EntertainmentSlashCommandNames.ECONOMY)
.commandName("credits")
.build();
return CommandConfiguration.builder()
.name(CREDITS_COMMAND)
.slashCommandConfig(slashCommandConfig)
.async(true)
.module(EntertainmentModuleDefinition.ENTERTAINMENT)
.templated(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ECONOMY;
}
}

View File

@@ -0,0 +1,124 @@
package dev.sheldan.abstracto.entertainment.command.economy;
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.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.ChannelService;
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.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.dto.PayDayResult;
import dev.sheldan.abstracto.entertainment.model.command.PayDayResponseModel;
import dev.sheldan.abstracto.entertainment.model.database.EconomyLeaderboardResult;
import dev.sheldan.abstracto.entertainment.service.EconomyService;
import dev.sheldan.abstracto.entertainment.service.management.EconomyUserManagementService;
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.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Payday extends AbstractConditionableCommand {
private static final String PAYDAY_COMMAND_NAME = "payday";
@Autowired
private EconomyService economyService;
@Autowired
private EconomyUserManagementService economyUserManagementService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private InteractionService interactionService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
private static final String PAYDAY_RESPONSE = "payday_response";
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Member member = commandContext.getAuthor();
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
PayDayResult payDayResult = economyService.triggerPayDay(aUserInAServer);
EconomyLeaderboardResult rank = economyUserManagementService.getRankOfUserInServer(aUserInAServer);
PayDayResponseModel responseModel = PayDayResponseModel
.builder()
.currentCredits(payDayResult.getCurrentCredits())
.gainedCredits(payDayResult.getGainedCredits())
.leaderboardPosition(rank.getRank().longValue())
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(PAYDAY_RESPONSE, responseModel, member.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Member member = event.getMember();
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
PayDayResult payDayResult = economyService.triggerPayDay(aUserInAServer);
EconomyLeaderboardResult rank = economyUserManagementService.getRankOfUserInServer(aUserInAServer);
PayDayResponseModel responseModel = PayDayResponseModel
.builder()
.currentCredits(payDayResult.getCurrentCredits())
.gainedCredits(payDayResult.getGainedCredits())
.leaderboardPosition(rank.getRank().longValue())
.build();
return interactionService.replyEmbed(PAYDAY_RESPONSE, responseModel, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(EntertainmentSlashCommandNames.ECONOMY)
.commandName("payday")
.build();
return CommandConfiguration.builder()
.name(PAYDAY_COMMAND_NAME)
.slashCommandConfig(slashCommandConfig)
.async(true)
.module(EntertainmentModuleDefinition.ENTERTAINMENT)
.templated(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ECONOMY;
}
}

View File

@@ -0,0 +1,124 @@
package dev.sheldan.abstracto.entertainment.command.economy;
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.config.validator.MinIntegerValueValidator;
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.ChannelService;
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.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.dto.SlotsResult;
import dev.sheldan.abstracto.entertainment.model.command.SlotsResponseModel;
import dev.sheldan.abstracto.entertainment.service.EconomyServiceBean;
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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Component
public class Slots extends AbstractConditionableCommand {
private static final String SLOTS_COMMAND_NAME = "slots";
private static final String BID_PARAMETER = "bid";
private static final String SLOTS_RESPONSE = "slots_response";
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private InteractionService interactionService;
@Autowired
private EconomyServiceBean economyUserServiceBean;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
Integer bid = (Integer) commandContext.getParameters().getParameters().get(0);
Member member = commandContext.getAuthor();
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
SlotsResult slotsResult = economyUserServiceBean.triggerSlots(aUserInAServer, bid.longValue());
SlotsResponseModel responseModel = SlotsResponseModel.fromSlotsResult(slotsResult);
MessageToSend messageToSend = templateService.renderEmbedTemplate(SLOTS_RESPONSE, responseModel, member.getGuild().getIdLong());
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Long bid = slashCommandParameterService.getCommandOption(BID_PARAMETER, event, Integer.class).longValue();
Member member = event.getMember();
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(member);
SlotsResult slotsResult = economyUserServiceBean.triggerSlots(aUserInAServer, bid);
SlotsResponseModel responseModel = SlotsResponseModel.fromSlotsResult(slotsResult);
return interactionService.replyEmbed(SLOTS_RESPONSE, responseModel, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
Parameter lowParameter = Parameter
.builder()
.name(BID_PARAMETER)
.type(Integer.class)
.templated(true)
.validators(Arrays.asList(MinIntegerValueValidator.min(0L)))
.build();
parameters.add(lowParameter);
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(EntertainmentSlashCommandNames.ECONOMY)
.commandName("slots")
.build();
return CommandConfiguration.builder()
.name(SLOTS_COMMAND_NAME)
.slashCommandConfig(slashCommandConfig)
.async(true)
.module(EntertainmentModuleDefinition.ENTERTAINMENT)
.templated(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ECONOMY;
}
}

View File

@@ -0,0 +1,117 @@
package dev.sheldan.abstracto.entertainment.command.economy;
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.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentSlashCommandNames;
import dev.sheldan.abstracto.entertainment.service.EconomyService;
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 TransferCredits extends AbstractConditionableCommand {
private static final String TRANSFER_CREDITS_COMMAND = "transferCredits";
private static final String TRANSFER_CREDITS_RESPONSE = "transferCredits_response";
private static final String MEMBER_PARAMETER = "targetMember";
private static final String AMOUNT_PARAMETER = "amount";
@Autowired
private InteractionService interactionService;
@Autowired
private SlashCommandParameterService slashCommandParameterService;
@Autowired
private UserInServerManagementService userInServerManagementService;
@Autowired
private EconomyService economyService;
@Override
public CommandResult execute(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();
}
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Member targetMember = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER, event, Member.class);
Integer amount = slashCommandParameterService.getCommandOption(AMOUNT_PARAMETER, event, Integer.class);
AUserInAServer targetUser = userInServerManagementService.loadOrCreateUser(targetMember);
AUserInAServer sourceUser = userInServerManagementService.loadOrCreateUser(event.getMember());
economyService.transferCredits(sourceUser, targetUser, amount.longValue());
return interactionService.replyEmbed(TRANSFER_CREDITS_RESPONSE, event)
.thenApply(interactionHook -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
HelpInfo helpInfo = HelpInfo
.builder()
.templated(true)
.build();
Parameter memberParameter = Parameter
.builder()
.name(MEMBER_PARAMETER)
.templated(true)
.type(Member.class)
.optional(true)
.build();
Parameter amountParameter = Parameter
.builder()
.name(AMOUNT_PARAMETER)
.templated(true)
.type(Integer.class)
.optional(true)
.build();
List<Parameter> parameters = Arrays.asList(memberParameter, amountParameter);
SlashCommandConfig slashCommandConfig = SlashCommandConfig
.builder()
.enabled(true)
.rootCommandName(EntertainmentSlashCommandNames.ECONOMY)
.commandName("transfer")
.build();
return CommandConfiguration.builder()
.name(TRANSFER_CREDITS_COMMAND)
.slashCommandConfig(slashCommandConfig)
.module(EntertainmentModuleDefinition.ENTERTAINMENT)
.templated(true)
.supportsEmbedException(true)
.parameters(parameters)
.causesReaction(true)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ECONOMY;
}
}

View File

@@ -0,0 +1,41 @@
package dev.sheldan.abstracto.entertainment.repository;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.EconomyLeaderboardResult;
import dev.sheldan.abstracto.entertainment.model.database.EconomyUser;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface EconomyUserRepository extends JpaRepository<EconomyUser, Long> {
Optional<EconomyUser> findByUser(AUserInAServer aUserInAServer);
@Query(value = "WITH economy_user_ranked AS" +
"( " +
" SELECT eu.id, eu.credits, uis.user_id, ROW_NUMBER() OVER ( ORDER BY credits DESC ) " +
" FROM economy_user eu INNER JOIN user_in_server uis ON eu.id = uis.user_in_server_id INNER JOIN server s ON s.id = uis.server_id WHERE s.id = :serverId" +
") " +
"SELECT rank.id as \"id\", rank.user_id as \"userid\", rank.credits as \"credits\", rank.row_number as \"rank\" " +
"FROM economy_user_ranked rank " +
"WHERE rank.id = :userInServerId", nativeQuery = true)
EconomyLeaderboardResult getRankOfUserInServer(@Param("userInServerId") Long id, @Param("serverId") Long serverId);
@Query(value = "WITH economy_user_ranked AS" +
"( " +
" SELECT eu.id, eu.credits, uis.user_id, ROW_NUMBER() OVER ( ORDER BY credits DESC ) " +
" FROM economy_user eu INNER JOIN user_in_server uis ON eu.id = uis.user_in_server_id INNER JOIN server s ON s.id = uis.server_id WHERE s.id = :serverId" +
") " +
"SELECT rank.id as \"id\", rank.user_id as \"userid\", rank.credits as \"credits\", rank.row_number as \"rank\" " +
"FROM economy_user_ranked rank ", nativeQuery = true)
List<EconomyLeaderboardResult> getRanksInServer(@Param("serverId") Long serverId);
List<EconomyUser> findTop10ByServerOrderByCreditsDesc(AServer server, Pageable pageable);
List<EconomyUser> findByServerOrderByCredits(AServer server);
}

View File

@@ -0,0 +1,354 @@
package dev.sheldan.abstracto.entertainment.service;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.entertainment.config.EconomyFeatureConfig;
import dev.sheldan.abstracto.entertainment.dto.CreditGambleResult;
import dev.sheldan.abstracto.entertainment.dto.PayDayResult;
import dev.sheldan.abstracto.entertainment.dto.SlotsResult;
import dev.sheldan.abstracto.entertainment.exception.NotEnoughCreditsException;
import dev.sheldan.abstracto.entertainment.exception.PayDayCooldownException;
import dev.sheldan.abstracto.entertainment.exception.SlotsCooldownException;
import dev.sheldan.abstracto.entertainment.model.command.CreditsLeaderboardEntry;
import dev.sheldan.abstracto.entertainment.model.database.EconomyLeaderboardResult;
import dev.sheldan.abstracto.entertainment.model.database.EconomyUser;
import dev.sheldan.abstracto.entertainment.service.management.EconomyUserManagementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import static dev.sheldan.abstracto.entertainment.config.EconomyFeatureConfig.PAYDAY_COOLDOWN_CONFIG_KEY;
import static dev.sheldan.abstracto.entertainment.config.EconomyFeatureConfig.SLOTS_COOLDOWN_CONFIG_KEY;
@Component
public class EconomyServiceBean implements EconomyService {
@Autowired
private ConfigService configService;
@Autowired
private EconomyUserManagementService economyUserManagementService;
@Autowired
private SecureRandom secureRandom;
private static final String CREDIT_GAMBLE_STORAGE = "creditGambleJackpot";
private static final String SNOWFLAKE = "";
private static final String CHERRY = "🍒";
private static final String COOKIE = "🍪";
private static final String TWO = "2";
private static final String CLOVER = "🍀";
private static final String MUSHROOM = "🍄";
private static final String SUNFLOWER = "🌻";
private static final String HEART = "";
private static final String SIX = "6";
private static final String CYCLONE = "🌀";
private static final String OUTCOME_KEY_THREE_CHERRIES = "threecherries";
private static final String OUTCOME_KEY_NOTHING = "nothing";
private static final String OUTCOME_KEY_JACKPOT = "jackpot";
private static final String OUTCOME_KEY_CLOVERS = "clovers";
private static final String OUTCOME_KEY_TWO_CHERRIES = "twocherries";
private static final String OUTCOME_KEY_TWOSIX = "twosix";
private static final String OUTCOME_KEY_3SYMBOLS = "threesymbols";
private static final String OUTCOME_KEY_2SYMBOLS = "twosymbols";
private static final List<String> POSSIBLE_SLOTS = Arrays.asList(SNOWFLAKE, CHERRY, COOKIE, TWO, CLOVER, MUSHROOM, SUNFLOWER, HEART, SIX, CYCLONE);
private static final List<SlotMapping> WINNING_PATTERNS = Arrays.asList(
new SlotMapping(Arrays.asList(CHERRY, CHERRY, CHERRY), 20, OUTCOME_KEY_THREE_CHERRIES),
new SlotMapping(Arrays.asList(CLOVER, CLOVER, CLOVER), 25, OUTCOME_KEY_CLOVERS),
new SlotMapping(Arrays.asList(TWO, TWO, SIX), 50, OUTCOME_KEY_JACKPOT),
new SlotMapping(Arrays.asList(TWO, SIX), 4, OUTCOME_KEY_TWOSIX),
new SlotMapping(Arrays.asList(CHERRY, CHERRY), 3, OUTCOME_KEY_TWO_CHERRIES)
);
private static final Integer TRIPLE_FACTOR = 10;
private static final Integer DOUBLE_FACTOR = 2;
@Override
public boolean canTriggerPayDay(AUserInAServer aUserInAServer) {
Optional<EconomyUser> userOptional = economyUserManagementService.getUser(aUserInAServer);
return userOptional.map(this::canTriggerPayDay).orElse(true);
}
@Override
public boolean canTriggerSlots(AUserInAServer aUserInAServer) {
Optional<EconomyUser> userOptional = economyUserManagementService.getUser(aUserInAServer);
return userOptional.map(this::canTriggerSlots).orElse(true);
}
@Override
public boolean canTriggerPayDay(EconomyUser economyUser) {
return slotsTriggerIn(economyUser).isNegative();
}
@Override
public boolean canTriggerSlots(EconomyUser economyUser) {
return payDayTriggerIn(economyUser).isNegative();
}
@Override
public Duration payDayTriggerIn(EconomyUser economyUser) {
Long cooldownSeconds = configService.getLongValueOrConfigDefault(PAYDAY_COOLDOWN_CONFIG_KEY, economyUser.getServer().getId());
Instant minTimeStamp = Instant.now().minus(cooldownSeconds, ChronoUnit.SECONDS);
return Duration.between(economyUser.getLastPayDay(), minTimeStamp);
}
@Override
public Duration slotsTriggerIn(EconomyUser economyUser) {
Long cooldownSeconds = configService.getLongValueOrConfigDefault(SLOTS_COOLDOWN_CONFIG_KEY, economyUser.getServer().getId());
Instant minTimeStamp = Instant.now().minus(cooldownSeconds, ChronoUnit.SECONDS);
return Duration.between(economyUser.getLastSlots(), minTimeStamp);
}
@Override
public EconomyUser addCredits(AUserInAServer aUserInAServer, Long credits) {
Optional<EconomyUser> existingUserOptional = economyUserManagementService.getUser(aUserInAServer);
if (existingUserOptional.isPresent()) {
EconomyUser existingUser = existingUserOptional.get();
addCredits(existingUser, credits);
return existingUser;
} else {
EconomyUser user = economyUserManagementService.createUser(aUserInAServer);
user.setCredits(credits);
return user;
}
}
@Override
public void addCredits(EconomyUser economyUser, Long credits) {
economyUser.setCredits(economyUser.getCredits() + credits);
}
@Override
public void addPayDayCredits(AUserInAServer aUserInAServer) {
Long creditsToAdd = configService.getLongValueOrConfigDefault(EconomyFeatureConfig.PAYDAY_CREDITS_CONFIG_KEY,
aUserInAServer.getServerReference().getId());
addCredits(aUserInAServer, creditsToAdd);
}
@Override
public PayDayResult triggerPayDay(AUserInAServer aUserInAServer) {
Long creditsToAdd = configService.getLongValueOrConfigDefault(EconomyFeatureConfig.PAYDAY_CREDITS_CONFIG_KEY,
aUserInAServer.getServerReference().getId());
EconomyUser economyUser = addCredits(aUserInAServer, creditsToAdd);
Duration durationForPayday = payDayTriggerIn(economyUser);
if (durationForPayday.isNegative()) {
throw new PayDayCooldownException(durationForPayday.abs());
}
economyUser.setLastPayDay(Instant.now());
return PayDayResult
.builder()
.currentCredits(economyUser.getCredits())
.gainedCredits(creditsToAdd)
.build();
}
@Override
public SlotsResult triggerSlots(AUserInAServer aUserInAServer, Long bid) {
Optional<EconomyUser> userOptional = economyUserManagementService.getUser(aUserInAServer);
if(!userOptional.isPresent()) {
throw new NotEnoughCreditsException();
}
EconomyUser user = userOptional.get();
Long oldCredits = user.getCredits();
if(user.getCredits() < bid) {
throw new NotEnoughCreditsException();
}
Duration durationForSlots = slotsTriggerIn(user);
if (durationForSlots.isNegative()) {
throw new SlotsCooldownException(durationForSlots.abs());
}
SlotGame slotGame = playSlots();
Integer factor = slotGame.getResultFactor();
Long creditChange = bid * factor;
addCredits(user, -bid);
addCredits(user, creditChange);
user.setLastSlots(Instant.now());
return SlotsResult
.builder()
.bid(bid)
.factor(factor.longValue())
.newCredits(user.getCredits())
.outComeKey(slotGame.getOutcome())
.oldCredits(oldCredits)
.winnings(creditChange)
.rows(slotGame.getRows())
.build();
}
@Override
public SlotGame playSlots() {
List<String> result = new ArrayList<>();
for (int i = 0; i < 3; i++) {
List<String> tempSlots = new ArrayList<>(POSSIBLE_SLOTS);
Collections.rotate(tempSlots, secureRandom.nextInt(2000) - 1000);
result.add(tempSlots.get(0));
result.add(tempSlots.get(1));
result.add(tempSlots.get(2));
}
List<List<String>> rows = new ArrayList<>();
rows.add(Arrays.asList(result.get(0), result.get(3), result.get(6)));
List<String> decidingRow = Arrays.asList(result.get(1), result.get(4), result.get(7));
rows.add(decidingRow);
rows.add(Arrays.asList(result.get(2), result.get(5), result.get(8)));
String decidingRowAsString = String.join("", decidingRow);
SlotMapping specialPattern = getSpecialPattern(decidingRowAsString);
Integer factor = 0;
String outcomeKey = OUTCOME_KEY_NOTHING;
if(specialPattern != null){
factor = specialPattern.factor;
outcomeKey = specialPattern.outcome;
} else {
Set<String> uniqueChars = new HashSet<>(decidingRow);
if(uniqueChars.size() == 1) {
factor = TRIPLE_FACTOR;
outcomeKey = OUTCOME_KEY_3SYMBOLS;
} else if(decidingRow.get(0).equals(decidingRow.get(1)) || decidingRow.get(1).equals(decidingRow.get(2)) ) {
factor = DOUBLE_FACTOR;
outcomeKey = OUTCOME_KEY_2SYMBOLS;
}
}
return SlotGame
.builder()
.rows(rows)
.outcome(outcomeKey)
.resultFactor(factor)
.build();
}
@Override
public List<CreditsLeaderboardEntry> getCreditLeaderboard(AServer server, Integer page) {
if(page <= 0) {
throw new IllegalArgumentException("Page needs to be >= 1");
}
page--;
int pageSize = 10;
List<CreditsLeaderboardEntry> entries = new ArrayList<>();
List<EconomyUser> ranks = economyUserManagementService.getRanksInServer(server, page, pageSize);
int pageOffset = page * pageSize;
for (int i = 0; i < ranks.size(); i++) {
EconomyUser rank = ranks.get(i);
CreditsLeaderboardEntry entry = CreditsLeaderboardEntry
.builder()
.credits(rank.getCredits())
.memberDisplay(MemberDisplay.fromAUserInAServer(rank.getUser()))
.rank(pageOffset + i + 1)
.build();
entries.add(entry);
}
return entries;
}
@Override
public CreditsLeaderboardEntry getRankOfUser(AUserInAServer aUserInAServer) {
EconomyLeaderboardResult rank = economyUserManagementService.getRankOfUserInServer(aUserInAServer);
if(rank != null) {
return CreditsLeaderboardEntry
.builder()
.credits(rank.getCredits())
.memberDisplay(MemberDisplay.fromAUserInAServer(aUserInAServer))
.rank(rank.getRank())
.build();
} else {
return CreditsLeaderboardEntry
.builder()
.credits(0L)
.memberDisplay(MemberDisplay.fromAUserInAServer(aUserInAServer))
.rank(-1)
.build();
}
}
@Override
public void transferCredits(AUserInAServer source, AUserInAServer target, Long amount) {
Optional<EconomyUser> userOptional = economyUserManagementService.getUser(source);
if(!userOptional.isPresent()) {
throw new NotEnoughCreditsException();
}
EconomyUser user = userOptional.get();
if(user.getCredits() < amount) {
throw new NotEnoughCreditsException();
}
addCredits(target, amount);
addCredits(user, -amount);
}
@Override
public CreditGambleResult triggerCreditGamble(AUserInAServer aUserInAServer) {
// TODO move these constants to system configs
Optional<EconomyUser> userOptional = economyUserManagementService.getUser(aUserInAServer);
if(!userOptional.isPresent()) {
throw new NotEnoughCreditsException();
}
EconomyUser user = userOptional.get();
Long bid = 25L;
if(user.getCredits() < bid) {
throw new NotEnoughCreditsException();
}
Long serverId = aUserInAServer.getServerReference().getId();
Long currentJackpot = configService.getLongValueOrConfigDefault(CREDIT_GAMBLE_STORAGE, serverId);
List<Integer> diceRoles = new ArrayList<>();
diceRoles.add(creditGambleDiceResult());
diceRoles.add(creditGambleDiceResult());
diceRoles.add(creditGambleDiceResult());
diceRoles.add(creditGambleDiceResult());
Long toJackpot = 20L;
Integer uniqueNumbers = new HashSet<>(diceRoles).size();
Boolean won = uniqueNumbers == 1;
CreditGambleResult result = CreditGambleResult
.builder()
.uniqueNumbers(uniqueNumbers)
.won(won)
.bid(bid)
.toBank(bid - toJackpot)
.toJackpot(toJackpot)
.rolls(diceRoles)
.currentJackpot(currentJackpot + toJackpot)
.build();
if(won) {
addCredits(user, currentJackpot);
currentJackpot = 1000L;
} else {
currentJackpot += toJackpot;
addCredits(user, -bid);
}
configService.setOrCreateConfigValue(CREDIT_GAMBLE_STORAGE, serverId, currentJackpot.toString());
return result;
}
private Integer creditGambleDiceResult() {
return secureRandom.nextInt(7) + 1;
}
private SlotMapping getSpecialPattern(String row){
return WINNING_PATTERNS
.stream()
.filter(slotMapping -> row.contains(slotMapping.processedMapping))
.findFirst()
.orElse(null);
}
private static class SlotMapping {
private Integer factor;
private String processedMapping;
private String outcome;
public SlotMapping(List<String> slots, Integer factor, String outcome) {
this.factor = factor;
this.processedMapping = String.join("", slots);
this.outcome = outcome;
}
}
}

View File

@@ -0,0 +1,51 @@
package dev.sheldan.abstracto.entertainment.service.management;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.model.database.EconomyLeaderboardResult;
import dev.sheldan.abstracto.entertainment.model.database.EconomyUser;
import dev.sheldan.abstracto.entertainment.repository.EconomyUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
@Component
public class EconomyUserManagementServiceBean implements EconomyUserManagementService {
@Autowired
private EconomyUserRepository repository;
@Override
public EconomyUser createUser(AUserInAServer aUserInAServer) {
EconomyUser user = EconomyUser
.builder()
.id(aUserInAServer.getUserInServerId())
.server(aUserInAServer.getServerReference())
.lastPayDay(Instant.now().minus(1, ChronoUnit.DAYS))
.lastSlots(Instant.now().minus(1, ChronoUnit.DAYS))
.credits(0L)
.user(aUserInAServer)
.build();
return repository.save(user);
}
@Override
public Optional<EconomyUser> getUser(AUserInAServer aUserInAServer) {
return repository.findByUser(aUserInAServer);
}
@Override
public EconomyLeaderboardResult getRankOfUserInServer(AUserInAServer aUserInAServer) {
return repository.getRankOfUserInServer(aUserInAServer.getUserInServerId(), aUserInAServer.getServerReference().getId());
}
@Override
public List<EconomyUser> getRanksInServer(AServer server, Integer page, Integer pagesize) {
return repository.findTop10ByServerOrderByCreditsDesc(server, PageRequest.of(page, pagesize));
}
}

View File

@@ -6,3 +6,20 @@ abstracto.systemConfigs.rollDefaultHigh.longValue=6
abstracto.featureFlags.entertainment.featureName=entertainment
abstracto.featureFlags.entertainment.enabled=false
abstracto.systemConfigs.paydayCredits.name=paydayCredits
abstracto.systemConfigs.paydayCredits.longValue=120
# maybe replace this by command cooldowns which are globally on the server, instead of only channel group based
abstracto.systemConfigs.paydayCooldown.name=paydayCooldown
abstracto.systemConfigs.paydayCooldown.longValue=300
abstracto.systemConfigs.slotsCooldown.name=slotsCooldown
abstracto.systemConfigs.slotsCooldown.longValue=30
abstracto.featureFlags.economy.featureName=economy
abstracto.featureFlags.economy.enabled=false
# for now this is fine
abstracto.systemConfigs.creditGambleJackpot.name=creditGambleJackpot
abstracto.systemConfigs.creditGambleJackpot.longValue=1000

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="seedData/data.xml" relativeToChangelogFile="true"/>
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,45 @@
<?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="economyFeature" value="(SELECT id FROM feature WHERE key = 'economy')"/>
<changeSet author="Sheldan" id="economy-command">
<insert tableName="command">
<column name="name" value="payday"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${economyFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="slots"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${economyFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="creditLeaderboard"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${economyFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="credits"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${economyFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="creditGamble"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${economyFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="transferCredits"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${economyFeature}"/>
</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="economy_feature-insertion">
<insert tableName="feature">
<column name="key" value="economy"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,44 @@
<?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="economy_user-table">
<createTable tableName="economy_user">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="economy_user_pkey"/>
</column>
<column name="credits" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_pay_day" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="last_slots" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
<column name="server_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseColumnNames="server_id" baseTableName="economy_user" constraintName="fk_economy_user_server"
deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id" referencedTableName="server" validate="true"/>
<sql>
DROP TRIGGER IF EXISTS economy_user_update_trigger ON economy_user;
CREATE TRIGGER economy_user_update_trigger BEFORE UPDATE ON economy_user FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
</sql>
<sql>
DROP TRIGGER IF EXISTS economy_user_insert_trigger ON economy_user;
CREATE TRIGGER economy_user_insert_trigger BEFORE INSERT ON economy_user FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
</sql>
</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="economy_user.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -9,4 +9,5 @@
<include file="1.0-entertainment/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.8-entertainment/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.9-entertainment/collection.xml" relativeToChangelogFile="true"/>
<include file="1.4.0/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

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 EconomyFeatureConfig implements FeatureConfig {
public static final String PAYDAY_CREDITS_CONFIG_KEY = "paydayCredits";
public static final String PAYDAY_COOLDOWN_CONFIG_KEY = "paydayCooldown";
public static final String SLOTS_COOLDOWN_CONFIG_KEY = "slotsCooldown";
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ECONOMY;
}
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(PAYDAY_CREDITS_CONFIG_KEY, PAYDAY_COOLDOWN_CONFIG_KEY, SLOTS_COOLDOWN_CONFIG_KEY);
}
}

View File

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

View File

@@ -3,4 +3,5 @@ package dev.sheldan.abstracto.entertainment.config;
public class EntertainmentSlashCommandNames {
public static final String ENTERTAINMENT = "entertainment";
public static final String UTILITY = "utility";
public static final String ECONOMY = "economy";
}

View File

@@ -0,0 +1,18 @@
package dev.sheldan.abstracto.entertainment.dto;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Builder
@Getter
public class CreditGambleResult {
private List<Integer> rolls;
private Integer uniqueNumbers;
private Long bid;
private Long toBank;
private Long toJackpot;
private Long currentJackpot;
private Boolean won;
}

View File

@@ -0,0 +1,11 @@
package dev.sheldan.abstracto.entertainment.dto;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class PayDayResult {
private Long currentCredits;
private Long gainedCredits;
}

View File

@@ -0,0 +1,18 @@
package dev.sheldan.abstracto.entertainment.dto;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Builder
@Getter
public class SlotsResult {
private Long bid;
private Long oldCredits;
private Long newCredits;
private Long winnings;
private Long factor;
private String outComeKey;
private List<List<String>> rows;
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.entertainment.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
public class NotEnoughCreditsException extends AbstractoTemplatableException {
@Override
public String getTemplateName() {
return "not_enough_wealth_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,28 @@
package dev.sheldan.abstracto.entertainment.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
import dev.sheldan.abstracto.entertainment.model.exception.PayDayCooldownExceptionModel;
import java.time.Duration;
public class PayDayCooldownException extends AbstractoTemplatableException {
private final PayDayCooldownExceptionModel model;
public PayDayCooldownException(Duration duration) {
this.model = PayDayCooldownExceptionModel
.builder()
.tryAgainDuration(duration)
.build();
}
@Override
public String getTemplateName() {
return "payday_cooldown_exception";
}
@Override
public Object getTemplateModel() {
return model;
}
}

View File

@@ -0,0 +1,29 @@
package dev.sheldan.abstracto.entertainment.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
import dev.sheldan.abstracto.entertainment.model.exception.PayDayCooldownExceptionModel;
import dev.sheldan.abstracto.entertainment.model.exception.SlotsCooldownExceptionModel;
import java.time.Duration;
public class SlotsCooldownException extends AbstractoTemplatableException {
private final SlotsCooldownExceptionModel model;
public SlotsCooldownException(Duration duration) {
this.model = SlotsCooldownExceptionModel
.builder()
.tryAgainDuration(duration)
.build();
}
@Override
public String getTemplateName() {
return "slots_cooldown_exception";
}
@Override
public Object getTemplateModel() {
return model;
}
}

View File

@@ -0,0 +1,32 @@
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.entertainment.dto.CreditGambleResult;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Getter
@Builder
public class CreditGambleResultModel {
private List<Integer> rolls;
private Integer uniqueNumbers;
private Long bid;
private Long toBank;
private Long toJackpot;
private Long currentJackpot;
private Boolean won;
public static CreditGambleResultModel fromCreditGambleResult(CreditGambleResult creditGambleResult) {
return CreditGambleResultModel
.builder()
.rolls(creditGambleResult.getRolls())
.uniqueNumbers(creditGambleResult.getUniqueNumbers())
.bid(creditGambleResult.getBid())
.toBank(creditGambleResult.getToBank())
.toJackpot(creditGambleResult.getToJackpot())
.currentJackpot(creditGambleResult.getCurrentJackpot())
.won(creditGambleResult.getWon())
.build();
}
}

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
@Getter
@Setter
@Builder
public class CreditsLeaderboardEntry {
private MemberDisplay memberDisplay;
private Member member;
private Long credits;
private Integer rank;
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.entertainment.model.command;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Getter
@Builder
public class CreditsLeaderboardResponseModel {
private List<CreditsLeaderboardEntry> entries;
private CreditsLeaderboardEntry ownRank;
}

View File

@@ -0,0 +1,10 @@
package dev.sheldan.abstracto.entertainment.model.command;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class CreditsModel {
private CreditsLeaderboardEntry entry;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.entertainment.model.command;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class PayDayResponseModel {
private Long currentCredits;
private Long leaderboardPosition;
private Long gainedCredits;
}

View File

@@ -0,0 +1,32 @@
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.entertainment.dto.SlotsResult;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
@Builder
@Getter
public class SlotsResponseModel {
private Long bid;
private Long oldCredits;
private Long newCredits;
private Long winnings;
private Long factor;
private String outComeKey;
private List<List<String>> rows;
public static SlotsResponseModel fromSlotsResult(SlotsResult result) {
return SlotsResponseModel
.builder()
.bid(result.getBid())
.factor(result.getFactor())
.newCredits(result.getNewCredits())
.outComeKey(result.getOutComeKey())
.oldCredits(result.getOldCredits())
.rows(result.getRows())
.winnings(result.getWinnings())
.build();
}
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.entertainment.model.database;
public interface EconomyLeaderboardResult {
Long getId();
Long getUserid();
Long getCredits();
Integer getRank();
}

View File

@@ -0,0 +1,49 @@
package dev.sheldan.abstracto.entertainment.model.database;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import java.io.Serializable;
import java.time.Instant;
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "economy_user")
@Getter
@Setter
@EqualsAndHashCode
public class EconomyUser implements Serializable {
@Id
@Column(name = "id", nullable = false)
private Long id;
@OneToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@PrimaryKeyJoinColumn
private AUserInAServer user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "server_id", nullable = false)
private AServer server;
@Column(name = "credits", nullable = false)
private Long credits;
@Column(name = "last_slots", nullable = false)
private Instant lastSlots;
@Column(name = "last_pay_day", nullable = false)
private Instant lastPayDay;
@Column(name = "created", nullable = false, insertable = false, updatable = false)
private Instant created;
@Column(name = "updated", insertable = false, updatable = false)
private Instant updated;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.entertainment.model.exception;
import lombok.Builder;
import lombok.Getter;
import java.time.Duration;
@Builder
@Getter
public class PayDayCooldownExceptionModel {
private Duration tryAgainDuration;
}

View File

@@ -0,0 +1,12 @@
package dev.sheldan.abstracto.entertainment.model.exception;
import lombok.Builder;
import lombok.Getter;
import java.time.Duration;
@Builder
@Getter
public class SlotsCooldownExceptionModel {
private Duration tryAgainDuration;
}

View File

@@ -0,0 +1,41 @@
package dev.sheldan.abstracto.entertainment.service;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.entertainment.dto.CreditGambleResult;
import dev.sheldan.abstracto.entertainment.dto.PayDayResult;
import dev.sheldan.abstracto.entertainment.dto.SlotsResult;
import dev.sheldan.abstracto.entertainment.model.command.CreditsLeaderboardEntry;
import dev.sheldan.abstracto.entertainment.model.database.EconomyUser;
import lombok.Builder;
import lombok.Getter;
import java.time.Duration;
import java.util.List;
public interface EconomyService {
boolean canTriggerPayDay(AUserInAServer aUserInAServer);
boolean canTriggerSlots(AUserInAServer aUserInAServer);
boolean canTriggerPayDay(EconomyUser economyUser);
boolean canTriggerSlots(EconomyUser economyUser);
Duration payDayTriggerIn(EconomyUser economyUser);
Duration slotsTriggerIn(EconomyUser economyUser);
EconomyUser addCredits(AUserInAServer aUserInAServer, Long credits);
void addCredits(EconomyUser economyUser, Long credits);
void addPayDayCredits(AUserInAServer aUserInAServer);
PayDayResult triggerPayDay(AUserInAServer aUserInAServer);
SlotsResult triggerSlots(AUserInAServer aUserInAServer, Long bid);
SlotGame playSlots();
List<CreditsLeaderboardEntry> getCreditLeaderboard(AServer server, Integer page);
CreditsLeaderboardEntry getRankOfUser(AUserInAServer aUserInAServer);
void transferCredits(AUserInAServer source, AUserInAServer target, Long amount);
CreditGambleResult triggerCreditGamble(AUserInAServer aUserInAServer);
@Builder
@Getter
class SlotGame {
private List<List<String>> rows;
private String outcome;
private Integer resultFactor;
}
}

View File

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