From 0c9b3ce651f758fa0b75c824f036b20ddfc6cdce Mon Sep 17 00:00:00 2001 From: Jason <11360596+jpenilla@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:15:51 -0700 Subject: [PATCH] Party chat (#299) --- .../api/event/events/PartyJoinEvent.java | 58 ++++ .../api/event/events/PartyLeaveEvent.java | 58 ++++ .../carbon/api/users/CarbonPlayer.java | 13 +- .../net/draycia/carbon/api/users/Party.java | 85 +++++ .../draycia/carbon/api/users/UserManager.java | 27 ++ .../carbon/common/CarbonCommonModule.java | 3 + .../channels/CarbonChannelRegistry.java | 44 ++- .../common/channels/ConfigChatChannel.java | 11 +- .../common/channels/PartyChatChannel.java | 106 ++++++ .../messages/ConfigChannelMessageSource.java | 4 +- .../messages/ConfigChannelMessages.java | 5 +- .../command/commands/IgnoreListCommand.java | 52 +-- .../command/commands/PartyCommands.java | 303 ++++++++++++++++++ .../carbon/common/config/PrimaryConfig.java | 7 + .../messages/CarbonMessageRenderer.java | 18 +- .../common/messages/CarbonMessages.java | 82 ++++- .../carbon/common/messages/Option.java | 24 ++ .../common/messages/OptionTagResolver.java | 57 ++++ .../OptionPlaceholderResolver.java | 52 +++ .../messaging/CarbonChatPacketHandler.java | 21 +- .../common/messaging/MessagingManager.java | 12 +- .../messaging/packets/CarbonPacket.java | 8 + .../packets/InvalidatePartyInvitePacket.java | 73 +++++ .../messaging/packets/PacketFactory.java | 7 + .../messaging/packets/PartyChangePacket.java | 84 +++++ .../messaging/packets/PartyInvitePacket.java | 92 ++++++ .../common/users/CachingUserManager.java | 101 +++++- .../common/users/CarbonPlayerCommon.java | 30 +- .../common/users/ConsoleCarbonPlayer.java | 7 + .../carbon/common/users/NetworkUsers.java | 9 + .../carbon/common/users/PartyImpl.java | 272 ++++++++++++++++ .../carbon/common/users/PartyInvites.java | 138 ++++++++ .../common/users/PlatformUserManager.java | 37 ++- .../common/users/UserManagerInternal.java | 6 + .../common/users/WrappedCarbonPlayer.java | 15 + .../common/users/db/DatabaseUserManager.java | 114 ++++++- .../users/db/mapper/PartyRowMapper.java | 46 +++ .../users/db/mapper/PlayerRowMapper.java | 6 +- .../common/users/json/JSONUserManager.java | 72 ++++- .../carbon/common/util/CloudUtils.java | 3 +- .../carbon/common/util/PaginationHelper.java | 81 +++++ .../locale/messages-en_US.properties | 27 ++ .../resources/queries/clear-party-members.sql | 1 + .../resources/queries/drop-party-member.sql | 1 + .../src/main/resources/queries/drop-party.sql | 1 + .../resources/queries/insert-party-member.sql | 1 + .../main/resources/queries/insert-party.sql | 7 + .../main/resources/queries/insert-player.sql | 6 +- .../queries/migrations/h2/V3__parties.sql | 12 + .../queries/migrations/mysql/V7__parties.sql | 12 + .../migrations/postgresql/V7__parties.sql | 12 + .../queries/select-party-members.sql | 1 + .../main/resources/queries/select-party.sql | 4 + .../main/resources/queries/select-player.sql | 3 +- .../main/resources/queries/update-player.sql | 3 +- .../carbon/fabric/FabricMessageRenderer.java | 7 +- .../paper/hooks/CarbonPAPIPlaceholders.java | 33 +- .../paper/messages/PaperMessageRenderer.java | 7 +- .../velocity/VelocityMessageRenderer.java | 7 +- 59 files changed, 2259 insertions(+), 129 deletions(-) create mode 100644 api/src/main/java/net/draycia/carbon/api/event/events/PartyJoinEvent.java create mode 100644 api/src/main/java/net/draycia/carbon/api/event/events/PartyLeaveEvent.java create mode 100644 api/src/main/java/net/draycia/carbon/api/users/Party.java create mode 100644 common/src/main/java/net/draycia/carbon/common/channels/PartyChatChannel.java create mode 100644 common/src/main/java/net/draycia/carbon/common/command/commands/PartyCommands.java create mode 100644 common/src/main/java/net/draycia/carbon/common/messages/Option.java create mode 100644 common/src/main/java/net/draycia/carbon/common/messages/OptionTagResolver.java create mode 100644 common/src/main/java/net/draycia/carbon/common/messages/placeholders/OptionPlaceholderResolver.java create mode 100644 common/src/main/java/net/draycia/carbon/common/messaging/packets/InvalidatePartyInvitePacket.java create mode 100644 common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyChangePacket.java create mode 100644 common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyInvitePacket.java create mode 100644 common/src/main/java/net/draycia/carbon/common/users/PartyImpl.java create mode 100644 common/src/main/java/net/draycia/carbon/common/users/PartyInvites.java create mode 100644 common/src/main/java/net/draycia/carbon/common/users/db/mapper/PartyRowMapper.java create mode 100644 common/src/main/java/net/draycia/carbon/common/util/PaginationHelper.java create mode 100644 common/src/main/resources/queries/clear-party-members.sql create mode 100644 common/src/main/resources/queries/drop-party-member.sql create mode 100644 common/src/main/resources/queries/drop-party.sql create mode 100644 common/src/main/resources/queries/insert-party-member.sql create mode 100644 common/src/main/resources/queries/insert-party.sql create mode 100644 common/src/main/resources/queries/migrations/h2/V3__parties.sql create mode 100644 common/src/main/resources/queries/migrations/mysql/V7__parties.sql create mode 100644 common/src/main/resources/queries/migrations/postgresql/V7__parties.sql create mode 100644 common/src/main/resources/queries/select-party-members.sql create mode 100644 common/src/main/resources/queries/select-party.sql diff --git a/api/src/main/java/net/draycia/carbon/api/event/events/PartyJoinEvent.java b/api/src/main/java/net/draycia/carbon/api/event/events/PartyJoinEvent.java new file mode 100644 index 000000000..331ea1ae8 --- /dev/null +++ b/api/src/main/java/net/draycia/carbon/api/event/events/PartyJoinEvent.java @@ -0,0 +1,58 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.api.event.events; + +import java.util.UUID; +import net.draycia.carbon.api.event.CarbonEvent; +import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +/** + * Called when a player is added to a {@link Party}. + * + * @since 2.1.0 + */ +@DefaultQualifier(NonNull.class) +public interface PartyJoinEvent extends CarbonEvent { + + /** + * ID of the player joining a party. + * + *

The player's {@link CarbonPlayer#party()} field is not guaranteed to be updated immediately, + * especially if the change needs to propagate cross-server.

+ * + * @return player id + * @since 2.1.0 + */ + UUID playerId(); + + /** + * The party being joined. + * + *

{@link Party#members()} will reflect the new member.

+ * + * @return party + * @since 2.1.0 + */ + Party party(); + +} diff --git a/api/src/main/java/net/draycia/carbon/api/event/events/PartyLeaveEvent.java b/api/src/main/java/net/draycia/carbon/api/event/events/PartyLeaveEvent.java new file mode 100644 index 000000000..84cd687ac --- /dev/null +++ b/api/src/main/java/net/draycia/carbon/api/event/events/PartyLeaveEvent.java @@ -0,0 +1,58 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.api.event.events; + +import java.util.UUID; +import net.draycia.carbon.api.event.CarbonEvent; +import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +/** + * Called when a player is removed from a {@link Party}. + * + * @since 2.1.0 + */ +@DefaultQualifier(NonNull.class) +public interface PartyLeaveEvent extends CarbonEvent { + + /** + * ID of the player leaving a party. + * + *

The player's {@link CarbonPlayer#party()} field is not guaranteed to be updated immediately, + * especially if the change needs to propagate cross-server.

+ * + * @return player id + * @since 2.1.0 + */ + UUID playerId(); + + /** + * The party being left. + * + *

{@link Party#members()} will reflect the removed member.

+ * + * @return party + * @since 2.1.0 + */ + Party party(); + +} diff --git a/api/src/main/java/net/draycia/carbon/api/users/CarbonPlayer.java b/api/src/main/java/net/draycia/carbon/api/users/CarbonPlayer.java index ca0c2ac22..a95329cc5 100644 --- a/api/src/main/java/net/draycia/carbon/api/users/CarbonPlayer.java +++ b/api/src/main/java/net/draycia/carbon/api/users/CarbonPlayer.java @@ -23,6 +23,7 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.util.InventorySlot; import net.kyori.adventure.audience.Audience; @@ -248,7 +249,7 @@ record ChannelMessage(Component message, ChatChannel channel) {} /** * Adds the player to and removes the player from the ignore list. * - * @param player the player to be added/removed + * @param player the player to be added/removed * @param nowIgnoring if the player should be ignored * @since 2.0.0 */ @@ -257,7 +258,7 @@ record ChannelMessage(Component message, ChatChannel channel) {} /** * Adds the player to and removes the player from the ignore list. * - * @param player the player to be added/removed + * @param player the player to be added/removed * @param nowIgnoring if the player should be ignored * @since 2.0.0 */ @@ -404,4 +405,12 @@ record ChannelMessage(Component message, ChatChannel channel) {} */ void leaveChannel(ChatChannel channel); + /** + * Get this player's current {@link Party}. + * + * @return party future + * @since 2.1.0 + */ + CompletableFuture<@Nullable Party> party(); + } diff --git a/api/src/main/java/net/draycia/carbon/api/users/Party.java b/api/src/main/java/net/draycia/carbon/api/users/Party.java new file mode 100644 index 000000000..5001c81e3 --- /dev/null +++ b/api/src/main/java/net/draycia/carbon/api/users/Party.java @@ -0,0 +1,85 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.api.users; + +import java.util.Set; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +/** + * Reference to a chat party. + * + * @see UserManager#createParty(Component) + * @see UserManager#party(UUID) + * @since 2.1.0 + */ +@DefaultQualifier(NonNull.class) +public interface Party { + + /** + * Get the name of this party. + * + * @return party name + * @since 2.1.0 + */ + Component name(); + + /** + * Get the unique id of this party. + * + * @return party id + * @since 2.1.0 + */ + UUID id(); + + /** + * Get a snapshot of the current party members. + * + * @return party members + * @since 2.1.0 + */ + Set members(); + + /** + * Add a user to this party. They will automatically be removed from their previous party if necessary. + * + * @param id user id + * @since 2.1.0 + */ + void addMember(UUID id); + + /** + * Remove a user from this party. + * + * @param id user id + * @since 2.1.0 + */ + void removeMember(UUID id); + + /** + * Disband this party. Will remove all members and delete persistent data. + * + * @since 2.1.0 + */ + void disband(); + +} diff --git a/api/src/main/java/net/draycia/carbon/api/users/UserManager.java b/api/src/main/java/net/draycia/carbon/api/users/UserManager.java index 12217009c..3f089c6a7 100644 --- a/api/src/main/java/net/draycia/carbon/api/users/UserManager.java +++ b/api/src/main/java/net/draycia/carbon/api/users/UserManager.java @@ -21,7 +21,9 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; /** @@ -45,4 +47,29 @@ public interface UserManager { */ CompletableFuture user(UUID uuid); + /** + * Create a new {@link Party} with the specified name. + * + *

Parties with no users will not be saved. Use {@link Party#disband()} to discard.

+ *

The returned reference will expire after one minute, store {@link Party#id()} rather than the instance and use {@link #party(UUID)} to retrieve.

+ * + * @param name party name + * @return new party + * @since 2.1.0 + */ + Party createParty(Component name); + + /** + * Look up an existing party by its id. + * + *

As parties that have never had a user are not saved, they are not retrievable here.

+ *

The returned reference will expire after one minute, do not cache it. The implementation handles caching as is appropriate.

+ * + * @param id party id + * @return existing party + * @see #createParty(Component) + * @since 2.1.0 + */ + CompletableFuture<@Nullable Party> party(UUID id); + } diff --git a/common/src/main/java/net/draycia/carbon/common/CarbonCommonModule.java b/common/src/main/java/net/draycia/carbon/common/CarbonCommonModule.java index 79e7204e9..54c4932ef 100644 --- a/common/src/main/java/net/draycia/carbon/common/CarbonCommonModule.java +++ b/common/src/main/java/net/draycia/carbon/common/CarbonCommonModule.java @@ -58,12 +58,14 @@ import net.draycia.carbon.common.messages.CarbonMessageSender; import net.draycia.carbon.common.messages.CarbonMessageSource; import net.draycia.carbon.common.messages.CarbonMessages; +import net.draycia.carbon.common.messages.Option; import net.draycia.carbon.common.messages.SourcedReceiverResolver; import net.draycia.carbon.common.messages.StandardPlaceholderResolverStrategyButDifferent; import net.draycia.carbon.common.messages.placeholders.BooleanPlaceholderResolver; import net.draycia.carbon.common.messages.placeholders.ComponentPlaceholderResolver; import net.draycia.carbon.common.messages.placeholders.IntPlaceholderResolver; import net.draycia.carbon.common.messages.placeholders.KeyPlaceholderResolver; +import net.draycia.carbon.common.messages.placeholders.OptionPlaceholderResolver; import net.draycia.carbon.common.messages.placeholders.StringPlaceholderResolver; import net.draycia.carbon.common.messages.placeholders.UUIDPlaceholderResolver; import net.draycia.carbon.common.messaging.ServerId; @@ -160,6 +162,7 @@ public CarbonMessages carbonMessages( .weightedPlaceholderResolver(Integer.class, intPlaceholderResolver, 0) .weightedPlaceholderResolver(Key.class, keyPlaceholderResolver, 0) .weightedPlaceholderResolver(Boolean.class, booleanPlaceholderResolver, 0) + .weightedPlaceholderResolver(Option.class, new OptionPlaceholderResolver<>(), 0) .create(this.getClass().getClassLoader()); } diff --git a/common/src/main/java/net/draycia/carbon/common/channels/CarbonChannelRegistry.java b/common/src/main/java/net/draycia/carbon/common/channels/CarbonChannelRegistry.java index c658bd9a7..3b20f264d 100644 --- a/common/src/main/java/net/draycia/carbon/common/channels/CarbonChannelRegistry.java +++ b/common/src/main/java/net/draycia/carbon/common/channels/CarbonChannelRegistry.java @@ -29,6 +29,7 @@ import com.seiama.registry.Holder; import com.seiama.registry.Registry; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -75,11 +76,14 @@ @DefaultQualifier(NonNull.class) public class CarbonChannelRegistry extends ChatListenerInternal implements ChannelRegistry { + private static final String PARTYCHAT_CONF = "partychat.conf"; private static @MonotonicNonNull ObjectMapper MAPPER; + private static @MonotonicNonNull ObjectMapper PARTY_MAPPER; static { try { MAPPER = ObjectMapper.factory().get(ConfigChatChannel.class); + PARTY_MAPPER = ObjectMapper.factory().get(PartyChatChannel.class); } catch (final SerializationException e) { e.printStackTrace(); } @@ -88,7 +92,7 @@ public class CarbonChannelRegistry extends ChatListenerInternal implements Chann private final Path configChannelDir; private final Injector injector; private final Logger logger; - private final ConfigManager configManager; + private final ConfigManager config; private @MonotonicNonNull Key defaultKey; private final CarbonMessages carbonMessages; private final CarbonEventHandler eventHandler; @@ -103,15 +107,15 @@ public CarbonChannelRegistry( @DataDirectory final Path dataDirectory, final Injector injector, final Logger logger, - final ConfigManager configManager, + final ConfigManager config, final CarbonMessages carbonMessages, final CarbonEventHandler events ) { - super(events, carbonMessages, configManager); + super(events, carbonMessages, config); this.configChannelDir = dataDirectory.resolve("channels"); this.injector = injector; this.logger = logger; - this.configManager = configManager; + this.config = config; this.carbonMessages = carbonMessages; this.eventHandler = events; @@ -211,10 +215,16 @@ public void loadConfigChannels(final CarbonMessages messages) { private void loadConfigChannels_(final CarbonMessages messages) { this.logger.info("Loading config channels..."); - this.defaultKey = this.configManager.primaryConfig().defaultChannel(); + this.defaultKey = this.config.primaryConfig().defaultChannel(); + + final boolean party = this.config.primaryConfig().partyChat(); + if (party) { + this.saveDefaultPartyConfig(); + } List channelConfigs = FileUtil.listDirectoryEntries(this.configChannelDir, "*.conf"); - if (channelConfigs.isEmpty()) { + if (channelConfigs.isEmpty() || + party && channelConfigs.size() == 1 && channelConfigs.get(0).getFileName().toString().equals(PARTYCHAT_CONF)) { this.saveDefaultChannelConfig(); channelConfigs = FileUtil.listDirectoryEntries(this.configChannelDir, "*.conf"); } @@ -258,7 +268,7 @@ private void saveDefaultChannelConfig() { try { final Path configFile = this.configChannelDir.resolve("global.conf"); final ConfigChatChannel configChannel = this.injector.getInstance(ConfigChatChannel.class); - final ConfigurationLoader loader = this.configManager.configurationLoader(FileUtil.mkParentDirs(configFile)); + final ConfigurationLoader loader = this.config.configurationLoader(FileUtil.mkParentDirs(configFile)); final ConfigurationNode node = loader.createNode(); node.set(ConfigChatChannel.class, configChannel); loader.save(node); @@ -267,13 +277,29 @@ private void saveDefaultChannelConfig() { } } + private void saveDefaultPartyConfig() { + final Path configFile = this.configChannelDir.resolve(PARTYCHAT_CONF); + if (Files.isRegularFile(configFile)) { + return; + } + try { + final ConfigChatChannel configChannel = this.injector.getInstance(PartyChatChannel.class); + final ConfigurationLoader loader = this.config.configurationLoader(FileUtil.mkParentDirs(configFile)); + final ConfigurationNode node = loader.createNode(); + node.set(PartyChatChannel.class, configChannel); + loader.save(node); + } catch (final IOException exception) { + throw Exceptions.rethrow(exception); + } + } + private @Nullable ChatChannel loadChannel(final Path channelFile) { - final ConfigurationLoader loader = this.configManager.configurationLoader(channelFile); + final ConfigurationLoader loader = this.config.configurationLoader(channelFile); try { final ConfigurationNode loaded = updateNode(loader.load()); loader.save(loaded); - return MAPPER.load(loaded); + return (this.config.primaryConfig().partyChat() && channelFile.getFileName().toString().equals(PARTYCHAT_CONF) ? PARTY_MAPPER : MAPPER).load(loaded); } catch (final ConfigurateException exception) { this.logger.warn("Failed to load channel from file '{}'", channelFile, exception); } diff --git a/common/src/main/java/net/draycia/carbon/common/channels/ConfigChatChannel.java b/common/src/main/java/net/draycia/carbon/common/channels/ConfigChatChannel.java index d0b704127..00d888e77 100644 --- a/common/src/main/java/net/draycia/carbon/common/channels/ConfigChatChannel.java +++ b/common/src/main/java/net/draycia/carbon/common/channels/ConfigChatChannel.java @@ -64,7 +64,7 @@ @ConfigSerializable @DefaultQualifier(NonNull.class) -public final class ConfigChatChannel implements ChatChannel { +public class ConfigChatChannel implements ChatChannel { private transient @MonotonicNonNull @Inject CarbonServer server; private transient @MonotonicNonNull @Inject CarbonMessageRenderer renderer; @@ -74,7 +74,7 @@ public final class ConfigChatChannel implements ChatChannel { You only need to change the second part of the key. "global" by default. The value is what's used in commands, this is probably what you want to change. """) - private @Nullable Key key = Key.key("carbon", "global"); + protected @Nullable Key key = Key.key("carbon", "global"); @Comment(""" The permission required to use the /channel and / commands. @@ -87,7 +87,7 @@ public final class ConfigChatChannel implements ChatChannel { @Setting("format") @Comment("The chat formats for this channel.") - private @Nullable ConfigChannelMessageSource messageSource = new ConfigChannelMessageSource(); + protected @Nullable ConfigChannelMessageSource messageSource = new ConfigChannelMessageSource(); @Comment("Messages will be sent in this channel if they start with this prefix.") private @Nullable String quickPrefix = null; @@ -151,7 +151,8 @@ public List commandAliases() { this.key(), sender.displayName(), sender.username(), - message + message, + Component.text("null") ); } @@ -219,7 +220,7 @@ private ConfigChannelMessages loadMessages() { } } - private ConfigChannelMessages carbonMessages() { + protected ConfigChannelMessages carbonMessages() { if (this.carbonMessages == null) { this.carbonMessages = this.loadMessages(); } diff --git a/common/src/main/java/net/draycia/carbon/common/channels/PartyChatChannel.java b/common/src/main/java/net/draycia/carbon/common/channels/PartyChatChannel.java new file mode 100644 index 000000000..4accef1bb --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/channels/PartyChatChannel.java @@ -0,0 +1,106 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.channels; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; +import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; +import net.draycia.carbon.common.messages.SourcedAudience; +import net.draycia.carbon.common.users.WrappedCarbonPlayer; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +@DefaultQualifier(NonNull.class) +public class PartyChatChannel extends ConfigChatChannel { + + public PartyChatChannel() { + this.key = Key.key("carbon", "partychat"); + this.messageSource = new ConfigChannelMessageSource(); + this.messageSource.defaults = Map.of( + "default_format", "(party: ) : ", + "console", "[party: ] - : " + ); + this.messageSource.locales = Map.of( + Locale.US, Map.of("default_format", "(party: ) : ") + ); + } + + @Override + public ChannelPermissionResult speechPermitted(final CarbonPlayer player) { + return player.party().join() != null + ? ChannelPermissionResult.allowed() + : ChannelPermissionResult.denied(Component.empty()); + } + + @Override + public ChannelPermissionResult hearingPermitted(final CarbonPlayer player) { + return player.party().join() != null + ? ChannelPermissionResult.allowed() + : ChannelPermissionResult.denied(Component.empty()); + } + + @Override + public List recipients(final CarbonPlayer sender) { + final WrappedCarbonPlayer wrapped = (WrappedCarbonPlayer) sender; + final @Nullable UUID party = wrapped.partyId(); + if (party == null) { + if (sender.online()) { + sender.sendMessage(Component.text("You must join a party to use this channel.", NamedTextColor.RED)); + } + return new ArrayList<>(); + } + final List recipients = super.recipients(sender); + recipients.removeIf(r -> r instanceof WrappedCarbonPlayer p && !Objects.equals(p.partyId(), party)); + return recipients; + } + + @Override + public @NotNull Component render( + final CarbonPlayer sender, + final Audience recipient, + final Component message, + final Component originalMessage + ) { + final @Nullable Party party = sender.party().join(); + return this.carbonMessages().chatFormat( + SourcedAudience.of(sender, recipient), + sender.uuid(), + this.key(), + sender.displayName(), + sender.username(), + message, + party == null ? Component.text("null") : party.name() + ); + } +} diff --git a/common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessageSource.java b/common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessageSource.java index b2380b523..33fa14dba 100644 --- a/common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessageSource.java +++ b/common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessageSource.java @@ -57,7 +57,7 @@ The keys are group names, the values are chat formats (MiniMessage). discord="" } """) - private final Map defaults = Map.of( + public Map defaults = Map.of( "default_format", ": ", "console", "[] - : ", "discord", "" @@ -69,7 +69,7 @@ The keys are group names, the values are chat formats (MiniMessage). You can safely delete this section if you don't want to use this feature. Will fall back to the defaults section if no format was found for the player. """) - private final Map> locales = Map.of( + public Map> locales = Map.of( Locale.US, Map.of("default_format", ": ") ); diff --git a/common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessages.java b/common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessages.java index 8e5928da1..676d32894 100644 --- a/common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessages.java +++ b/common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessages.java @@ -34,12 +34,13 @@ public interface ConfigChannelMessages { // TODO: locale placeholders? @Message("channel.format") Component chatFormat( - final SourcedAudience audience, + SourcedAudience audience, @Placeholder UUID uuid, @Placeholder Key channel, @Placeholder("display_name") Component displayName, @Placeholder String username, - @Placeholder Component message + @Placeholder Component message, + @Placeholder("party_name") Component partyName ); } diff --git a/common/src/main/java/net/draycia/carbon/common/command/commands/IgnoreListCommand.java b/common/src/main/java/net/draycia/carbon/common/command/commands/IgnoreListCommand.java index 8b4f503ef..6f3a3e579 100644 --- a/common/src/main/java/net/draycia/carbon/common/command/commands/IgnoreListCommand.java +++ b/common/src/main/java/net/draycia/carbon/common/command/commands/IgnoreListCommand.java @@ -24,7 +24,6 @@ import cloud.commandframework.context.CommandContext; import cloud.commandframework.minecraft.extras.MinecraftExtrasMetaKeys; import com.google.inject.Inject; -import java.util.function.IntFunction; import java.util.function.Supplier; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; @@ -34,34 +33,30 @@ import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.util.Pagination; +import net.draycia.carbon.common.util.PaginationHelper; import net.kyori.adventure.key.Key; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.ComponentLike; -import net.kyori.adventure.text.TextComponent; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; -import static net.kyori.adventure.text.Component.empty; -import static net.kyori.adventure.text.Component.space; -import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.event.ClickEvent.runCommand; - @DefaultQualifier(NonNull.class) public final class IgnoreListCommand extends CarbonCommand { private final UserManager users; private final CommandManager commandManager; private final CarbonMessages messages; + private final PaginationHelper pagination; @Inject public IgnoreListCommand( final UserManager userManager, final CommandManager commandManager, - final CarbonMessages messages + final CarbonMessages messages, + final PaginationHelper pagination ) { this.users = userManager; this.commandManager = commandManager; this.messages = messages; + this.pagination = pagination; } @Override @@ -105,7 +100,7 @@ private void execute(final CommandContext ctx) { final CarbonPlayer p = e.get(); return this.messages.commandIgnoreListPaginationElement(p.displayName(), p.username()); }) - .footer(this.footerRenderer(p -> "/" + this.commandSettings().name() + " " + p)) + .footer(this.pagination.footerRenderer(p -> "/" + this.commandSettings().name() + " " + p)) .pageOutOfRange(this.messages::paginationOutOfRange) .build(); @@ -114,39 +109,4 @@ private void execute(final CommandContext ctx) { pagination.render(elements, page, 6).forEach(sender::sendMessage); } - private Pagination.BiIntFunction footerRenderer(final IntFunction commandFunction) { - return (currentPage, pages) -> { - if (pages == 1) { - return empty(); // we don't need to see 'Page 1/1' - } - final TextComponent.Builder buttons = text(); - if (currentPage > 1) { - buttons.append(this.previousPageButton(currentPage, commandFunction)); - } - if (currentPage > 1 && currentPage < pages) { - buttons.append(space()); - } - if (currentPage < pages) { - buttons.append(this.nextPageButton(currentPage, commandFunction)); - } - return this.messages.paginationFooter(currentPage, pages, buttons.build()); - }; - } - - private Component previousPageButton(final int currentPage, final IntFunction commandFunction) { - return text() - .content("←") - .clickEvent(runCommand(commandFunction.apply(currentPage - 1))) - .hoverEvent(this.messages.paginationClickForPreviousPage()) - .build(); - } - - private Component nextPageButton(final int currentPage, final IntFunction commandFunction) { - return text() - .content("→") - .clickEvent(runCommand(commandFunction.apply(currentPage + 1))) - .hoverEvent(this.messages.paginationClickForNextPage()) - .build(); - } - } diff --git a/common/src/main/java/net/draycia/carbon/common/command/commands/PartyCommands.java b/common/src/main/java/net/draycia/carbon/common/command/commands/PartyCommands.java new file mode 100644 index 000000000..63cded636 --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/command/commands/PartyCommands.java @@ -0,0 +1,303 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.command.commands; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.arguments.standard.IntegerArgument; +import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.minecraft.extras.MinecraftExtrasMetaKeys; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.inject.Inject; +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Supplier; +import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; +import net.draycia.carbon.common.command.ArgumentFactory; +import net.draycia.carbon.common.command.CarbonCommand; +import net.draycia.carbon.common.command.CommandSettings; +import net.draycia.carbon.common.command.Commander; +import net.draycia.carbon.common.command.PlayerCommander; +import net.draycia.carbon.common.config.ConfigManager; +import net.draycia.carbon.common.messages.CarbonMessages; +import net.draycia.carbon.common.messages.Option; +import net.draycia.carbon.common.users.NetworkUsers; +import net.draycia.carbon.common.users.PartyInvites; +import net.draycia.carbon.common.users.UserManagerInternal; +import net.draycia.carbon.common.users.WrappedCarbonPlayer; +import net.draycia.carbon.common.util.Pagination; +import net.draycia.carbon.common.util.PaginationHelper; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public final class PartyCommands extends CarbonCommand { + + private final CommandManager commandManager; + private final ArgumentFactory argumentFactory; + private final UserManagerInternal userManager; + private final PartyInvites partyInvites; + private final ConfigManager config; + private final CarbonMessages messages; + private final PaginationHelper pagination; + private final NetworkUsers network; + + @Inject + public PartyCommands( + final CommandManager commandManager, + final ArgumentFactory argumentFactory, + final UserManagerInternal userManager, + final PartyInvites partyInvites, + final ConfigManager config, + final CarbonMessages messages, + final PaginationHelper pagination, + final NetworkUsers network + ) { + this.commandManager = commandManager; + this.argumentFactory = argumentFactory; + this.userManager = userManager; + this.partyInvites = partyInvites; + this.config = config; + this.messages = messages; + this.pagination = pagination; + this.network = network; + } + + @Override + public void init() { + if (!this.config.primaryConfig().partyChat()) { + return; + } + + final var root = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) + .permission("carbon.parties"); + final var info = root.meta(MinecraftExtrasMetaKeys.DESCRIPTION, this.messages.partyDesc()).handler(this::info); + this.commandManager.command(info); + this.commandManager.command(info.literal("page") + .argument(IntegerArgument.builder("page").withMin(1).asOptionalWithDefault(1))); + this.commandManager.command( + root.literal("create") + .meta(MinecraftExtrasMetaKeys.DESCRIPTION, this.messages.partyCreateDesc()) + .argument(StringArgument.builder("name").greedy().asOptional()) + .senderType(PlayerCommander.class) + .handler(this::createParty) + ); + this.commandManager.command( + root.literal("invite") + .meta(MinecraftExtrasMetaKeys.DESCRIPTION, this.messages.partyInviteDesc()) + .senderType(PlayerCommander.class) + .argument(this.argumentFactory.carbonPlayer("player")) + .handler(this::invitePlayer) + ); + this.commandManager.command( + root.literal("accept") + .meta(MinecraftExtrasMetaKeys.DESCRIPTION, this.messages.partyAcceptDesc()) + .senderType(PlayerCommander.class) + .argument(this.argumentFactory.carbonPlayer("sender").asOptional()) + .handler(this::acceptInvite) + ); + this.commandManager.command( + root.literal("leave") + .meta(MinecraftExtrasMetaKeys.DESCRIPTION, this.messages.partyLeaveDesc()) + .senderType(PlayerCommander.class) + .handler(this::leaveParty) + ); + this.commandManager.command( + root.literal("disband") + .meta(MinecraftExtrasMetaKeys.DESCRIPTION, this.messages.partyDisbandDesc()) + .senderType(PlayerCommander.class) + .handler(this::disbandParty) + ); + } + + @Override + protected CommandSettings _commandSettings() { + return new CommandSettings("party", "group"); + } + + @Override + public Key key() { + return Key.key("carbon", "party"); + } + + private void info(final CommandContext ctx) { + final CarbonPlayer player = ((PlayerCommander) ctx.getSender()).carbonPlayer(); + final @Nullable Party party = player.party().join(); + if (party == null) { + this.messages.notInParty(player); + return; + } + + this.messages.currentParty(player, party.name()); + + final var elements = party.members().stream() + .sorted(Comparator.comparing(this.network::online).reversed().thenComparing(UUID::compareTo)) + .map(id -> (Supplier) () -> this.userManager.user(id).join()) + .toList(); + + if (elements.isEmpty()) { + throw new IllegalStateException(); + } + + final Pagination> pagination = Pagination.>builder() + .header((page, pages) -> this.messages.commandPartyPaginationHeader(party.name())) + .item((e, lastOfPage) -> { + final CarbonPlayer p = e.get(); + return this.messages.commandPartyPaginationElement(p.displayName(), p.username(), new Option(this.network.online(p))); + }) + .footer(this.pagination.footerRenderer(p -> "/" + this.commandSettings().name() + " page " + p)) + .pageOutOfRange(this.messages::paginationOutOfRange) + .build(); + + final int page = ctx.getOrDefault("page", 1); + + pagination.render(elements, page, 6).forEach(player::sendMessage); + } + + private void createParty(final CommandContext ctx) { + final CarbonPlayer player = ((PlayerCommander) ctx.getSender()).carbonPlayer(); + final @Nullable Party oldParty = player.party().join(); + if (oldParty != null) { + this.messages.mustLeavePartyFirst(player); + return; + } + final String name = ctx.getOrDefault("name", player.username() + "'s party"); + final Component component = ((WrappedCarbonPlayer) player).parseMessageTags(name); + final Party party; + try { + party = this.userManager.createParty(component); + } catch (final IllegalArgumentException e) { + this.messages.partyNameTooLong(player); + return; + } + party.addMember(player.uuid()); + this.messages.partyCreated(player, party.name()); + } + + private void invitePlayer(final CommandContext ctx) { + final CarbonPlayer player = ((PlayerCommander) ctx.getSender()).carbonPlayer(); + final CarbonPlayer recipient = ctx.get("player"); + if (recipient.uuid().equals(player.uuid())) { + this.messages.cannotInviteSelf(player); + return; + } + final @Nullable Party party = player.party().join(); + if (party == null) { + this.messages.mustBeInParty(player); + return; + } + final @Nullable Party recipientParty = recipient.party().join(); + if (recipientParty != null && recipientParty.id().equals(party.id())) { + this.messages.alreadyInParty(player, recipient.displayName()); + return; + } + this.partyInvites.sendInvite(player.uuid(), recipient.uuid(), party.id()); + this.messages.receivedPartyInvite(recipient, player.displayName(), party.name()); + this.messages.sentPartyInvite(player, recipient.displayName(), party.name()); + } + + private void acceptInvite(final CommandContext ctx) { + final @Nullable CarbonPlayer sender = ctx.getOrDefault("sender", null); + final CarbonPlayer player = ((PlayerCommander) ctx.getSender()).carbonPlayer(); + final @Nullable Invite invite = this.findInvite(player, sender); + if (invite == null) { + return; + } + final @Nullable Party old = player.party().join(); + if (old != null) { + this.messages.mustLeavePartyFirst(player); + return; + } + this.partyInvites.invalidateInvite(invite.sender(), player.uuid()); + invite.party().addMember(player.uuid()); + this.messages.joinedParty(player, invite.party().name()); + } + + private void leaveParty(final CommandContext ctx) { + final CarbonPlayer player = ((PlayerCommander) ctx.getSender()).carbonPlayer(); + final @Nullable Party old = player.party().join(); + if (old == null) { + this.messages.mustBeInParty(player); + return; + } + if (old.members().size() == 1) { + this.disbandParty(ctx); + return; + } + old.removeMember(player.uuid()); + this.messages.leftParty(player, old.name()); + } + + private void disbandParty(final CommandContext ctx) { + final CarbonPlayer player = ((PlayerCommander) ctx.getSender()).carbonPlayer(); + final @Nullable Party old = player.party().join(); + if (old == null) { + this.messages.mustBeInParty(player); + return; + } + if (old.members().size() != 1) { + this.messages.cannotDisbandParty(player, old.name()); + return; + } + old.disband(); + this.messages.disbandedParty(player, old.name()); + } + + private @Nullable Invite findInvite(final CarbonPlayer player, final @Nullable CarbonPlayer sender) { + final @Nullable Cache cache = this.partyInvites.invitesFor(player.uuid()); + final @Nullable Map map = cache != null ? Map.copyOf(cache.asMap()) : null; + + if (map == null || map.isEmpty()) { + this.messages.noPendingPartyInvites(player); + return null; + } else if (sender != null) { + final @Nullable Party p = Optional.ofNullable(map.get(sender.uuid())) + .map(id -> this.userManager.party(id).join()) + .orElse(null); + if (p == null) { + this.messages.noPartyInviteFrom(player, sender.displayName()); + return null; + } + return new Invite(sender.uuid(), p); + } + + if (map.size() == 1) { + final Map.Entry e = map.entrySet().iterator().next(); + final @Nullable Party p = this.userManager.party(e.getValue()).join(); + if (p == null) { + this.messages.noPendingPartyInvites(player); + return null; + } + return new Invite(e.getKey(), p); + } + + this.messages.mustSpecifyPartyInvite(player); + return null; + } + + private record Invite(UUID sender, Party party) {} + +} diff --git a/common/src/main/java/net/draycia/carbon/common/config/PrimaryConfig.java b/common/src/main/java/net/draycia/carbon/common/config/PrimaryConfig.java index 8496b99e2..8f71862a6 100644 --- a/common/src/main/java/net/draycia/carbon/common/config/PrimaryConfig.java +++ b/common/src/main/java/net/draycia/carbon/common/config/PrimaryConfig.java @@ -97,6 +97,13 @@ public class PrimaryConfig { private MessagingSettings messagingSettings = new MessagingSettings(); private NicknameSettings nicknameSettings = new NicknameSettings(); + @Comment("Enable and disable party chat.") + private boolean partyChat = true; + + public boolean partyChat() { + return this.partyChat; + } + public NicknameSettings nickname() { return this.nicknameSettings; } diff --git a/common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageRenderer.java b/common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageRenderer.java index 86428d7b7..25add83bb 100644 --- a/common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageRenderer.java +++ b/common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageRenderer.java @@ -19,15 +19,29 @@ */ package net.draycia.carbon.common.messages; +import java.util.Map; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.moonshine.message.IMessageRenderer; @FunctionalInterface -public interface CarbonMessageRenderer extends IMessageRenderer { +public interface CarbonMessageRenderer extends IMessageRenderer { - default IMessageRenderer asSourced() { + static void addResolved(final TagResolver.Builder tagResolver, final Map resolvedPlaceholders) { + for (final var entry : resolvedPlaceholders.entrySet()) { + if (entry.getValue() instanceof Tag tag) { + tagResolver.tag(entry.getKey(), tag); + } else if (entry.getValue() instanceof TagResolver resolver) { + tagResolver.resolver(resolver); + } else { + throw new IllegalArgumentException(entry.getValue().toString()); + } + } + } + + default IMessageRenderer asSourced() { return this::render; } diff --git a/common/src/main/java/net/draycia/carbon/common/messages/CarbonMessages.java b/common/src/main/java/net/draycia/carbon/common/messages/CarbonMessages.java index e08afb816..f036385ab 100644 --- a/common/src/main/java/net/draycia/carbon/common/messages/CarbonMessages.java +++ b/common/src/main/java/net/draycia/carbon/common/messages/CarbonMessages.java @@ -318,7 +318,6 @@ void errorCommandCommandExecution( @Message("command.ignorelist.pagination_header") Component commandIgnoreListPaginationHeader(int page, int pages); - // todo make username placeholder work in click event commands (change placeholder type to TagResolver) @Message("command.ignorelist.pagination_element") Component commandIgnoreListPaginationElement(Component displayName, String username); @@ -427,6 +426,87 @@ void errorCommandCommandExecution( @Message("command.updateusername.updated") void usernameUpdated(final Audience audience, @Placeholder("newname") final String newName); + @Message("command.party.pagination_header") + Component commandPartyPaginationHeader(Component partyName); + + @Message("command.party.pagination_element") + Component commandPartyPaginationElement(Component displayName, String username, Option online); + + @Message("command.party.created") + void partyCreated(Audience audience, Component partyName); + + @Message("command.party.not_in_party") + void notInParty(Audience audience); + + @Message("command.party.current_party") + void currentParty(Audience audience, Component partyName); + + @Message("command.party.must_leave_current_first") + void mustLeavePartyFirst(Audience audience); + + @Message("command.party.name_too_long") + void partyNameTooLong(Audience audience); + + @Message("command.party.received_invite") + void receivedPartyInvite(Audience audience, Component senderDisplayName, Component partyName); + + @Message("command.party.sent_invite") + void sentPartyInvite(Audience audience, Component recipientDisplayName, Component partyName); + + @Message("command.party.must_specify_invite") + void mustSpecifyPartyInvite(Audience audience); + + @Message("command.party.no_pending_invites") + void noPendingPartyInvites(Audience audience); + + @Message("command.party.no_invite_from") + void noPartyInviteFrom(Audience audience, Component senderDisplayName); + + @Message("command.party.joined_party") + void joinedParty(Audience audience, Component partyName); + + @Message("command.party.left_party") + void leftParty(Audience audience, Component partyName); + + @Message("command.party.disbanded") + void disbandedParty(Audience audience, Component partyName); + + @Message("command.party.cannot_disband_multiple_members") + void cannotDisbandParty(Audience audience, Component partyName); + + @Message("command.party.must_be_in_party") + void mustBeInParty(Audience audience); + + @Message("command.party.cannot_invite_self") + void cannotInviteSelf(Audience audience); + + @Message("command.party.already_in_party") + void alreadyInParty(Audience audience, Component displayName); + + @Message("command.party.description") + Component partyDesc(); + + @Message("command.party.create.description") + Component partyCreateDesc(); + + @Message("command.party.invite.description") + Component partyInviteDesc(); + + @Message("command.party.accept.description") + Component partyAcceptDesc(); + + @Message("command.party.leave.description") + Component partyLeaveDesc(); + + @Message("command.party.disband.description") + Component partyDisbandDesc(); + + @Message("party.player_joined") + void playerJoinedParty(Audience audience, Component partyName, Component displayName); + + @Message("party.player_left") + void playerLeftParty(Audience audience, Component partyName, Component displayName); + @Message("deletemessage.prefix") Component deleteMessagePrefix(); diff --git a/common/src/main/java/net/draycia/carbon/common/messages/Option.java b/common/src/main/java/net/draycia/carbon/common/messages/Option.java new file mode 100644 index 000000000..195c45275 --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/messages/Option.java @@ -0,0 +1,24 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.messages; + +public record Option(boolean value) { + +} diff --git a/common/src/main/java/net/draycia/carbon/common/messages/OptionTagResolver.java b/common/src/main/java/net/draycia/carbon/common/messages/OptionTagResolver.java new file mode 100644 index 000000000..6f220d3b2 --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/messages/OptionTagResolver.java @@ -0,0 +1,57 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.messages; + +import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.ParsingException; +import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.Nullable; + +@DefaultQualifier(NonNull.class) +public final class OptionTagResolver implements TagResolver { + + private final String name; + private final boolean state; + + public OptionTagResolver(final String name, final boolean state) { + this.name = name; + this.state = state; + } + + @Override + public @Nullable Tag resolve(final String name, final ArgumentQueue arguments, final Context ctx) throws ParsingException { + if (!this.has(name)) { + return null; + } + final Tag.Argument t = arguments.popOr("Missing option 1"); + final Tag.Argument f = arguments.popOr("Missing option 2"); + return Tag.preProcessParsed(this.state ? t.value() : f.value()); + } + + @Override + public boolean has(final String name) { + return name.equals(this.name); + } + +} diff --git a/common/src/main/java/net/draycia/carbon/common/messages/placeholders/OptionPlaceholderResolver.java b/common/src/main/java/net/draycia/carbon/common/messages/placeholders/OptionPlaceholderResolver.java new file mode 100644 index 000000000..cae3e17d4 --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/messages/placeholders/OptionPlaceholderResolver.java @@ -0,0 +1,52 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.messages.placeholders; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Map; +import net.draycia.carbon.common.messages.Option; +import net.draycia.carbon.common.messages.OptionTagResolver; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.moonshine.placeholder.ConclusionValue; +import net.kyori.moonshine.placeholder.ContinuanceValue; +import net.kyori.moonshine.placeholder.IPlaceholderResolver; +import net.kyori.moonshine.util.Either; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class OptionPlaceholderResolver implements IPlaceholderResolver { + + @Override + public Map, ContinuanceValue>> resolve( + final String placeholderName, + final Option value, + final R receiver, + final Type owner, + final Method method, + final @Nullable Object[] parameters + ) { + if (value == null) { + throw new IllegalArgumentException(); + } + + return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(new OptionTagResolver(placeholderName, value.value())))); + } + +} diff --git a/common/src/main/java/net/draycia/carbon/common/messaging/CarbonChatPacketHandler.java b/common/src/main/java/net/draycia/carbon/common/messaging/CarbonChatPacketHandler.java index b61a17efd..fa6b0915c 100644 --- a/common/src/main/java/net/draycia/carbon/common/messaging/CarbonChatPacketHandler.java +++ b/common/src/main/java/net/draycia/carbon/common/messaging/CarbonChatPacketHandler.java @@ -31,11 +31,15 @@ import net.draycia.carbon.common.command.commands.WhisperCommand; import net.draycia.carbon.common.event.events.CarbonChatEventImpl; import net.draycia.carbon.common.messaging.packets.ChatMessagePacket; +import net.draycia.carbon.common.messaging.packets.InvalidatePartyInvitePacket; import net.draycia.carbon.common.messaging.packets.LocalPlayerChangePacket; import net.draycia.carbon.common.messaging.packets.LocalPlayersPacket; +import net.draycia.carbon.common.messaging.packets.PartyChangePacket; +import net.draycia.carbon.common.messaging.packets.PartyInvitePacket; import net.draycia.carbon.common.messaging.packets.SaveCompletedPacket; import net.draycia.carbon.common.messaging.packets.WhisperPacket; import net.draycia.carbon.common.users.NetworkUsers; +import net.draycia.carbon.common.users.PartyInvites; import net.draycia.carbon.common.users.UserManagerInternal; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; @@ -44,7 +48,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; -import org.jetbrains.annotations.NotNull; @DefaultQualifier(NonNull.class) public final class CarbonChatPacketHandler extends AbstractMessagingHandler { @@ -55,13 +58,15 @@ public final class CarbonChatPacketHandler extends AbstractMessagingHandler { private final UserManagerInternal userManager; private final NetworkUsers networkUsers; private final WhisperCommand.WhisperHandler whisper; + private final PartyInvites partyInvites; CarbonChatPacketHandler( final CarbonChat carbonChat, final MessagingManager messagingManager, final UserManagerInternal userManager, final NetworkUsers networkUsers, - final WhisperCommand.WhisperHandler whisper + final WhisperCommand.WhisperHandler whisper, + final PartyInvites partyInvites ) { super(messagingManager.requirePacketService()); this.events = carbonChat.eventHandler(); @@ -70,13 +75,23 @@ public final class CarbonChatPacketHandler extends AbstractMessagingHandler { this.userManager = userManager; this.networkUsers = networkUsers; this.whisper = whisper; + this.partyInvites = partyInvites; } @Override - protected boolean handlePacket(final @NotNull Packet packet) { + protected boolean handlePacket(final Packet packet) { if (packet instanceof SaveCompletedPacket statePacket) { this.userManager.saveCompleteMessageReceived(statePacket.playerId()); return true; + } else if (packet instanceof PartyChangePacket pkt) { + this.userManager.partyChangeMessageReceived(pkt); + return true; + } else if (packet instanceof PartyInvitePacket pkt) { + this.partyInvites.handle(pkt); + return true; + } else if (packet instanceof InvalidatePartyInvitePacket pkt) { + this.partyInvites.handle(pkt); + return true; } else if (packet instanceof ChatMessagePacket messagePacket) { return this.handleMessagePacket(messagePacket); } else if (packet instanceof LocalPlayersPacket playersPacket) { diff --git a/common/src/main/java/net/draycia/carbon/common/messaging/MessagingManager.java b/common/src/main/java/net/draycia/carbon/common/messaging/MessagingManager.java index 5b299bb71..2b5dd8448 100644 --- a/common/src/main/java/net/draycia/carbon/common/messaging/MessagingManager.java +++ b/common/src/main/java/net/draycia/carbon/common/messaging/MessagingManager.java @@ -40,12 +40,16 @@ import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.config.MessagingSettings; import net.draycia.carbon.common.messaging.packets.ChatMessagePacket; +import net.draycia.carbon.common.messaging.packets.InvalidatePartyInvitePacket; import net.draycia.carbon.common.messaging.packets.LocalPlayerChangePacket; import net.draycia.carbon.common.messaging.packets.LocalPlayersPacket; import net.draycia.carbon.common.messaging.packets.PacketFactory; +import net.draycia.carbon.common.messaging.packets.PartyChangePacket; +import net.draycia.carbon.common.messaging.packets.PartyInvitePacket; import net.draycia.carbon.common.messaging.packets.SaveCompletedPacket; import net.draycia.carbon.common.messaging.packets.WhisperPacket; import net.draycia.carbon.common.users.NetworkUsers; +import net.draycia.carbon.common.users.PartyInvites; import net.draycia.carbon.common.users.UserManagerInternal; import net.draycia.carbon.common.util.ConcurrentUtil; import net.draycia.carbon.common.util.ExceptionLoggingScheduledThreadPoolExecutor; @@ -95,7 +99,8 @@ public MessagingManager( final UserManagerInternal userManager, final NetworkUsers networkUsers, final WhisperCommand.WhisperHandler whisper, - final PacketFactory packetFactory + final PacketFactory packetFactory, + final PartyInvites partyInvites ) { this.serverId = serverId; this.logger = logger; @@ -124,6 +129,9 @@ public MessagingManager( PacketManager.register(LocalPlayersPacket.class, LocalPlayersPacket::new); PacketManager.register(LocalPlayerChangePacket.class, LocalPlayerChangePacket::new); PacketManager.register(WhisperPacket.class, WhisperPacket::new); + PacketManager.register(PartyChangePacket.class, PartyChangePacket::new); + PacketManager.register(PartyInvitePacket.class, PartyInvitePacket::new); + PacketManager.register(InvalidatePartyInvitePacket.class, InvalidatePartyInvitePacket::new); this.packetService = new PacketService(4, false, protocolVersion); this.scheduledExecutor = new ExceptionLoggingScheduledThreadPoolExecutor(4, @@ -131,7 +139,7 @@ public MessagingManager( final MessagingHandlerImpl handlerImpl = new MessagingHandlerImpl(this.packetService); handlerImpl.addHandler(new CarbonServerHandler(server, serverId, this.packetService, handlerImpl, packetFactory)); - handlerImpl.addHandler(new CarbonChatPacketHandler(carbonChat, this, userManager, networkUsers, whisper)); + handlerImpl.addHandler(new CarbonChatPacketHandler(carbonChat, this, userManager, networkUsers, whisper, partyInvites)); try { this.messagingService = this.initMessagingService( diff --git a/common/src/main/java/net/draycia/carbon/common/messaging/packets/CarbonPacket.java b/common/src/main/java/net/draycia/carbon/common/messaging/packets/CarbonPacket.java index fa998cb81..0bf4330c6 100644 --- a/common/src/main/java/net/draycia/carbon/common/messaging/packets/CarbonPacket.java +++ b/common/src/main/java/net/draycia/carbon/common/messaging/packets/CarbonPacket.java @@ -87,4 +87,12 @@ protected final Map readMap( return map; } + protected final > void writeEnum(final E value, final ByteBuf buf) { + this.writeString(value.name(), buf); + } + + protected final > E readEnum(final ByteBuf buf, final Class cls) { + return Enum.valueOf(cls, this.readString(buf)); + } + } diff --git a/common/src/main/java/net/draycia/carbon/common/messaging/packets/InvalidatePartyInvitePacket.java b/common/src/main/java/net/draycia/carbon/common/messaging/packets/InvalidatePartyInvitePacket.java new file mode 100644 index 000000000..dbf14086a --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/messaging/packets/InvalidatePartyInvitePacket.java @@ -0,0 +1,73 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.messaging.packets; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import io.netty.buffer.ByteBuf; +import java.util.UUID; +import net.draycia.carbon.common.messaging.ServerId; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public final class InvalidatePartyInvitePacket extends CarbonPacket { + + private @MonotonicNonNull UUID from; + private @MonotonicNonNull UUID to; + + @AssistedInject + public InvalidatePartyInvitePacket( + final @ServerId UUID serverId, + final @Assisted("from") UUID from, + final @Assisted("to") UUID to + ) { + super(serverId); + this.from = from; + this.to = to; + } + + public InvalidatePartyInvitePacket(final UUID sender, final ByteBuf data) { + super(sender); + this.read(data); + } + + public UUID from() { + return this.from; + } + + public UUID to() { + return this.to; + } + + @Override + public void read(final ByteBuf buffer) { + this.from = this.readUUID(buffer); + this.to = this.readUUID(buffer); + } + + @Override + public void write(final ByteBuf buffer) { + this.writeUUID(this.from, buffer); + this.writeUUID(this.to, buffer); + } + +} diff --git a/common/src/main/java/net/draycia/carbon/common/messaging/packets/PacketFactory.java b/common/src/main/java/net/draycia/carbon/common/messaging/packets/PacketFactory.java index 1396ced08..88b1373b0 100644 --- a/common/src/main/java/net/draycia/carbon/common/messaging/packets/PacketFactory.java +++ b/common/src/main/java/net/draycia/carbon/common/messaging/packets/PacketFactory.java @@ -22,6 +22,7 @@ import com.google.inject.assistedinject.Assisted; import java.util.Map; import java.util.UUID; +import net.draycia.carbon.common.users.PartyImpl; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -50,4 +51,10 @@ default LocalPlayerChangePacket removeLocalPlayerPacket(final UUID id) { WhisperPacket whisperPacket(@Assisted("from") UUID from, @Assisted("to") UUID to, Component msg); + PartyChangePacket partyChange(UUID partyId, Map changes); + + PartyInvitePacket partyInvite(@Assisted("from") UUID from, @Assisted("to") UUID to, @Assisted("party") UUID party); + + InvalidatePartyInvitePacket invalidatePartyInvite(@Assisted("from") UUID from, @Assisted("to") UUID to); + } diff --git a/common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyChangePacket.java b/common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyChangePacket.java new file mode 100644 index 000000000..7f216e43f --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyChangePacket.java @@ -0,0 +1,84 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.messaging.packets; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import io.netty.buffer.ByteBuf; +import java.util.Map; +import java.util.UUID; +import net.draycia.carbon.common.messaging.ServerId; +import net.draycia.carbon.common.users.PartyImpl; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public final class PartyChangePacket extends CarbonPacket { + + private @MonotonicNonNull UUID partyId; + private @MonotonicNonNull Map changes; + + @AssistedInject + public PartyChangePacket( + final @ServerId UUID serverId, + final @Assisted UUID partyId, + final @Assisted Map changes + ) { + super(serverId); + this.partyId = partyId; + this.changes = changes; + } + + public PartyChangePacket(final UUID sender, final ByteBuf data) { + super(sender); + this.read(data); + } + + public UUID partyId() { + return this.partyId; + } + + public Map changes() { + return this.changes; + } + + @Override + public void read(final ByteBuf buffer) { + this.partyId = this.readUUID(buffer); + this.changes = this.readMap(buffer, this::readUUID, buf -> this.readEnum(buf, PartyImpl.ChangeType.class)); + } + + @Override + public void write(final ByteBuf buffer) { + this.writeUUID(this.partyId, buffer); + this.writeMap(this.changes, this::writeUUID, this::writeEnum, buffer); + } + + @Override + public String toString() { + return "PartyChangePacket{" + + "partyId=" + this.partyId + + ", changes=" + this.changes + + ", sender=" + this.sender + + '}'; + } + +} diff --git a/common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyInvitePacket.java b/common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyInvitePacket.java new file mode 100644 index 000000000..1c7bc69ae --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyInvitePacket.java @@ -0,0 +1,92 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.messaging.packets; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import io.netty.buffer.ByteBuf; +import java.util.UUID; +import net.draycia.carbon.common.messaging.ServerId; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public final class PartyInvitePacket extends CarbonPacket { + + private @MonotonicNonNull UUID from; + private @MonotonicNonNull UUID to; + private @MonotonicNonNull UUID party; + + @AssistedInject + public PartyInvitePacket( + final @ServerId UUID serverId, + final @Assisted("from") UUID from, + final @Assisted("to") UUID to, + final @Assisted("party") UUID party + ) { + super(serverId); + this.from = from; + this.to = to; + this.party = party; + } + + public PartyInvitePacket(final UUID sender, final ByteBuf data) { + super(sender); + this.read(data); + } + + public UUID from() { + return this.from; + } + + public UUID to() { + return this.to; + } + + public UUID party() { + return this.party; + } + + @Override + public void read(final ByteBuf buffer) { + this.from = this.readUUID(buffer); + this.to = this.readUUID(buffer); + this.party = this.readUUID(buffer); + } + + @Override + public void write(final ByteBuf buffer) { + this.writeUUID(this.from, buffer); + this.writeUUID(this.to, buffer); + this.writeUUID(this.party, buffer); + } + + @Override + public String toString() { + return "PartyInvitePacket{" + + "from=" + this.from + + ", to=" + this.to + + ", party=" + this.party + + ", sender=" + this.sender + + '}'; + } + +} diff --git a/common/src/main/java/net/draycia/carbon/common/users/CachingUserManager.java b/common/src/main/java/net/draycia/carbon/common/users/CachingUserManager.java index 9118a7f48..19f685c49 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/CachingUserManager.java +++ b/common/src/main/java/net/draycia/carbon/common/users/CachingUserManager.java @@ -19,8 +19,11 @@ */ package net.draycia.carbon.common.users; -import com.google.inject.MembersInjector; +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.inject.Injector; import com.google.inject.Provider; +import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,10 +35,15 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.stream.Collectors; +import net.draycia.carbon.api.CarbonServer; +import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.messaging.packets.PacketFactory; +import net.draycia.carbon.common.messaging.packets.PartyChangePacket; import net.draycia.carbon.common.users.db.DatabaseUserManager; import net.draycia.carbon.common.util.ConcurrentUtil; +import net.kyori.adventure.text.Component; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -49,25 +57,32 @@ public abstract class CachingUserManager implements UserManagerInternal playerInjector; + private final Injector injector; private final Provider messagingManager; private final PacketFactory packetFactory; + private final CarbonServer server; private final ReentrantLock cacheLock; private final Map> cache; + private final AsyncCache partyCache; protected CachingUserManager( final Logger logger, final ProfileResolver profileResolver, - final MembersInjector playerInjector, + final Injector injector, final Provider messagingManager, - final PacketFactory packetFactory + final PacketFactory packetFactory, + final CarbonServer server ) { this.logger = logger; this.executor = Executors.newSingleThreadExecutor(ConcurrentUtil.carbonThreadFactory(logger, this.getClass().getSimpleName())); + this.partyCache = Caffeine.newBuilder() + .expireAfterAccess(Duration.ofMinutes(5)) + .buildAsync(); this.profileResolver = profileResolver; - this.playerInjector = playerInjector; + this.injector = injector; this.messagingManager = messagingManager; this.packetFactory = packetFactory; + this.server = server; this.cacheLock = new ReentrantLock(); this.cache = new HashMap<>(); } @@ -76,6 +91,12 @@ protected CachingUserManager( protected abstract void saveSync(CarbonPlayerCommon player); + protected abstract @Nullable PartyImpl loadParty(UUID uuid); + + protected abstract void saveSync(PartyImpl info, Map polledChanges); + + protected abstract void disbandSync(UUID id); + private CompletableFuture save(final CarbonPlayerCommon player) { return CompletableFuture.runAsync(() -> { this.saveSync(player); @@ -87,6 +108,11 @@ private CompletableFuture save(final CarbonPlayerCommon player) { }, this.executor); } + @Override + public Party createParty(final Component name) { + throw new UnsupportedOperationException(); + } + @Override public void saveCompleteMessageReceived(final UUID playerId) { this.cacheLock.lock(); @@ -112,7 +138,7 @@ public CompletableFuture user(final UUID uuid) { return this.cache.computeIfAbsent(uuid, $ -> { final CompletableFuture future = CompletableFuture.supplyAsync(() -> { final CarbonPlayerCommon player = this.loadOrCreate(uuid); - this.playerInjector.injectMembers(player); + this.injector.injectMembers(player); if (this instanceof DatabaseUserManager) { player.registerPropertyUpdateListener(() -> this.save(player).exceptionally(saveExceptionHandler(this.logger, player.username, uuid))); @@ -198,4 +224,67 @@ private void attachPostLoad(final UUID uuid, final CompletableFuture party(final UUID id) { + return this.partyCache.get(id, (uuid, cacheExecutor) -> CompletableFuture.supplyAsync(() -> { + final @Nullable PartyImpl party = this.loadParty(uuid); + if (party != null) { + this.injector.injectMembers(party); + } + return party; + }, this.executor)); + } + + @Override + public CompletableFuture saveParty(final PartyImpl info) { + return CompletableFuture.runAsync(() -> { + final Map changes = info.pollChanges(); + if (changes.isEmpty()) { + return; + } + this.saveSync(info, changes); + this.messagingManager.get().withPacketService(service -> { + service.queuePacket(this.packetFactory.partyChange(info.id(), changes)); + service.flushQueue(); + }); + }, this.executor); + } + + @Override + public final void disbandParty(final UUID id) { + this.partyCache.synchronous().invalidate(id); + this.executor.execute(() -> this.disbandSync(id)); + } + + @Override + public void partyChangeMessageReceived(final PartyChangePacket pkt) { + @Nullable CompletableFuture<@Nullable Party> future = this.partyCache.getIfPresent(pkt.partyId()); + if (future == null) { + // we want to notify any online members even if the party isn't loaded locally yet + for (final CarbonPlayer player : this.server.players()) { + if (pkt.partyId().equals(((WrappedCarbonPlayer) player).partyId())) { + future = this.party(pkt.partyId()); + } + } + } + if (future == null) { + return; + } + future.thenAccept(party -> { + if (party == null) { + return; + } + final PartyImpl impl = (PartyImpl) party; + pkt.changes().forEach((id, type) -> { + switch (type) { + case ADD -> impl.addMemberRaw(id); + case REMOVE -> impl.removeMemberRaw(id); + } + }); + }).whenComplete(($, thr) -> { + if (thr != null) { + this.logger.warn("Exception handling party change packet {}", pkt, thr); + } + }); + } } diff --git a/common/src/main/java/net/draycia/carbon/common/users/CarbonPlayerCommon.java b/common/src/main/java/net/draycia/carbon/common/users/CarbonPlayerCommon.java index 5e6df2446..30b8098ee 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/CarbonPlayerCommon.java +++ b/common/src/main/java/net/draycia/carbon/common/users/CarbonPlayerCommon.java @@ -28,10 +28,12 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; import net.draycia.carbon.api.util.InventorySlot; import net.draycia.carbon.common.PlatformScheduler; import net.draycia.carbon.common.config.ConfigManager; @@ -56,6 +58,7 @@ public class CarbonPlayerCommon implements CarbonPlayer, ForwardingAudience.Sing private transient @MonotonicNonNull @Inject PlatformScheduler scheduler; private transient @MonotonicNonNull @Inject ConfigManager config; private transient @MonotonicNonNull @Inject CarbonMessageRenderer messageRenderer; + private transient @MonotonicNonNull @Inject UserManagerInternal users; private volatile transient long transientLoadedSince = -1; protected final PersistentUserProperty muted; @@ -82,6 +85,8 @@ public class CarbonPlayerCommon implements CarbonPlayer, ForwardingAudience.Sing protected final PersistentUserProperty> leftChannels; + protected final PersistentUserProperty party; + public CarbonPlayerCommon( final boolean muted, final boolean deafened, @@ -92,7 +97,8 @@ public CarbonPlayerCommon( final @Nullable UUID lastWhisperTarget, final @Nullable UUID whisperReplyTarget, final boolean spying, - final boolean ignoreDirectMessages + final boolean ignoreDirectMessages, + final @Nullable UUID party ) { this.muted = PersistentUserProperty.of(muted); this.deafened = PersistentUserProperty.of(deafened); @@ -106,6 +112,7 @@ public CarbonPlayerCommon( this.ignoredPlayers = PersistentUserProperty.of(Collections.emptySet()); this.leftChannels = PersistentUserProperty.of(Collections.emptySet()); this.ignoringDirectMessages = PersistentUserProperty.of(ignoreDirectMessages); + this.party = PersistentUserProperty.of(party); } public CarbonPlayerCommon( @@ -122,6 +129,7 @@ public CarbonPlayerCommon( this.username = username; this.uuid = uuid; this.ignoringDirectMessages = PersistentUserProperty.of(false); + this.party = PersistentUserProperty.empty(); } public CarbonPlayerCommon() { @@ -133,6 +141,7 @@ public CarbonPlayerCommon() { this.ignoredPlayers = PersistentUserProperty.of(Collections.emptySet()); this.leftChannels = PersistentUserProperty.of(Collections.emptySet()); this.ignoringDirectMessages = PersistentUserProperty.of(false); + this.party = PersistentUserProperty.empty(); } public boolean needsSave() { @@ -148,7 +157,8 @@ private Stream> properties() { this.spying, this.ignoredPlayers, this.leftChannels, - this.ignoringDirectMessages + this.ignoringDirectMessages, + this.party ); } @@ -476,4 +486,20 @@ public void saved() { this.properties().forEach(PersistentUserProperty::saved); } + public @Nullable UUID partyId() { + return this.party.orNull(); + } + + @Override + public CompletableFuture<@Nullable Party> party() { + final @Nullable UUID id = this.party.orNull(); + if (id == null) { + return CompletableFuture.completedFuture(null); + } + return this.users.party(id); + } + + public void party(final @Nullable Party party) { + this.party.set(party == null ? null : party.id()); + } } diff --git a/common/src/main/java/net/draycia/carbon/common/users/ConsoleCarbonPlayer.java b/common/src/main/java/net/draycia/carbon/common/users/ConsoleCarbonPlayer.java index 24a3dad48..2a2f83727 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/ConsoleCarbonPlayer.java +++ b/common/src/main/java/net/draycia/carbon/common/users/ConsoleCarbonPlayer.java @@ -24,8 +24,10 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; import net.draycia.carbon.api.util.InventorySlot; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; @@ -246,6 +248,11 @@ public void leaveChannel(final ChatChannel channel) { } + @Override + public CompletableFuture<@Nullable Party> party() { + return CompletableFuture.completedFuture(null); + } + @Override public @NotNull Identity identity() { return Identity.nil(); diff --git a/common/src/main/java/net/draycia/carbon/common/users/NetworkUsers.java b/common/src/main/java/net/draycia/carbon/common/users/NetworkUsers.java index 5286d3042..a9dbd8973 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/NetworkUsers.java +++ b/common/src/main/java/net/draycia/carbon/common/users/NetworkUsers.java @@ -41,6 +41,7 @@ import net.draycia.carbon.common.messaging.packets.LocalPlayersPacket; import net.draycia.carbon.common.util.Exceptions; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; /** @@ -138,4 +139,12 @@ public boolean online(final CarbonPlayer player) { return this.map.values().stream().anyMatch(server -> server.containsKey(player.uuid())); } + public boolean online(final UUID uuid) { + final @Nullable CarbonPlayer player = this.server.players().stream() + .filter(it -> it.uuid().equals(uuid)) + .findFirst() + .orElse(null); + return player != null || this.map.values().stream().anyMatch(server -> server.containsKey(uuid)); + } + } diff --git a/common/src/main/java/net/draycia/carbon/common/users/PartyImpl.java b/common/src/main/java/net/draycia/carbon/common/users/PartyImpl.java new file mode 100644 index 000000000..a03fcdd80 --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/users/PartyImpl.java @@ -0,0 +1,272 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.users; + +import com.google.common.base.Suppliers; +import com.google.inject.Inject; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import net.draycia.carbon.api.CarbonServer; +import net.draycia.carbon.api.event.CarbonEventHandler; +import net.draycia.carbon.api.event.events.PartyJoinEvent; +import net.draycia.carbon.api.event.events.PartyLeaveEvent; +import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; +import net.draycia.carbon.common.messages.CarbonMessages; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public final class PartyImpl implements Party { + + private final Component name; + private final UUID id; + private final Set members; + private transient final @Nullable String serializedName; + private transient volatile @MonotonicNonNull Map changes; + private transient @MonotonicNonNull @Inject UserManagerInternal userManager; + private transient @MonotonicNonNull @Inject CarbonServer server; + private transient @MonotonicNonNull @Inject Logger logger; + private transient @MonotonicNonNull @Inject CarbonEventHandler events; + private transient @MonotonicNonNull @Inject CarbonMessages messages; + private transient volatile boolean disbanded = false; + + private PartyImpl( + final Component name, + final UUID id + ) { + this.serializedName = GsonComponentSerializer.gson().serialize(name); + if (this.serializedName.toCharArray().length > 8192) { + throw new IllegalArgumentException("Serialized party name is too long: '%s', %s > 8192".formatted(name, this.serializedName.toCharArray().length)); + } + this.name = name; + this.id = id; + this.members = ConcurrentHashMap.newKeySet(); + this.changes = new ConcurrentHashMap<>(); + } + + public static PartyImpl create(final Component name) { + return create(name, UUID.randomUUID()); + } + + public static PartyImpl create(final Component name, final UUID id) { + return new PartyImpl(name, id); + } + + private Map changes() { + if (this.changes == null) { + synchronized (this) { + if (this.changes == null) { + this.changes = new ConcurrentHashMap<>(); + } + } + } + return this.changes; + } + + @Override + public void addMember(final UUID id) { + if (this.disbanded) { + throw new IllegalStateException("This party was disbanded."); + } + this.changes().put(id, ChangeType.ADD); + this.addMemberRaw(id); + final BiConsumer exceptionHandler = ($, thr) -> { + if (thr != null) { + this.logger.warn("Exception adding member {} to group {}", id, this.id(), thr); + } + }; + this.userManager.saveParty(this).whenComplete(exceptionHandler); + this.userManager.user(id).thenCompose(user -> { + final WrappedCarbonPlayer wrapped = (WrappedCarbonPlayer) user; + final @Nullable UUID oldPartyId = wrapped.partyId(); + wrapped.party(this); + if (oldPartyId != null) { + return this.userManager.party(oldPartyId).thenAccept(old -> { + if (old != null) { + old.removeMember(user.uuid()); + } + }); + } + return CompletableFuture.completedFuture(null); + }).whenComplete(exceptionHandler); + } + + @Override + public void removeMember(final UUID id) { + if (this.disbanded) { + throw new IllegalStateException("This party was disbanded."); + } + this.changes().put(id, ChangeType.REMOVE); + this.removeMemberRaw(id); + final BiConsumer exceptionHandler = ($, thr) -> { + if (thr != null) { + this.logger.warn("Exception removing member {} from group {}", id, this.id(), thr); + } + }; + this.userManager.saveParty(this).whenComplete(exceptionHandler); + this.userManager.user(id).thenAccept(user -> { + final WrappedCarbonPlayer wrapped = (WrappedCarbonPlayer) user; + if (Objects.equals(wrapped.partyId(), this.id)) { + wrapped.party(null); + } + }).whenComplete(exceptionHandler); + } + + @Override + public Set members() { + if (this.disbanded) { + throw new IllegalStateException("This party was disbanded."); + } + return Set.copyOf(this.members); + } + + @Override + public void disband() { + if (this.disbanded) { + throw new IllegalStateException("This party is already disbanded."); + } + this.server.players().stream().filter(p -> this.members.contains(p.uuid())).forEach(p -> ((WrappedCarbonPlayer) p).party(null)); + this.userManager.disbandParty(this.id); + this.disbanded = true; + } + + public Set rawMembers() { + return this.members; + } + + public void addMemberRaw(final UUID id) { + this.members.add(id); + + this.events.emit(new PartyJoinEvent() { + + @Override + public UUID playerId() { + return id; + } + + @Override + public Party party() { + return PartyImpl.this; + } + }); + + this.notifyJoin(id); + } + + public void removeMemberRaw(final UUID id) { + this.members.remove(id); + + this.events.emit(new PartyLeaveEvent() { + + @Override + public UUID playerId() { + return id; + } + + @Override + public Party party() { + return PartyImpl.this; + } + }); + + this.notifyLeave(id); + } + + public Map pollChanges() { + final Map ret = Map.copyOf(this.changes()); + ret.forEach((id, t) -> this.changes().remove(id)); + return ret; + } + + @Override + public Component name() { + return this.name; + } + + public String serializedName() { + return Objects.requireNonNullElseGet(this.serializedName, () -> GsonComponentSerializer.gson().serialize(this.name)); + } + + @Override + public UUID id() { + return this.id; + } + + private void notifyJoin(final UUID joined) { + this.notifyMembersChanged(joined, (p, party, member) -> { + this.messages.playerJoinedParty(member, party.name(), p.displayName()); + }); + } + + private void notifyLeave(final UUID left) { + this.notifyMembersChanged(left, (p, party, member) -> { + this.messages.playerLeftParty(member, party.name(), p.displayName()); + }); + } + + private void notifyMembersChanged(final UUID changed, final ChangeNotifier notify) { + final Supplier> changedPlayer = Suppliers.memoize(() -> this.userManager.user(changed)); + for (final CarbonPlayer player : this.server.players()) { + if (player.uuid().equals(changed)) { + continue; + } + if (this.members.contains(player.uuid())) { + changedPlayer.get().thenAccept(p -> { + notify.notify(p, this, player); + }).whenComplete(($, thr) -> { + this.logger.warn("Exception notifying members of party change", thr); + }); + } + } + } + + @FunctionalInterface + private interface ChangeNotifier { + + void notify(CarbonPlayer changed, Party party, CarbonPlayer member); + + } + + @Override + public String toString() { + return "PartyImpl[" + + "name=" + this.name + ", " + + "id=" + this.id + ", " + + "members=" + this.members + ", " + + "changes=" + this.changes + ']'; + } + + public enum ChangeType { + ADD, REMOVE + } + +} diff --git a/common/src/main/java/net/draycia/carbon/common/users/PartyInvites.java b/common/src/main/java/net/draycia/carbon/common/users/PartyInvites.java new file mode 100644 index 000000000..41ac8de81 --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/users/PartyInvites.java @@ -0,0 +1,138 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.users; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.time.Duration; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; +import net.draycia.carbon.common.messages.CarbonMessages; +import net.draycia.carbon.common.messaging.MessagingManager; +import net.draycia.carbon.common.messaging.packets.InvalidatePartyInvitePacket; +import net.draycia.carbon.common.messaging.packets.PacketFactory; +import net.draycia.carbon.common.messaging.packets.PartyInvitePacket; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +@Singleton +public final class PartyInvites { + + private final Map> pendingInvites = new ConcurrentHashMap<>(); + private final Provider messaging; + private final PacketFactory packetFactory; + private final UserManagerInternal users; + private final Logger logger; + private final CarbonMessages messages; + + @Inject + private PartyInvites( + final Provider messaging, + final PacketFactory packetFactory, + final UserManagerInternal users, + final Logger logger, + final CarbonMessages messages + ) { + this.messaging = messaging; + this.packetFactory = packetFactory; + this.users = users; + this.logger = logger; + this.messages = messages; + } + + public void sendInvite(final UUID from, final UUID to, final UUID party) { + final Cache cache = this.orCreateInvitesFor(to); + cache.put(from, party); + this.clean(); + + this.messaging.get().withPacketService(service -> { + service.queuePacket(this.packetFactory.partyInvite(from, to, party)); + service.flushQueue(); + }); + } + + public void invalidateInvite(final UUID from, final UUID to) { + this.invalidateInvite_(from, to); + + this.messaging.get().withPacketService(service -> { + service.queuePacket(this.packetFactory.invalidatePartyInvite(from, to)); + service.flushQueue(); + }); + } + + private void invalidateInvite_(final UUID from, final UUID to) { + final @Nullable Cache cache = this.invitesFor(to); + if (cache != null) { + cache.invalidate(from); + } + this.clean(); + } + + public @Nullable Cache invitesFor(final UUID recipient) { + return this.pendingInvites.get(recipient); + } + + private Cache orCreateInvitesFor(final UUID recipient) { + return this.pendingInvites.computeIfAbsent(recipient, $ -> makeCache()); + } + + private static Cache makeCache() { + return Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(30)).build(); + } + + public void handle(final InvalidatePartyInvitePacket pkt) { + this.invalidateInvite_(pkt.from(), pkt.to()); + this.clean(); + } + + private void clean() { + this.pendingInvites.values().removeIf(it -> it.asMap().size() == 0); + } + + public void handle(final PartyInvitePacket pkt) { + final @Nullable Cache cache = this.orCreateInvitesFor(pkt.to()); + cache.put(pkt.from(), pkt.party()); + this.clean(); + + final CompletableFuture to = this.users.user(pkt.to()); + final CompletableFuture from = this.users.user(pkt.to()); + final CompletableFuture party = this.users.party(pkt.party()); + + CompletableFuture.allOf(to, from, party).thenRun(() -> { + if (to.join().online()) { + this.messages.receivedPartyInvite(to.join(), from.join().displayName(), party.join().name()); + } + }).whenComplete(($, thr) -> { + if (thr != null) { + this.logger.warn("Exception handling {}", pkt, thr); + } + }); + } +} diff --git a/common/src/main/java/net/draycia/carbon/common/users/PlatformUserManager.java b/common/src/main/java/net/draycia/carbon/common/users/PlatformUserManager.java index ab5f43bf3..25b0de9c5 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/PlatformUserManager.java +++ b/common/src/main/java/net/draycia/carbon/common/users/PlatformUserManager.java @@ -20,12 +20,17 @@ package net.draycia.carbon.common.users; import com.google.inject.Inject; +import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Singleton; import com.google.inject.assistedinject.FactoryModuleBuilder; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import net.draycia.carbon.api.users.Party; +import net.draycia.carbon.common.messaging.packets.PartyChangePacket; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @Singleton @@ -34,14 +39,17 @@ public final class PlatformUserManager implements UserManagerInternal backingManager; private final PlayerFactory playerFactory; + private final Injector injector; @Inject private PlatformUserManager( final @Backing UserManagerInternal backingManager, - final PlayerFactory playerFactory + final PlayerFactory playerFactory, + final Injector injector ) { this.backingManager = backingManager; this.playerFactory = playerFactory; + this.injector = injector; } @Override @@ -53,6 +61,13 @@ public CompletableFuture user(final UUID uuid) { }); } + @Override + public Party createParty(final Component name) { + final PartyImpl party = PartyImpl.create(name); + this.injector.injectMembers(party); + return party; + } + @Override public void shutdown() { this.backingManager.shutdown(); @@ -78,6 +93,26 @@ public void cleanup() { this.backingManager.cleanup(); } + @Override + public CompletableFuture<@Nullable Party> party(final UUID id) { + return this.backingManager.party(id); + } + + @Override + public CompletableFuture saveParty(final PartyImpl info) { + return this.backingManager.saveParty(info); + } + + @Override + public void disbandParty(final UUID id) { + this.backingManager.disbandParty(id); + } + + @Override + public void partyChangeMessageReceived(final PartyChangePacket pkt) { + this.backingManager.partyChangeMessageReceived(pkt); + } + public interface PlayerFactory { WrappedCarbonPlayer wrap(CarbonPlayerCommon common); diff --git a/common/src/main/java/net/draycia/carbon/common/users/UserManagerInternal.java b/common/src/main/java/net/draycia/carbon/common/users/UserManagerInternal.java index ffb863ceb..321adaff0 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/UserManagerInternal.java +++ b/common/src/main/java/net/draycia/carbon/common/users/UserManagerInternal.java @@ -23,6 +23,7 @@ import java.util.concurrent.CompletableFuture; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; +import net.draycia.carbon.common.messaging.packets.PartyChangePacket; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @@ -39,4 +40,9 @@ public interface UserManagerInternal extends UserManager void cleanup(); + CompletableFuture saveParty(PartyImpl info); + + void disbandParty(UUID id); + + void partyChangeMessageReceived(PartyChangePacket pkt); } diff --git a/common/src/main/java/net/draycia/carbon/common/users/WrappedCarbonPlayer.java b/common/src/main/java/net/draycia/carbon/common/users/WrappedCarbonPlayer.java index 090ec65f2..2d26dad83 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/WrappedCarbonPlayer.java +++ b/common/src/main/java/net/draycia/carbon/common/users/WrappedCarbonPlayer.java @@ -27,9 +27,11 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; import net.draycia.carbon.api.util.InventorySlot; import net.draycia.carbon.common.config.PrimaryConfig; import net.draycia.carbon.common.messages.SourcedAudience; @@ -428,4 +430,17 @@ public int hashCode() { return this.carbonPlayerCommon.hashCode(); } + public @Nullable UUID partyId() { + return this.carbonPlayerCommon.partyId(); + } + + @Override + public CompletableFuture<@Nullable Party> party() { + return this.carbonPlayerCommon.party(); + } + + public void party(final @Nullable Party party) { + this.carbonPlayerCommon.party(party); + } + } diff --git a/common/src/main/java/net/draycia/carbon/common/users/db/DatabaseUserManager.java b/common/src/main/java/net/draycia/carbon/common/users/db/DatabaseUserManager.java index 03e3df0f3..d3be52f6f 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/db/DatabaseUserManager.java +++ b/common/src/main/java/net/draycia/carbon/common/users/db/DatabaseUserManager.java @@ -20,16 +20,18 @@ package net.draycia.carbon.common.users.db; import com.google.inject.Inject; -import com.google.inject.MembersInjector; +import com.google.inject.Injector; import com.google.inject.Provider; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import net.draycia.carbon.api.CarbonChat; +import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.common.config.ConfigManager; @@ -38,11 +40,13 @@ import net.draycia.carbon.common.messaging.packets.PacketFactory; import net.draycia.carbon.common.users.CachingUserManager; import net.draycia.carbon.common.users.CarbonPlayerCommon; +import net.draycia.carbon.common.users.PartyImpl; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.common.users.db.argument.ComponentArgumentFactory; import net.draycia.carbon.common.users.db.argument.KeyArgumentFactory; import net.draycia.carbon.common.users.db.mapper.ComponentColumnMapper; import net.draycia.carbon.common.users.db.mapper.KeyColumnMapper; +import net.draycia.carbon.common.users.db.mapper.PartyRowMapper; import net.draycia.carbon.common.users.db.mapper.PlayerRowMapper; import net.draycia.carbon.common.util.ConcurrentUtil; import net.draycia.carbon.common.util.SQLDrivers; @@ -57,6 +61,7 @@ import org.flywaydb.core.api.logging.Log; import org.flywaydb.core.api.logging.LogCreator; import org.flywaydb.core.api.logging.LogFactory; +import org.jdbi.v3.core.Handle; import org.jdbi.v3.core.Jdbi; import org.jdbi.v3.core.statement.PreparedBatch; import org.jdbi.v3.core.statement.Update; @@ -76,17 +81,19 @@ private DatabaseUserManager( final QueriesLocator locator, final Logger logger, final ProfileResolver profileResolver, - final MembersInjector playerInjector, + final Injector injector, final Provider messagingManager, final PacketFactory packetFactory, - final ChannelRegistry channelRegistry + final ChannelRegistry channelRegistry, + final CarbonServer server ) { super( logger, profileResolver, - playerInjector, + injector, messagingManager, - packetFactory + packetFactory, + server ); this.jdbi = jdbi; this.dataSource = dataSource; @@ -161,6 +168,81 @@ public void saveSync(final CarbonPlayerCommon player) { }); } + @Override + protected @Nullable PartyImpl loadParty(final UUID uuid) { + return this.jdbi.withHandle(handle -> { + final @Nullable PartyImpl party = this.selectParty(handle, uuid); + if (party == null) { + return null; + } + + final List members = handle.createQuery(this.locator.query("select-party-members")) + .bind("partyid", uuid) + .mapTo(UUID.class) + .list(); + + party.rawMembers().addAll(members); + + return party; + }); + } + + private @Nullable PartyImpl selectParty(final Handle handle, final UUID uuid) { + return handle.createQuery(this.locator.query("select-party")) + .bind("partyid", uuid) + .mapTo(PartyImpl.class) + .findOne() + .orElse(null); + } + + @Override + protected void saveSync(final PartyImpl party, final Map changes) { + this.jdbi.useTransaction(handle -> { + final @Nullable PartyImpl existing = this.selectParty(handle, party.id()); + if (existing == null) { + handle.createUpdate(this.locator.query("insert-party")) + .bind("partyid", party.id()) + .bind("name", party.serializedName()) + .execute(); + } + + @Nullable PreparedBatch add = null; + @Nullable PreparedBatch remove = null; + for (final Map.Entry entry : changes.entrySet()) { + final UUID id = entry.getKey(); + final PartyImpl.ChangeType type = entry.getValue(); + switch (type) { + case ADD -> { + if (add == null) { + add = handle.prepareBatch(this.locator.query("insert-party-member")); + } + add.bind("partyid", party.id()).bind("playerid", id).add(); + } + case REMOVE -> { + if (remove == null) { + remove = handle.prepareBatch(this.locator.query("drop-party-member")); + } + remove.bind("playerid", id).add(); + } + } + } + if (add != null) { + add.execute(); + } + if (remove != null) { + remove.execute(); + } + }); + } + + @Override + public void disbandSync(final UUID id) { + this.jdbi.useHandle(handle -> { + handle.createUpdate(this.locator.query("drop-party")).bind("partyid", id).execute(); + handle.createUpdate(this.locator.query("clear-party-members")).bind("partyid", id).execute(); + }); + } + @Override public void shutdown() { super.shutdown(); @@ -170,7 +252,7 @@ public void shutdown() { private Update bindPlayerArguments(final Update update, final CarbonPlayerCommon player) { final @Nullable Component nickname = player.nicknameRaw(); @Nullable String nicknameJson = GsonComponentSerializer.gson().serializeOrNull(nickname); - if (nicknameJson != null && nicknameJson.length() > 8192) { + if (nicknameJson != null && nicknameJson.toCharArray().length > 8192) { this.logger.error("Serialized nickname for player {} was too long ({}>8192), it cannot be saved: {}", player.uuid(), nicknameJson.length(), nicknameJson); nicknameJson = null; } @@ -182,7 +264,8 @@ private Update bindPlayerArguments(final Update update, final CarbonPlayerCommon .bind("lastwhispertarget", player.lastWhisperTarget()) .bind("whisperreplytarget", player.whisperReplyTarget()) .bind("spying", player.spying()) - .bind("ignoringdms", player.ignoringDirectMessages()); + .bind("ignoringdms", player.ignoringDirectMessages()) + .bind("party", player.partyId()); } public static final class Factory { @@ -191,9 +274,10 @@ public static final class Factory { private final ConfigManager configManager; private final Logger logger; private final ProfileResolver profileResolver; - private final MembersInjector playerInjector; + private final Injector injector; private final Provider messagingManager; private final PacketFactory packetFactory; + private final CarbonServer server; @Inject private Factory( @@ -201,17 +285,19 @@ private Factory( final ConfigManager configManager, final Logger logger, final ProfileResolver profileResolver, - final MembersInjector playerInjector, + final Injector injector, final Provider messagingManager, - final PacketFactory packetFactory + final PacketFactory packetFactory, + final CarbonServer server ) { this.channelRegistry = channelRegistry; this.configManager = configManager; this.logger = logger; this.profileResolver = profileResolver; - this.playerInjector = playerInjector; + this.injector = injector; this.messagingManager = messagingManager; this.packetFactory = packetFactory; + this.server = server; } public DatabaseUserManager create(final String migrationsLocation, final Consumer configureJdbi) { @@ -256,6 +342,7 @@ public DatabaseUserManager create(final String migrationsLocation, final Consume .registerArgument(new ComponentArgumentFactory()) .registerArgument(new KeyArgumentFactory()) .registerRowMapper(CarbonPlayerCommon.class, new PlayerRowMapper()) + .registerRowMapper(PartyImpl.class, new PartyRowMapper()) .registerColumnMapper(Key.class, new KeyColumnMapper()) .registerColumnMapper(Component.class, new ComponentColumnMapper()) .installPlugin(new SqlObjectPlugin()); @@ -268,10 +355,11 @@ public DatabaseUserManager create(final String migrationsLocation, final Consume new QueriesLocator(this.configManager.primaryConfig().storageType()), this.logger, this.profileResolver, - this.playerInjector, + this.injector, this.messagingManager, this.packetFactory, - this.channelRegistry + this.channelRegistry, + this.server ); } diff --git a/common/src/main/java/net/draycia/carbon/common/users/db/mapper/PartyRowMapper.java b/common/src/main/java/net/draycia/carbon/common/users/db/mapper/PartyRowMapper.java new file mode 100644 index 000000000..ed0e1070f --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/users/db/mapper/PartyRowMapper.java @@ -0,0 +1,46 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.users.db.mapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; +import net.draycia.carbon.common.users.PartyImpl; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jdbi.v3.core.mapper.ColumnMapper; +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; + +@DefaultQualifier(NonNull.class) +public final class PartyRowMapper implements RowMapper { + + @Override + public PartyImpl map(final ResultSet rs, final StatementContext ctx) throws SQLException { + final ColumnMapper component = ctx.findColumnMapperFor(Component.class).orElseThrow(); + final ColumnMapper uuid = ctx.findColumnMapperFor(UUID.class).orElseThrow(); + return PartyImpl.create( + component.map(rs, "name", ctx), + uuid.map(rs, "partyid", ctx) + ); + } + +} diff --git a/common/src/main/java/net/draycia/carbon/common/users/db/mapper/PlayerRowMapper.java b/common/src/main/java/net/draycia/carbon/common/users/db/mapper/PlayerRowMapper.java index f08dc3025..7356f35e0 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/db/mapper/PlayerRowMapper.java +++ b/common/src/main/java/net/draycia/carbon/common/users/db/mapper/PlayerRowMapper.java @@ -25,10 +25,13 @@ import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; import org.jdbi.v3.core.mapper.ColumnMapper; import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.statement.StatementContext; +@DefaultQualifier(NonNull.class) public final class PlayerRowMapper implements RowMapper { @Override @@ -44,7 +47,8 @@ public CarbonPlayerCommon map(final ResultSet rs, final StatementContext ctx) th uuid.map(rs, "lastwhispertarget", ctx), uuid.map(rs, "whisperreplytarget", ctx), rs.getBoolean("spying"), - rs.getBoolean("ignoringdms") + rs.getBoolean("ignoringdms"), + uuid.map(rs, "party", ctx) ); } diff --git a/common/src/main/java/net/draycia/carbon/common/users/json/JSONUserManager.java b/common/src/main/java/net/draycia/carbon/common/users/json/JSONUserManager.java index a57e21b8b..a90c5efbb 100644 --- a/common/src/main/java/net/draycia/carbon/common/users/json/JSONUserManager.java +++ b/common/src/main/java/net/draycia/carbon/common/users/json/JSONUserManager.java @@ -22,13 +22,15 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.inject.Inject; -import com.google.inject.MembersInjector; +import com.google.inject.Injector; import com.google.inject.Provider; import java.io.IOException; import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import java.util.UUID; +import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.common.DataDirectory; @@ -39,8 +41,10 @@ import net.draycia.carbon.common.serialisation.gson.UUIDSerializerGson; import net.draycia.carbon.common.users.CachingUserManager; import net.draycia.carbon.common.users.CarbonPlayerCommon; +import net.draycia.carbon.common.users.PartyImpl; import net.draycia.carbon.common.users.PersistentUserProperty; import net.draycia.carbon.common.users.ProfileResolver; +import net.draycia.carbon.common.util.Exceptions; import net.draycia.carbon.common.util.FileUtil; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.apache.logging.log4j.Logger; @@ -53,6 +57,7 @@ public class JSONUserManager extends CachingUserManager { private final Gson serializer; private final Path userDirectory; + private final Path partyDirectory; private final ChannelRegistry channelRegistry; @Inject @@ -60,24 +65,28 @@ public JSONUserManager( final @DataDirectory Path dataDirectory, final Logger logger, final ProfileResolver profileResolver, - final MembersInjector playerInjector, + final Injector injector, final ChatChannelSerializerGson channelSerializer, final UUIDSerializerGson uuidSerializer, final Provider messagingManager, final PacketFactory packetFactory, - final CarbonChannelRegistry channelRegistry + final CarbonChannelRegistry channelRegistry, + final CarbonServer server ) throws IOException { super( logger, profileResolver, - playerInjector, + injector, messagingManager, - packetFactory + packetFactory, + server ); this.userDirectory = dataDirectory.resolve("users"); + this.partyDirectory = dataDirectory.resolve("party"); this.channelRegistry = channelRegistry; Files.createDirectories(this.userDirectory); + Files.createDirectories(this.partyDirectory); this.serializer = GsonComponentSerializer.gson().populator() .apply(new GsonBuilder()) @@ -121,6 +130,10 @@ private Path userFile(final UUID id) { return this.userDirectory.resolve(id + ".json"); } + private Path partyFile(final UUID id) { + return this.partyDirectory.resolve(id + ".json"); + } + @Override public void saveSync(final CarbonPlayerCommon player) { final Path userFile = this.userFile(player.uuid()); @@ -138,4 +151,53 @@ public void saveSync(final CarbonPlayerCommon player) { } } + @Override + protected @Nullable PartyImpl loadParty(final UUID uuid) { + final Path partyFile = this.partyFile(uuid); + + if (Files.exists(partyFile)) { + try { + final @Nullable PartyImpl party; + try (final Reader reader = Files.newBufferedReader(partyFile)) { + party = this.serializer.<@Nullable PartyImpl>fromJson(reader, PartyImpl.class); + } + + if (party == null) { + throw new IllegalStateException("Party file found but was empty."); + } + + return party; + } catch (final IOException exception) { + throw new RuntimeException(exception); + } + } + + return null; + } + + @Override + protected void saveSync(final PartyImpl party, final Map changes) { + final Path partyFile = this.partyFile(party.id()); + + try { + final String json = this.serializer.toJson(party); + + if (json == null || json.isBlank()) { + throw new IllegalStateException("No data to save - toJson returned null or blank."); + } + + Files.writeString(FileUtil.mkParentDirs(partyFile), json); + } catch (final IOException exception) { + throw new RuntimeException("Exception while saving data for party " + party, exception); + } + } + + @Override + public void disbandSync(final UUID id) { + try { + Files.deleteIfExists(this.partyFile(id)); + } catch (final IOException ex) { + Exceptions.rethrow(ex); + } + } } diff --git a/common/src/main/java/net/draycia/carbon/common/util/CloudUtils.java b/common/src/main/java/net/draycia/carbon/common/util/CloudUtils.java index 774da264b..fa4a6335d 100644 --- a/common/src/main/java/net/draycia/carbon/common/util/CloudUtils.java +++ b/common/src/main/java/net/draycia/carbon/common/util/CloudUtils.java @@ -51,6 +51,7 @@ import net.draycia.carbon.common.command.commands.MuteCommand; import net.draycia.carbon.common.command.commands.MuteInfoCommand; import net.draycia.carbon.common.command.commands.NicknameCommand; +import net.draycia.carbon.common.command.commands.PartyCommands; import net.draycia.carbon.common.command.commands.ReloadCommand; import net.draycia.carbon.common.command.commands.ReplyCommand; import net.draycia.carbon.common.command.commands.ToggleMessagesCommand; @@ -78,7 +79,7 @@ public final class CloudUtils { ContinueCommand.class, DebugCommand.class, HelpCommand.class, IgnoreCommand.class, MuteCommand.class, MuteInfoCommand.class, NicknameCommand.class, ReloadCommand.class, ReplyCommand.class, ToggleMessagesCommand.class, UnignoreCommand.class, UnmuteCommand.class, UpdateUsernameCommand.class, WhisperCommand.class, JoinCommand.class, LeaveCommand.class, - IgnoreListCommand.class); + IgnoreListCommand.class, PartyCommands.class); private static final List CONSTRUCTED_COMMANDS = new ArrayList<>(); diff --git a/common/src/main/java/net/draycia/carbon/common/util/PaginationHelper.java b/common/src/main/java/net/draycia/carbon/common/util/PaginationHelper.java new file mode 100644 index 000000000..3499b58b6 --- /dev/null +++ b/common/src/main/java/net/draycia/carbon/common/util/PaginationHelper.java @@ -0,0 +1,81 @@ +/* + * CarbonChat + * + * Copyright (c) 2023 Josua Parks (Vicarious) + * Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.draycia.carbon.common.util; + +import com.google.inject.Inject; +import java.util.function.IntFunction; +import net.draycia.carbon.common.messages.CarbonMessages; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.TextComponent; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +import static net.kyori.adventure.text.Component.empty; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.event.ClickEvent.runCommand; + +@DefaultQualifier(NonNull.class) +public final class PaginationHelper { + + private final CarbonMessages messages; + + @Inject + private PaginationHelper(final CarbonMessages messages) { + this.messages = messages; + } + + public Pagination.BiIntFunction footerRenderer(final IntFunction commandFunction) { + return (currentPage, pages) -> { + if (pages == 1) { + return empty(); // we don't need to see 'Page 1/1' + } + final TextComponent.Builder buttons = text(); + if (currentPage > 1) { + buttons.append(this.previousPageButton(currentPage, commandFunction)); + } + if (currentPage > 1 && currentPage < pages) { + buttons.append(space()); + } + if (currentPage < pages) { + buttons.append(this.nextPageButton(currentPage, commandFunction)); + } + return this.messages.paginationFooter(currentPage, pages, buttons.build()); + }; + } + + private Component previousPageButton(final int currentPage, final IntFunction commandFunction) { + return text() + .content("←") + .clickEvent(runCommand(commandFunction.apply(currentPage - 1))) + .hoverEvent(this.messages.paginationClickForPreviousPage()) + .build(); + } + + private Component nextPageButton(final int currentPage, final IntFunction commandFunction) { + return text() + .content("→") + .clickEvent(runCommand(commandFunction.apply(currentPage + 1))) + .hoverEvent(this.messages.paginationClickForNextPage()) + .build(); + } + +} diff --git a/common/src/main/resources/locale/messages-en_US.properties b/common/src/main/resources/locale/messages-en_US.properties index 17cab3ae7..a59e7efe0 100644 --- a/common/src/main/resources/locale/messages-en_US.properties +++ b/common/src/main/resources/locale/messages-en_US.properties @@ -61,6 +61,33 @@ command.updateusername.updated=Updated 's username! command.whisper.argument.message=The message to send. command.whisper.argument.player=The name of the player to message. command.whisper.description=Sends a private message to the specified player. +command.party.pagination_header=Party members: +command.party.pagination_element=':''> - +command.party.created=Successfully created and joined party ''! +command.party.not_in_party=You are not in a party. +command.party.current_party=You are in party: +command.party.must_leave_current_first=You must leave your current party first. +command.party.name_too_long=Party name is too long. +command.party.received_invite=You were invited to the party '' by . +command.party.sent_invite=Sent party invite to . +command.party.must_specify_invite=You must specify whose party invite to accept. +command.party.no_pending_invites=You do not have any pending party invites. +command.party.no_invite_from=You do not have a pending invite from . +command.party.joined_party=Successfully joined party ''! +command.party.left_party=Successfully left party ''. +command.party.disbanded=Successfully disbanded party ''. +command.party.cannot_disband_multiple_members=Cannot disband party '', you are not the last member. +command.party.must_be_in_party=You must be in a party to use this command. +command.party.cannot_invite_self=You cannot invite yourself. +command.party.description=Get info about and see members of your current party. +command.party.create.description=Create a new party. +command.party.invite.description=Invite a player to your party. +command.party.accept.description=Accept party invites. +command.party.leave.description=Leave your current party. +command.party.already_in_party= is already in your party. +command.party.disband.description=Disband your current party. +party.player_joined= joined your party. +party.player_left= left your party. config.reload.failed=Config failed to reload config.reload.success=Config reloaded successfully error.command.argument_parsing=Invalid command argument: diff --git a/common/src/main/resources/queries/clear-party-members.sql b/common/src/main/resources/queries/clear-party-members.sql new file mode 100644 index 000000000..694fb80b4 --- /dev/null +++ b/common/src/main/resources/queries/clear-party-members.sql @@ -0,0 +1 @@ +DELETE FROM carbon_party_members WHERE (partyid = :partyid); diff --git a/common/src/main/resources/queries/drop-party-member.sql b/common/src/main/resources/queries/drop-party-member.sql new file mode 100644 index 000000000..e1a1c45f8 --- /dev/null +++ b/common/src/main/resources/queries/drop-party-member.sql @@ -0,0 +1 @@ +DELETE FROM carbon_party_members WHERE (playerid = :playerid); diff --git a/common/src/main/resources/queries/drop-party.sql b/common/src/main/resources/queries/drop-party.sql new file mode 100644 index 000000000..1988d7e46 --- /dev/null +++ b/common/src/main/resources/queries/drop-party.sql @@ -0,0 +1 @@ +DELETE FROM carbon_parties WHERE (partyid = :partyid); diff --git a/common/src/main/resources/queries/insert-party-member.sql b/common/src/main/resources/queries/insert-party-member.sql new file mode 100644 index 000000000..d1bf47b10 --- /dev/null +++ b/common/src/main/resources/queries/insert-party-member.sql @@ -0,0 +1 @@ +INSERT{!PSQL: IGNORE} INTO carbon_party_members (partyid, playerid) VALUES(:partyid, :playerid){PSQL: ON CONFLICT DO NOTHING}; diff --git a/common/src/main/resources/queries/insert-party.sql b/common/src/main/resources/queries/insert-party.sql new file mode 100644 index 000000000..fabe1ffd5 --- /dev/null +++ b/common/src/main/resources/queries/insert-party.sql @@ -0,0 +1,7 @@ +INSERT INTO carbon_parties( + partyid, + name +) VALUES ( + :partyid, + :name +); diff --git a/common/src/main/resources/queries/insert-player.sql b/common/src/main/resources/queries/insert-player.sql index 18f7a8000..50157f39d 100644 --- a/common/src/main/resources/queries/insert-player.sql +++ b/common/src/main/resources/queries/insert-player.sql @@ -7,7 +7,8 @@ INSERT{!PSQL: IGNORE} INTO carbon_users( lastwhispertarget, whisperreplytarget, spying, - ignoringdms + ignoringdms, + party ) VALUES ( :id, :muted, @@ -17,5 +18,6 @@ INSERT{!PSQL: IGNORE} INTO carbon_users( :lastwhispertarget, :whisperreplytarget, :spying, - :ignoringdms + :ignoringdms, + :party ){PSQL: ON CONFLICT DO NOTHING}; diff --git a/common/src/main/resources/queries/migrations/h2/V3__parties.sql b/common/src/main/resources/queries/migrations/h2/V3__parties.sql new file mode 100644 index 000000000..0225060bf --- /dev/null +++ b/common/src/main/resources/queries/migrations/h2/V3__parties.sql @@ -0,0 +1,12 @@ +CREATE TABLE carbon_party_members ( + `partyid` UUID NOT NULL, + `playerid` UUID NOT NULL, + PRIMARY KEY (partyid, playerid) +); + +CREATE TABLE carbon_parties ( + `partyid` UUID NOT NULL PRIMARY KEY, + `name` VARCHAR(8192) +); + +ALTER TABLE carbon_users ADD COLUMN party UUID; diff --git a/common/src/main/resources/queries/migrations/mysql/V7__parties.sql b/common/src/main/resources/queries/migrations/mysql/V7__parties.sql new file mode 100644 index 000000000..b55c3b36d --- /dev/null +++ b/common/src/main/resources/queries/migrations/mysql/V7__parties.sql @@ -0,0 +1,12 @@ +CREATE TABLE carbon_party_members ( + `partyid` BINARY(16) NOT NULL, + `playerid` BINARY(16) NOT NULL, + PRIMARY KEY (partyid, playerid) +); + +CREATE TABLE carbon_parties ( + `partyid` BINARY(16) NOT NULL PRIMARY KEY, + `name` VARCHAR(8192) +); + +ALTER TABLE carbon_users ADD COLUMN party BINARY(16); diff --git a/common/src/main/resources/queries/migrations/postgresql/V7__parties.sql b/common/src/main/resources/queries/migrations/postgresql/V7__parties.sql new file mode 100644 index 000000000..59ccbb645 --- /dev/null +++ b/common/src/main/resources/queries/migrations/postgresql/V7__parties.sql @@ -0,0 +1,12 @@ +CREATE TABLE carbon_party_members ( + partyid UUID NOT NULL, + playerid UUID NOT NULL, + PRIMARY KEY (partyid, playerid) +); + +CREATE TABLE carbon_parties ( + partyid UUID NOT NULL PRIMARY KEY, + name VARCHAR(8192) +); + +ALTER TABLE carbon_users ADD COLUMN party UUID; diff --git a/common/src/main/resources/queries/select-party-members.sql b/common/src/main/resources/queries/select-party-members.sql new file mode 100644 index 000000000..497d8dce4 --- /dev/null +++ b/common/src/main/resources/queries/select-party-members.sql @@ -0,0 +1 @@ +SELECT playerid FROM carbon_party_members WHERE (partyid = :partyid); diff --git a/common/src/main/resources/queries/select-party.sql b/common/src/main/resources/queries/select-party.sql new file mode 100644 index 000000000..5d164c28a --- /dev/null +++ b/common/src/main/resources/queries/select-party.sql @@ -0,0 +1,4 @@ +SELECT + partyid, + name +FROM carbon_parties WHERE (partyid = :partyid); diff --git a/common/src/main/resources/queries/select-player.sql b/common/src/main/resources/queries/select-player.sql index 6a78e9e12..add291d4a 100644 --- a/common/src/main/resources/queries/select-player.sql +++ b/common/src/main/resources/queries/select-player.sql @@ -7,5 +7,6 @@ SELECT lastwhispertarget, whisperreplytarget, spying, - ignoringdms + ignoringdms, + party FROM carbon_users WHERE (id = :id); diff --git a/common/src/main/resources/queries/update-player.sql b/common/src/main/resources/queries/update-player.sql index 6dfd36464..7735feba9 100644 --- a/common/src/main/resources/queries/update-player.sql +++ b/common/src/main/resources/queries/update-player.sql @@ -6,5 +6,6 @@ UPDATE carbon_users SET lastwhispertarget = :lastwhispertarget, whisperreplytarget = :whisperreplytarget, spying = :spying, - ignoringdms = :ignoringdms + ignoringdms = :ignoringdms, + party = :party WHERE (id = :id); diff --git a/fabric/src/main/java/net/draycia/carbon/fabric/FabricMessageRenderer.java b/fabric/src/main/java/net/draycia/carbon/fabric/FabricMessageRenderer.java index 0f8d97eed..8e2bd04de 100644 --- a/fabric/src/main/java/net/draycia/carbon/fabric/FabricMessageRenderer.java +++ b/fabric/src/main/java/net/draycia/carbon/fabric/FabricMessageRenderer.java @@ -34,7 +34,6 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @@ -54,15 +53,13 @@ public FabricMessageRenderer(final ConfigManager configManager) { public Component render( final Audience receiver, final String intermediateMessage, - final Map resolvedPlaceholders, + final Map resolvedPlaceholders, final Method method, final Type owner ) { final TagResolver.Builder tagResolver = TagResolver.builder(); - for (final var entry : resolvedPlaceholders.entrySet()) { - tagResolver.tag(entry.getKey(), entry.getValue()); - } + CarbonMessageRenderer.addResolved(tagResolver, resolvedPlaceholders); final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage); diff --git a/paper/src/main/java/net/draycia/carbon/paper/hooks/CarbonPAPIPlaceholders.java b/paper/src/main/java/net/draycia/carbon/paper/hooks/CarbonPAPIPlaceholders.java index a5dcc4ef0..10ae23693 100644 --- a/paper/src/main/java/net/draycia/carbon/paper/hooks/CarbonPAPIPlaceholders.java +++ b/paper/src/main/java/net/draycia/carbon/paper/hooks/CarbonPAPIPlaceholders.java @@ -22,6 +22,7 @@ import com.google.inject.Inject; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import net.draycia.carbon.api.users.CarbonPlayer; +import net.draycia.carbon.api.users.Party; import net.draycia.carbon.api.users.UserManager; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -64,17 +65,35 @@ public boolean persist() { @Override public @Nullable String onRequest(final OfflinePlayer player, final String params) { - final CarbonPlayer carbonPlayer = this.userManager.user(player.getUniqueId()).join(); - - final Component nickname = carbonPlayer.displayName(); - - if (params.endsWith("nickname")) { - return MiniMessage.miniMessage().serialize(nickname); + if (params.endsWith("party")) { + return mm(this.partyName(player)); + } else if (params.endsWith("party_l")) { + return legacy(this.partyName(player)); + } else if (params.endsWith("nickname")) { + return mm(this.displayName(player)); } else if (params.endsWith("nickname_l")) { - return LegacyComponentSerializer.legacySection().serialize(nickname); + return legacy(this.displayName(player)); } return null; } + private static String mm(final Component in) { + return MiniMessage.miniMessage().serialize(in); + } + + private static String legacy(final Component in) { + return LegacyComponentSerializer.legacySection().serialize(in); + } + + private Component partyName(final OfflinePlayer player) { + final @Nullable Party party = this.userManager.user(player.getUniqueId()).thenCompose(CarbonPlayer::party).join(); + return party == null ? Component.empty() : party.name(); + } + + private Component displayName(final OfflinePlayer player) { + final CarbonPlayer carbonPlayer = this.userManager.user(player.getUniqueId()).join(); + return carbonPlayer.displayName(); + } + } diff --git a/paper/src/main/java/net/draycia/carbon/paper/messages/PaperMessageRenderer.java b/paper/src/main/java/net/draycia/carbon/paper/messages/PaperMessageRenderer.java index ce1fb81b6..a6e534d7e 100644 --- a/paper/src/main/java/net/draycia/carbon/paper/messages/PaperMessageRenderer.java +++ b/paper/src/main/java/net/draycia/carbon/paper/messages/PaperMessageRenderer.java @@ -36,7 +36,6 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -71,15 +70,13 @@ public PaperMessageRenderer(final ConfigManager configManager) { public Component render( final Audience receiver, final String intermediateMessage, - final Map resolvedPlaceholders, + final Map resolvedPlaceholders, final Method method, final Type owner ) { final TagResolver.Builder tagResolver = TagResolver.builder(); - for (final var entry : resolvedPlaceholders.entrySet()) { - tagResolver.tag(entry.getKey(), entry.getValue()); - } + CarbonMessageRenderer.addResolved(tagResolver, resolvedPlaceholders); final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage); diff --git a/velocity/src/main/java/net/draycia/carbon/velocity/VelocityMessageRenderer.java b/velocity/src/main/java/net/draycia/carbon/velocity/VelocityMessageRenderer.java index 27dfac70d..8c54f47e9 100644 --- a/velocity/src/main/java/net/draycia/carbon/velocity/VelocityMessageRenderer.java +++ b/velocity/src/main/java/net/draycia/carbon/velocity/VelocityMessageRenderer.java @@ -34,7 +34,6 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @@ -56,15 +55,13 @@ public VelocityMessageRenderer(final ConfigManager configManager, final PluginMa public Component render( final Audience receiver, final String intermediateMessage, - final Map resolvedPlaceholders, + final Map resolvedPlaceholders, final Method method, final Type owner ) { final TagResolver.Builder tagResolver = TagResolver.builder(); - for (final var entry : resolvedPlaceholders.entrySet()) { - tagResolver.tag(entry.getKey(), entry.getValue()); - } + CarbonMessageRenderer.addResolved(tagResolver, resolvedPlaceholders); final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage);