Repository: Hexaoxide/Carbon Branch: trunk Commit: 3f445b6955b8 Files: 376 Total size: 1.3 MB Directory structure: gitextract_gdfuth62/ ├── .checkstyle/ │ ├── checkstyle.xml │ └── suppressions.xml ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ └── workflows/ │ └── build.yml ├── .gitignore ├── LICENSE ├── LICENSE_HEADER ├── README.md ├── api/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── draycia/ │ └── carbon/ │ └── api/ │ ├── CarbonChat.java │ ├── CarbonChatProvider.java │ ├── CarbonServer.java │ ├── channels/ │ │ ├── ChannelPermissionResult.java │ │ ├── ChannelPermissionResultImpl.java │ │ ├── ChannelPermissions.java │ │ ├── ChannelRegistry.java │ │ └── ChatChannel.java │ ├── event/ │ │ ├── Cancellable.java │ │ ├── CarbonEvent.java │ │ ├── CarbonEventHandler.java │ │ ├── CarbonEventSubscriber.java │ │ ├── CarbonEventSubscription.java │ │ └── events/ │ │ ├── CarbonChannelRegisterEvent.java │ │ ├── CarbonChatEvent.java │ │ ├── CarbonPrivateChatEvent.java │ │ ├── ChannelSwitchEvent.java │ │ ├── PartyJoinEvent.java │ │ └── PartyLeaveEvent.java │ ├── users/ │ │ ├── CarbonPlayer.java │ │ ├── Party.java │ │ └── UserManager.java │ └── util/ │ ├── ChatComponentRenderer.java │ ├── InventorySlot.java │ ├── KeyedRenderer.java │ └── KeyedRendererImpl.java ├── build-logic/ │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ ├── CarbonPermissionsExtension.kt │ ├── CarbonPlatformExtension.kt │ ├── ConfigurablePluginsExt.kt │ ├── FetchLuckPermsDownloads.kt │ ├── FetchLuckPermsJar.kt │ ├── FileCopyTask.kt │ ├── carbon.base-conventions.gradle.kts │ ├── carbon.build-logic.gradle.kts │ ├── carbon.configurable-plugins.gradle.kts │ ├── carbon.permissions.gradle.kts │ ├── carbon.platform-conventions.gradle.kts │ ├── carbon.publishing-conventions.gradle.kts │ ├── carbon.shadow-platform.gradle.kts │ ├── constants.kt │ └── extensions.kt ├── build.gradle.kts ├── common/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── java/ │ │ ├── com/ │ │ │ └── google/ │ │ │ └── inject/ │ │ │ └── assistedinject/ │ │ │ └── FactoryProvider3.java │ │ └── net/ │ │ └── draycia/ │ │ └── carbon/ │ │ └── common/ │ │ ├── CarbonChatInternal.java │ │ ├── CarbonCommonModule.java │ │ ├── CarbonPlatformModule.java │ │ ├── DataDirectory.java │ │ ├── PeriodicTasks.java │ │ ├── PlatformScheduler.java │ │ ├── RawChat.java │ │ ├── channels/ │ │ │ ├── CarbonChannelRegistry.java │ │ │ ├── ChannelPermissionsImpl.java │ │ │ ├── ConfigChannelSettings.java │ │ │ ├── ConfigChatChannel.java │ │ │ ├── PartyChatChannel.java │ │ │ └── messages/ │ │ │ ├── ConfigChannelMessageSource.java │ │ │ └── ConfigChannelMessages.java │ │ ├── command/ │ │ │ ├── CarbonCommand.java │ │ │ ├── CommandSettings.java │ │ │ ├── Commander.java │ │ │ ├── ExecutionCoordinatorHolder.java │ │ │ ├── ParserFactory.java │ │ │ ├── PlayerCommander.java │ │ │ ├── argument/ │ │ │ │ ├── CarbonPlayerParser.java │ │ │ │ └── PlayerSuggestions.java │ │ │ ├── commands/ │ │ │ │ ├── ClearChatCommand.java │ │ │ │ ├── ContinueCommand.java │ │ │ │ ├── DebugCommand.java │ │ │ │ ├── FilterCommand.java │ │ │ │ ├── HelpCommand.java │ │ │ │ ├── IgnoreCommand.java │ │ │ │ ├── IgnoreListCommand.java │ │ │ │ ├── JoinCommand.java │ │ │ │ ├── LeaveCommand.java │ │ │ │ ├── MuteCommand.java │ │ │ │ ├── MuteInfoCommand.java │ │ │ │ ├── NicknameCommand.java │ │ │ │ ├── PartyCommands.java │ │ │ │ ├── RealNameCommand.java │ │ │ │ ├── ReloadCommand.java │ │ │ │ ├── ReplyCommand.java │ │ │ │ ├── SpyCommand.java │ │ │ │ ├── ToggleMessagesCommand.java │ │ │ │ ├── UnignoreCommand.java │ │ │ │ ├── UnmuteCommand.java │ │ │ │ ├── UpdateUsernameCommand.java │ │ │ │ └── WhisperCommand.java │ │ │ └── exception/ │ │ │ ├── CommandCompleted.java │ │ │ └── ComponentException.java │ │ ├── config/ │ │ │ ├── ClearChatSettings.java │ │ │ ├── CommandConfig.java │ │ │ ├── ConfigHeader.java │ │ │ ├── ConfigManager.java │ │ │ ├── DatabaseSettings.java │ │ │ ├── IntegrationConfigContainer.java │ │ │ ├── MessagingSettings.java │ │ │ ├── PingSettings.java │ │ │ └── PrimaryConfig.java │ │ ├── event/ │ │ │ ├── CancellableImpl.java │ │ │ ├── CarbonEventHandlerImpl.java │ │ │ ├── CarbonEventSubscriptionImpl.java │ │ │ └── events/ │ │ │ ├── CarbonChatEventImpl.java │ │ │ ├── CarbonEarlyChatEvent.java │ │ │ ├── CarbonPrivateChatEventImpl.java │ │ │ ├── CarbonReloadEvent.java │ │ │ ├── ChannelRegisterEventImpl.java │ │ │ └── ChannelSwitchEventImpl.java │ │ ├── integration/ │ │ │ ├── Integration.java │ │ │ └── miniplaceholders/ │ │ │ ├── MiniPlaceholdersExpansion.java │ │ │ ├── MiniPlaceholdersIntegration.java │ │ │ └── MiniPlaceholdersUtil.java │ │ ├── listeners/ │ │ │ ├── ChatListenerInternal.java │ │ │ ├── DeafenHandler.java │ │ │ ├── FilterHandler.java │ │ │ ├── HyperlinkHandler.java │ │ │ ├── IgnoreHandler.java │ │ │ ├── ItemLinkHandler.java │ │ │ ├── Listener.java │ │ │ ├── MessagePacketHandler.java │ │ │ ├── MuteHandler.java │ │ │ ├── PartyChatSpyHandler.java │ │ │ ├── PartyPingHandler.java │ │ │ ├── PingHandler.java │ │ │ └── RadiusListener.java │ │ ├── messages/ │ │ │ ├── CarbonMessageRenderer.java │ │ │ ├── CarbonMessageSender.java │ │ │ ├── CarbonMessageSource.java │ │ │ ├── CarbonMessages.java │ │ │ ├── NotPlaceholder.java │ │ │ ├── Option.java │ │ │ ├── OptionTagResolver.java │ │ │ ├── PrefixedDelegateIterator.java │ │ │ ├── RenderForTagResolver.java │ │ │ ├── SourcedAudience.java │ │ │ ├── SourcedAudienceImpl.java │ │ │ ├── SourcedMessageSender.java │ │ │ ├── SourcedReceiverResolver.java │ │ │ ├── StandardPlaceholderResolverStrategyButDifferent.java │ │ │ ├── TagPermissions.java │ │ │ └── placeholders/ │ │ │ ├── BooleanPlaceholderResolver.java │ │ │ ├── ComponentPlaceholderResolver.java │ │ │ ├── IntPlaceholderResolver.java │ │ │ ├── KeyPlaceholderResolver.java │ │ │ ├── LongPlaceholderResolver.java │ │ │ ├── OptionPlaceholderResolver.java │ │ │ ├── StringPlaceholderResolver.java │ │ │ └── UUIDPlaceholderResolver.java │ │ ├── messaging/ │ │ │ ├── CarbonChatPacketHandler.java │ │ │ ├── MessagingManager.java │ │ │ ├── ServerId.java │ │ │ └── packets/ │ │ │ ├── CarbonPacket.java │ │ │ ├── ChatMessagePacket.java │ │ │ ├── DisbandPartyPacket.java │ │ │ ├── InvalidatePartyInvitePacket.java │ │ │ ├── LocalPlayerChangePacket.java │ │ │ ├── LocalPlayersPacket.java │ │ │ ├── PacketFactory.java │ │ │ ├── PartyChangePacket.java │ │ │ ├── PartyInvitePacket.java │ │ │ ├── SaveCompletedPacket.java │ │ │ └── WhisperPacket.java │ │ ├── serialisation/ │ │ │ └── gson/ │ │ │ ├── ChatChannelSerializerGson.java │ │ │ ├── LocaleSerializerConfigurate.java │ │ │ └── UUIDSerializerGson.java │ │ ├── users/ │ │ │ ├── Backing.java │ │ │ ├── CachingUserManager.java │ │ │ ├── CarbonPlayerCommon.java │ │ │ ├── ConsoleCarbonPlayer.java │ │ │ ├── MojangProfileResolver.java │ │ │ ├── NetworkUsers.java │ │ │ ├── PartyImpl.java │ │ │ ├── PartyInvites.java │ │ │ ├── PersistentUserProperty.java │ │ │ ├── PlatformUserManager.java │ │ │ ├── PlayerUtils.java │ │ │ ├── ProfileCache.java │ │ │ ├── ProfileResolver.java │ │ │ ├── UserManagerInternal.java │ │ │ ├── WrappedCarbonPlayer.java │ │ │ ├── db/ │ │ │ │ ├── DatabaseUserManager.java │ │ │ │ ├── QueriesLocator.java │ │ │ │ ├── argument/ │ │ │ │ │ ├── BinaryUUIDArgumentFactory.java │ │ │ │ │ ├── ComponentArgumentFactory.java │ │ │ │ │ └── KeyArgumentFactory.java │ │ │ │ └── mapper/ │ │ │ │ ├── BinaryUUIDColumnMapper.java │ │ │ │ ├── ComponentColumnMapper.java │ │ │ │ ├── KeyColumnMapper.java │ │ │ │ ├── NativeUUIDColumnMapper.java │ │ │ │ ├── PartyRowMapper.java │ │ │ │ └── PlayerRowMapper.java │ │ │ └── json/ │ │ │ └── JSONUserManager.java │ │ └── util/ │ │ ├── CarbonDependencies.java │ │ ├── ChannelUtils.java │ │ ├── CloudUtils.java │ │ ├── ColorUtils.java │ │ ├── ConcurrentUtil.java │ │ ├── DiscordRecipient.java │ │ ├── EmptyAudienceWithPointers.java │ │ ├── ExceptionLoggingScheduledThreadPoolExecutor.java │ │ ├── Exceptions.java │ │ ├── FastUuidSansHyphens.java │ │ ├── FileUtil.java │ │ ├── Pagination.java │ │ ├── PaginationHelper.java │ │ ├── SQLDrivers.java │ │ ├── Strings.java │ │ └── UpdateChecker.java │ └── resources/ │ ├── carbon-permissions.yml │ ├── locale/ │ │ ├── messages-de_AT.properties │ │ ├── messages-de_CH.properties │ │ ├── messages-de_DE.properties │ │ ├── messages-en_US.properties │ │ ├── messages-es_CL.properties │ │ ├── messages-es_ES.properties │ │ ├── messages-fi_FI.properties │ │ ├── messages-fr_CA.properties │ │ ├── messages-fr_FR.properties │ │ ├── messages-ja_JP.properties │ │ ├── messages-nl_NL.properties │ │ ├── messages-nn_NO.properties │ │ ├── messages-no_NO.properties │ │ ├── messages-pl_PL.properties │ │ ├── messages-pt_BR.properties │ │ ├── messages-ru_RU.properties │ │ ├── messages-tr_TR.properties │ │ ├── messages-uk_UA.properties │ │ ├── messages-zh_CN.properties │ │ └── messages-zh_TW.properties │ └── queries/ │ ├── clear-ignores.sql │ ├── clear-leftchannels.sql │ ├── clear-party-members.sql │ ├── drop-party-member.sql │ ├── drop-party.sql │ ├── insert-party-member.sql │ ├── insert-party.sql │ ├── insert-player.sql │ ├── migrations/ │ │ ├── h2/ │ │ │ ├── V1__create_tables.sql │ │ │ ├── V2__increase_nickname_size.sql │ │ │ ├── V3__parties.sql │ │ │ ├── V4__filters.sql │ │ │ ├── V5__tempmute.sql │ │ │ └── V6__tempmute.sql │ │ ├── mysql/ │ │ │ ├── V10__tempmute.sql │ │ │ ├── V1__create_tables.sql │ │ │ ├── V2__create_tables.sql │ │ │ ├── V3__fix_leftchannels.sql │ │ │ ├── V4__drop_usernames.sql │ │ │ ├── V5__add_dmtoggle.sql │ │ │ ├── V6__increase_nickname_size.sql │ │ │ ├── V7__parties.sql │ │ │ ├── V8__filters.sql │ │ │ └── V9__tempmute.sql │ │ └── postgresql/ │ │ ├── V10__tempmute.sql │ │ ├── V1__create_tables.sql │ │ ├── V2__create_tables.sql │ │ ├── V3__fix_leftchannels.sql │ │ ├── V4__drop_usernames.sql │ │ ├── V5__add_dmtoggle.sql │ │ ├── V6__increase_nickname_size.sql │ │ ├── V7__parties.sql │ │ ├── V8__filters.sql │ │ └── V9__tempmute.sql │ ├── save-ignores.sql │ ├── save-leftchannels.sql │ ├── select-ignores.sql │ ├── select-leftchannels.sql │ ├── select-party-members.sql │ ├── select-party.sql │ ├── select-player.sql │ └── update-player.sql ├── crowdin.yml ├── fabric/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── java/ │ │ └── net/ │ │ └── draycia/ │ │ └── carbon/ │ │ └── fabric/ │ │ ├── CarbonChatFabric.java │ │ ├── CarbonChatFabricModule.java │ │ ├── CarbonFabricBootstrap.java │ │ ├── CarbonServerFabric.java │ │ ├── FabricMessageRenderer.java │ │ ├── FabricScheduler.java │ │ ├── MinecraftServerHolder.java │ │ ├── command/ │ │ │ ├── FabricCommander.java │ │ │ └── FabricPlayerCommander.java │ │ ├── listeners/ │ │ │ ├── FabricChatHandler.java │ │ │ └── FabricJoinQuitListener.java │ │ └── users/ │ │ ├── CarbonPlayerFabric.java │ │ └── FabricProfileResolver.java │ └── resources/ │ ├── carbonchat.mixins.json │ ├── data/ │ │ └── carbonchat/ │ │ └── chat_type/ │ │ └── chat.json │ └── pack.mcmeta ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── paper/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── draycia/ │ └── carbon/ │ └── paper/ │ ├── CarbonChatPaper.java │ ├── CarbonChatPaperModule.java │ ├── CarbonPaperBootstrap.java │ ├── CarbonPaperLoader.java │ ├── CarbonServerPaper.java │ ├── PaperScheduler.java │ ├── command/ │ │ ├── PaperCommander.java │ │ └── PaperPlayerCommander.java │ ├── hooks/ │ │ ├── CarbonPAPIPlaceholders.java │ │ └── PAPIChatHook.java │ ├── integration/ │ │ ├── alessiodp_parties/ │ │ │ ├── AlessiodpPartiesIntegration.java │ │ │ └── AlessiodpPartiesPartyChannel.java │ │ ├── dsrv/ │ │ │ ├── DSRVIntegration.java │ │ │ └── DSRVListener.java │ │ ├── essxd/ │ │ │ ├── EssXDIntegration.java │ │ │ └── EssXDListener.java │ │ ├── fuuid/ │ │ │ ├── AbstractFactionsChannel.java │ │ │ ├── AllianceChannel.java │ │ │ ├── FactionChannel.java │ │ │ ├── FactionModChannel.java │ │ │ ├── FactionsIntegration.java │ │ │ └── TruceChannel.java │ │ ├── mcmmo/ │ │ │ ├── McmmoIntegration.java │ │ │ └── McmmoPartyChannel.java │ │ ├── plotsquared/ │ │ │ ├── PlotChannel.java │ │ │ └── PlotSquaredIntegration.java │ │ └── towny/ │ │ ├── AllianceChannel.java │ │ ├── NationChannel.java │ │ ├── ResidentListChannel.java │ │ ├── TownChannel.java │ │ └── TownyIntegration.java │ ├── listeners/ │ │ ├── PaperChatListener.java │ │ └── PaperPlayerJoinListener.java │ ├── messages/ │ │ ├── PaperMessageRenderer.java │ │ └── PlaceholderAPIMiniMessageParser.java │ └── users/ │ ├── CarbonPlayerPaper.java │ └── PaperProfileResolver.java ├── renovate.json ├── settings.gradle.kts ├── sponge/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── net/ │ └── draycia/ │ └── carbon/ │ └── sponge/ │ ├── CarbonChatSponge.java │ ├── CarbonChatSpongeModule.java │ ├── CarbonServerSponge.java │ ├── SpongeMessageRenderer.java │ ├── SpongeUserManager.java │ ├── command/ │ │ ├── SpongeCommander.java │ │ └── SpongePlayerCommander.java │ ├── listeners/ │ │ ├── SpongeChatListener.java │ │ ├── SpongePlayerJoinListener.java │ │ └── SpongeReloadListener.java │ └── users/ │ └── CarbonPlayerSponge.java └── velocity/ ├── build.gradle.kts └── src/ └── main/ └── java/ └── net/ └── draycia/ └── carbon/ └── velocity/ ├── CarbonChatVelocity.java ├── CarbonChatVelocityModule.java ├── CarbonServerVelocity.java ├── CarbonVelocityBootstrap.java ├── VelocityMessageRenderer.java ├── command/ │ ├── VelocityCommander.java │ └── VelocityPlayerCommander.java ├── listeners/ │ ├── VelocityChatListener.java │ ├── VelocityListener.java │ ├── VelocityPlayerJoinListener.java │ └── VelocityPlayerLeaveListener.java └── users/ ├── CarbonPlayerVelocity.java └── VelocityProfileResolver.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .checkstyle/checkstyle.xml ================================================ ================================================ FILE: .checkstyle/suppressions.xml ================================================ ================================================ FILE: .editorconfig ================================================ # MIT License # # Copyright (c) 2017-2020 KyoriPowered # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. root = true [*] charset = utf-8 indent_size = 4 indent_style = space insert_final_newline = true max_line_length = off [*.java] ij_java_imports_layout = *, |, $* ij_java_class_count_to_use_import_on_demand = 999 ij_java_names_count_to_use_import_on_demand = 999 [{*.kt,*.kts,*.yml}] indent_size = 2 ================================================ FILE: .github/FUNDING.yml ================================================ github: [Draycia, jpenilla] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: "\U0001F41E Bug report" about: Describe a bug in Carbon title: "[Bug]" labels: unconfirmed bug assignees: '' --- ## Bug Description: ### What is not working as it should? ### Steps to reproduce: ### System Details: 1. Server Type: 2. Server Software: 3. MC Version: 4. Carbon Version: ### Pastebins: ### Any other relevant details: ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 💬 ​ Ask a question url: https://discord.gg/S8s75Yf about: Support for Carbon is provided on Discord. Instead of making an issue to ask a question, join us on discord and we'll be happy to help! ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: "⚡ Feature request" about: Suggest an idea for Carbon title: "[Feature]" labels: proposed enhancement assignees: '' --- ### Proposed Feature Description: ### Proposed Feature Functionality: ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: [ "**" ] tags: [ "v**" ] pull_request: release: types: [ published ] jobs: build: # Only run on PRs if the source branch is on someone else's repo if: ${{ (github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name) && (github.event.name != 'push' || !startsWith(github.ref, 'refs/tags/') || contains(github.ref, '-beta.')) }} runs-on: ubuntu-latest strategy: matrix: java: [ 21 ] fail-fast: true steps: - uses: actions/checkout@v6 - name: JDK ${{ matrix.java }} uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} distribution: 'temurin' - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 # gradle build action can't handle project dir local caches - uses: actions/cache@v5 name: Cache Loom Files with: path: | .gradle/loom-cache key: ${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-gradle- - name: Build run: ./gradlew build --stacktrace - name: Determine Snapshot Status run: | if [ "$(./gradlew properties | awk '/^version:/ { print $2; }' | grep '\-SNAPSHOT')" ]; then echo "STATUS=snapshot" >> $GITHUB_ENV else echo "STATUS=release" >> $GITHUB_ENV fi - name: "publish snapshot to sonatype snapshots" if: "${{ env.STATUS != 'release' && github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}" run: ./gradlew publishAllPublicationsToSonatypeSnapshots env: ORG_GRADLE_PROJECT_sonatypeUsername: "${{ secrets.SONATYPE_USERNAME }}" ORG_GRADLE_PROJECT_sonatypePassword: "${{ secrets.SONATYPE_PASSWORD }}" - name: "publish (pre-)release to maven central" if: "${{ env.STATUS == 'release' && github.event_name == 'release' }}" run: ./gradlew publishReleaseCentralPortalBundle env: ORG_GRADLE_PROJECT_sonatypeUsername: "${{ secrets.SONATYPE_USERNAME }}" ORG_GRADLE_PROJECT_sonatypePassword: "${{ secrets.SONATYPE_PASSWORD }}" ORG_GRADLE_PROJECT_signingKey: "${{ secrets.SIGNING_KEY }}" ORG_GRADLE_PROJECT_signingPassword: "${{ secrets.SIGNING_PASSWORD }}" - name: Parse tag if: "${{ github.event_name == 'push' && contains(github.ref, '-beta.') }}" id: vars run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} - name: Create changelog and Pre-Release if: "${{ github.event_name == 'push' && contains(github.ref, '-beta.') }}" uses: MC-Machinations/auto-release-changelog@v1.1.3 with: token: ${{ secrets.RELEASE_TOKEN }} title: CarbonChat ${{ steps.vars.outputs.tag }} pre-release: true files: | build/libs/carbonchat-paper-*.jar build/libs/carbonchat-velocity-*.jar build/libs/carbonchat-fabric-*.jar - name: Publish (Pre-)Release to Modrinth if: "${{ env.STATUS == 'release' && github.event_name == 'release' }}" run: ./gradlew :carbonchat-paper:publishModrinth :carbonchat-velocity:publishModrinth :carbonchat-fabric:publishModrinth env: MODRINTH_TOKEN: "${{ secrets.MODRINTH_TOKEN }}" RELEASE_NOTES: "${{ github.event.release.body }}" - name: Publish (Pre-)Release to Hangar if: "${{ env.STATUS == 'release' && github.event_name == 'release' }}" run: ./gradlew publishAllPublicationsToHangar env: HANGAR_UPLOAD_KEY: "${{ secrets.HANGAR_UPLOAD_KEY }}" RELEASE_NOTES: "${{ github.event.release.body }}" - name: Upload Artifacts uses: actions/upload-artifact@v7 with: name: Jars path: build/libs/*.jar smoketest: needs: build runs-on: ubuntu-latest services: postgres: image: postgres:13 env: POSTGRES_USER: username POSTGRES_PASSWORD: password POSTGRES_DB: carbon ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 mariadb: image: mariadb:10.11 env: MARIADB_USER: username MARIADB_PASSWORD: password MARIADB_ROOT_PASSWORD: rootpassword MARIADB_DATABASE: carbon ports: - 3306:3306 options: >- --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 strategy: matrix: java: [ 21 ] fail-fast: true steps: - uses: actions/checkout@v6 - name: JDK ${{ matrix.java }} uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} distribution: 'temurin' - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 # gradle build action can't handle project dir local caches - uses: actions/cache@v5 name: Cache Loom Files with: path: | .gradle/loom-cache key: ${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-gradle- - uses: actions/cache@v5 name: Cache Smoke Test Files with: path: | paper/build/tmp/smokeTest/cache paper/build/tmp/smokeTest/libraries paper/build/tmp/smokeTest/plugins/CarbonChat/libraries paper/build/tmp/smokeTest/world paper/build/tmp/smokeTest/world_nether paper/build/tmp/smokeTest/world_the_end key: ${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-gradle-smoketest- - name: Prime Build run: ./gradlew build --stacktrace - name: Smoke test (Paper, JSON) run: ./gradlew :carbonchat-paper:runServer -PsmokeTest=true -PsmokeTestMode=json timeout-minutes: 5 - name: Smoke test (Paper, H2) run: ./gradlew :carbonchat-paper:runServer -PsmokeTest=true -PsmokeTestMode=h2 timeout-minutes: 5 - name: Smoke test (Paper, MariaDB) run: ./gradlew :carbonchat-paper:runServer -PsmokeTest=true -PsmokeTestMode=mariadb timeout-minutes: 5 - name: Smoke test (Paper, Postgres) run: ./gradlew :carbonchat-paper:runServer -PsmokeTest=true -PsmokeTestMode=postgres timeout-minutes: 5 ================================================ FILE: .gitignore ================================================ # Compiled class file *.class # Kotlin temp files .kotlin/ # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* *.iml *.ipr *.iws /out/ /bin/ # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf /.idea/ # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. .idea/modules.xml .idea/*.iml .idea/modules .idea/misc.xml # Dolphin browser keeps recreating this .directory # Gradle .gradle **/build/ !src/**/build/ # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar # Cache of project .gradletasknamecache # Mac filesystem dust .DS_Store/ .DS_Store **/run/ **/run2/ **/run-plugins.yml ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: LICENSE_HEADER ================================================ CarbonChat Copyright (c) 2024 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 . ================================================ FILE: README.md ================================================

Carbon plugin banner.
Carbon is a modern chat Java Edition plugin built on channels, with just about every single setting and format configurable.

## Support Support is given through [GitHub Issues](https://github.com/Hexaoxide/Carbon/issues) and [Discord](https://discord.gg/S8s75Yf). Please use the discord for help setting up the plugin, and use issues for bug reports. ## Checkstyle Carbon uses (a fork of) checkstyle to ensure code style is consistent across the entire project. For checkstyle support in IDEA: 1. Install the [checkstyle plugin](https://github.com/jshiell/checkstyle-idea). 2. Compile https://gitlab.com/stellardrift/stylecheck 3. `Settings` -> `Tools` -> `Checkstyle` `Third-Party Checks`, add the compiled stylecheck jar 4. While still in the `Checkstyle` tab, go to `Configuration File`, add `.checkstyle/checkstyle.xml` and tick the check box. ================================================ FILE: api/build.gradle.kts ================================================ plugins { id("carbon.publishing-conventions") alias(libs.plugins.javadoc.links) } description = "API for interfacing with the CarbonChat Minecraft mod/plugin" dependencies { // Doesn't add any dependencies, only version constraints api(platform(libs.adventureBom)) // Provided by platform compileOnlyApi(libs.adventureApi) compileOnlyApi(libs.adventureTextSerializerPlain) compileOnlyApi(libs.adventureTextSerializerLegacy) compileOnlyApi(libs.adventureTextSerializerGson) { exclude("com.google.code.gson") } compileOnlyApi(libs.minimessage) compileOnlyApi(libs.checkerQual) // Provided by Minecraft compileOnlyApi(libs.gson) } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/CarbonChat.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.event.CarbonEvent; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.users.UserManager; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * The {@link CarbonChat} interface is the gateway to interacting with the majority of the CarbonChat API. * *

Instances may be obtained through {@link CarbonChatProvider#carbonChat()} once Carbon is loaded.

* *

On most platforms, you should use the provided load order mechanism to ensure your addon loads after * Carbon.

* *

On Fabric, use the {@code carbonchat} entrypoint (type: {@code Consumer}) to have a callback * when Carbon is loaded.

* * @since 1.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonChat { /** * The {@link CarbonEventHandler event handler}, used for listening to * and emitting {@link CarbonEvent events}. * * @return the event handler * @since 2.0.0 */ CarbonEventHandler eventHandler(); /** * The server that carbon is running on. * * @return the server * @since 2.0.0 */ CarbonServer server(); /** * The user manager. * * @return the user manager * @since 3.0.0 */ UserManager userManager(); /** * The registry that channels are registered to. * * @return the channel registry * @since 2.0.0 */ ChannelRegistry channelRegistry(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/CarbonChatProvider.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.jetbrains.annotations.ApiStatus; /** * Static accessor for the {@link CarbonChat} instance. * * @since 1.0.0 */ @DefaultQualifier(NonNull.class) public final class CarbonChatProvider { private static @Nullable CarbonChat instance; private CarbonChatProvider() { } /** * Registers the {@link CarbonChat} implementation. * * @param carbonChat the carbon implementation * @since 1.0.0 */ @ApiStatus.Internal public static void register(final CarbonChat carbonChat) { CarbonChatProvider.instance = carbonChat; } /** * Gets the currently registered {@link CarbonChat} implementation. * * @return the registered carbon implementation * @since 1.0.0 */ public static CarbonChat carbonChat() { if (CarbonChatProvider.instance == null) { throw new IllegalStateException("CarbonChat not initialized!"); } return CarbonChatProvider.instance; } } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/CarbonServer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import java.util.List; import net.draycia.carbon.api.users.CarbonPlayer; import net.kyori.adventure.audience.Audience; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * The server that carbon is running on. * * @since 2.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonServer extends Audience { /** * The server's console. * * @return the server's console * @since 2.0.0 */ Audience console(); /** * The players that are online on the server. * * @return the online players * @since 2.0.0 */ List players(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/channels/ChannelPermissionResult.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.channels; import java.util.function.Supplier; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * Represents the result of a channel permission check. * * @since 3.0.0 */ @DefaultQualifier(NonNull.class) public interface ChannelPermissionResult { /** * Check whether the action checked was permitted. * * @return permitted * @since 3.0.0 */ boolean permitted(); /** * Reason for permission being denied. When the action * was permitted, this should be equal to {@link Component#empty()}. * * @return deny reason * @since 3.0.0 */ Component reason(); /** * Returns a result denoting that the player is permitted for the action. * * @return that the action is allowed * @since 3.0.0 */ static ChannelPermissionResult allowed() { return ChannelPermissionResultImpl.ALLOWED; } /** * Returns a result denoting that the action is denied for the player. * * @param reason the reason the action was denied * @return that the action is denied * @since 3.0.0 */ static ChannelPermissionResult denied(final Component reason) { return new ChannelPermissionResultImpl(false, () -> reason); } /** * Returns a result denoting that the action is denied for the player. * * @param reason the reason the action was denied * @return that the action is denied * @since 3.0.0 */ static ChannelPermissionResult denied(final Supplier reason) { return new ChannelPermissionResultImpl(false, reason); } /** * Create a {@link ChannelPermissionResult} based on {@code allowed}, * computing {@code denyReason} when needed. * * @param allowed whether the result is allowed * @param denyReason deny reason supplier * @return permission result * @since 3.0.0 */ static ChannelPermissionResult channelPermissionResult( final boolean allowed, final Supplier denyReason ) { if (allowed) { return allowed(); } return denied(denyReason); } } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/channels/ChannelPermissionResultImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.channels; import java.util.function.Supplier; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) record ChannelPermissionResultImpl( boolean permitted, Supplier reasonSupplier ) implements ChannelPermissionResult { static final ChannelPermissionResult ALLOWED = new ChannelPermissionResultImpl(true, Component::empty); @Override public Component reason() { return this.reasonSupplier.get(); } } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/channels/ChannelPermissions.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.channels; import java.util.function.Function; import net.draycia.carbon.api.users.CarbonPlayer; /** * Permissions handling for a channel. * * @since 3.0.0 */ public interface ChannelPermissions { /** * Checks if the player may join this channel. * * @param carbonPlayer the player attempting to join * @return if the player may join * @since 3.0.0 */ ChannelPermissionResult joinPermitted(CarbonPlayer carbonPlayer); /** * Checks if the player may send messages in this channel. * * @param carbonPlayer the player attempting to speak * @return if the player may speak * @since 3.0.0 */ ChannelPermissionResult speechPermitted(CarbonPlayer carbonPlayer); /** * Checks if the player may receive messages from this channel. * * @param player the player that's receiving messages * @return if the player may receive messages * @since 3.0.0 */ ChannelPermissionResult hearingPermitted(CarbonPlayer player); /** * Returns whether the result of {@link #joinPermitted(CarbonPlayer)} is dynamic. * *

An example of a dynamic permissions is the built-in party channel that only allows players in a party to join.

* *

An example of static permissions is the built-in config channels that simply check permission strings. The fact that a player's * permissions may change during gameplay does not make the permission dynamic, as the server will resend commands on permission changes.

* *

If the result is static, then we can avoid sending commands to the player that they will just get denied use * of on execute. If it's dynamic, we must send the command regardless in case they gain permission later.

* * @return whether the permissions are dynamic * @since 3.0.0 */ boolean dynamic(); /** * Creates a new {@link ChannelPermissions} that performs the same check for * {@link #joinPermitted(CarbonPlayer)}, {@link #hearingPermitted(CarbonPlayer)}, * and {@link #speechPermitted(CarbonPlayer)}. * * @param check permission check * @return new permissions object * @since 3.0.0 */ static ChannelPermissions uniformDynamic(final Function check) { return new ChannelPermissions() { @Override public ChannelPermissionResult joinPermitted(final CarbonPlayer player) { return check.apply(player); } @Override public ChannelPermissionResult speechPermitted(final CarbonPlayer player) { return check.apply(player); } @Override public ChannelPermissionResult hearingPermitted(final CarbonPlayer player) { return check.apply(player); } @Override public boolean dynamic() { return true; } }; } } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/channels/ChannelRegistry.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.channels; import java.util.NoSuchElementException; import java.util.Set; import java.util.function.Consumer; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; /** * Registry for {@link ChatChannel chat channels}. * * @since 2.0.0 */ public interface ChannelRegistry { /** * Registers the chat channel with its key. * *

Registrations will persist when reloading Carbon's configuration.

* * @param channel the channel to register * @since 3.0.0 */ void register(ChatChannel channel); /** * Retrieve a channel by its key. If there is no matching channel, * returns {@code null}. * * @param key the channel's key * @return the channel * @since 3.0.0 */ @Nullable ChatChannel channel(Key key); /** * Gets the key for the default channel. * * @return the default key * @since 3.0.0 */ @NonNull Key defaultKey(); /** * Gets the default channel. * * @return the default value * @since 3.0.0 */ @NonNull ChatChannel defaultChannel(); /** * Gets the list of registered channel keys. * * @return the registered channel keys * @since 3.0.0 */ @NonNull Set keys(); /** * Retrieve a channel by its key. If there is no matching channel, * returns {@link #defaultChannel() the default channel}. * * @param key the channel key * @return the channel, or the default one * @since 3.0.0 */ ChatChannel channelOrDefault(Key key); /** * Retrieve a channel by its key. If there is no matching channel, * throws {@link NoSuchElementException}. * * @param key channel key * @return channel * @throws NoSuchElementException when no matching channel is found * @since 3.0.0 */ ChatChannel channelOrThrow(Key key); /** * The provided action will be executed immediately for all currently registered * channels. * *

When new channels are registered, the action will be invoked again for each new channel.

* * @param action action * @since 3.0.0 */ void allKeys(Consumer action); /** * Create a {@link ChannelPermissions channel permissions handler} for the provided base permission string. * *

The handler will check the base permission for joins, {@literal .see} for receiving/seeing messages, * and {@literal .speak} for speaking/sending messages.

* *

The built-in deny messages are used, same as user-configured config channels.

* * @param permission permission string * @return permission handler * @since 3.0.0 */ ChannelPermissions permission(String permission); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/channels/ChatChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.channels; import java.util.List; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.ChatComponentRenderer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Keyed; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; /** * ChatChannel interface, supplies a formatter and filters recipients.
* Extends Keyed for identification purposes. * * @since 2.0.0 */ @DefaultQualifier(NonNull.class) public interface ChatChannel extends Keyed, ChatComponentRenderer { /** * Returns the permissions handler for the channel. * * @return the permissions handler * @since 3.0.0 */ ChannelPermissions permissions(); /** * Returns a list of all recipients that will receive messages from the sender. * * @param sender the sender of messages * @return the recipients * @since 2.0.0 */ List recipients(CarbonPlayer sender); /** * Messages will be sent in this channel if they start with this prefix. * * @return the message prefix that sends messages in this channel * @since 2.0.0 */ @Nullable String quickPrefix(); /** * If commands should be registered for this channel. * * @return if commands should be registered for this channel. * @since 2.0.0 */ boolean shouldRegisterCommands(); /** * The text that can be used to refer to this channel in commands. * * @return this channel's name when used in commands * @since 2.0.0 */ String commandName(); /** * Alternative command names for this channel. * * @return alternative command names * @since 2.0.0 */ List commandAliases(); /** * The distance from the sender players must be to receive chat messages.
* Return of '0' means players must be in the same world/server.
* Return of '-1' means there is no radius. * * @return the channel radius * @since 3.0.0 */ double radius(); /** * If the empty receipt message should be sent to the sender. * * @return Returns true if the channel should display a message when a player is out of range. * @since 3.0.0 */ boolean emptyRadiusRecipientsMessage(); /** * The time in milliseconds between player messages. * -1 and 0 disable the cooldown for this channel. * * @return The message cooldown in millis. * @since 3.0.0 */ long cooldown(); /** * The epoch time (millis) when the player's cooldown expires. * * @param player The player * @return The epoch time (millis) when the player's cooldown expires. * @since 3.0.0 */ long playerCooldown(CarbonPlayer player); /** * Starts the cooldown timer for the specified player. Duration will be the channel cooldown. * Returns the player's old cooldown time, if they have one. * * @param player The player * @return The player's old cooldown, or 0 if they don't have one. * @since 3.0.0 */ long startCooldown(CarbonPlayer player); /** * Whether messages from this channel should be broadcast and sent to other servers. * * @return if this channel's messages should be sent cross-server * @since 3.0.0 */ boolean shouldCrossServer(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/Cancellable.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; /** * Marks an event as cancellable. * * @since 3.0.0 */ public interface Cancellable { /** * Gets if the event is cancelled. * * @return if the event is cancelled * @since 3.0.0 */ boolean cancelled(); /** * Sets the cancelled state. * * @param cancelled new cancelled state * @since 3.0.0 */ void cancelled(boolean cancelled); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/CarbonEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; /** * Marker interface for events. * * @since 1.0.0 */ public interface CarbonEvent { } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/CarbonEventHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * The {@link CarbonEventHandler} is responsible for managing {@link CarbonEventSubscription event subscriptions} * and emitting {@link CarbonEvent events}. * * @since 3.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonEventHandler { /** * Registers a subscriber for the given event class. * * @param eventClass the class to listen for * @param subscriber the subscriber that's executed when the event is emitted * @param the class to listen for * @return the subscription, so that it may be unregistered * @since 2.0.0 */ CarbonEventSubscription subscribe( Class eventClass, CarbonEventSubscriber subscriber ); /** * Registers a subscriber for the given event class.
* Includes extra values to control when the consumer is executed. * * @param eventClass the class to listen for * @param order the order of the consumer * @param acceptsCancelled if the consumer should be executed if the event is cancelled early * @param subscriber the consumer that's executed when the event is emitted * @param the class to listen for * @return the subscription, so that it may be unregistered * @since 2.0.0 */ CarbonEventSubscription subscribe( Class eventClass, int order, boolean acceptsCancelled, CarbonEventSubscriber subscriber ); /** * Emits the supplied event. * *

Events are modified in place, meaning you must keep a reference to the event * yourself if you wish to inspect it's state after this call.

* * @param event the event to be emitted * @param the class to emit * @since 2.0.0 */ void emit(T event); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/CarbonEventSubscriber.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * An EventSubscriber. * * @param CarbonEvent implementations * @since 3.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonEventSubscriber { /** * Invokes this event consumer. * * @param event the event * @throws Throwable if an exception is thrown * @since 1.0.0 */ void on(T event) throws Throwable; } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/CarbonEventSubscription.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * A subscription to a specific event type. * * @param event type * @since 3.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonEventSubscription { /** * Gets the event type. * * @return the event type * @since 3.0.0 */ Class event(); /** * Gets the {@link CarbonEventSubscriber subscriber}. * * @return the subscriber * @since 3.0.0 */ CarbonEventSubscriber subscriber(); /** * Disposes this subscription. * *

The subscriber held by this subscription will no longer receive events.

* * @since 3.0.0 */ void dispose(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/events/CarbonChannelRegisterEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.Set; import java.util.function.Consumer; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.event.CarbonEvent; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * {@link CarbonEvent} that's called after new channels are registered. * *

Note that some invocations of this event may be too early for * API consumers to be notified. {@link ChannelRegistry#allKeys(Consumer)} * is provided as a helper for when knowledge of all registered channels * is needed.

* * @since 3.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonChannelRegisterEvent extends CarbonEvent { /** * Gets the channel registry. * * @return the channel registry * @since 3.0.0 */ ChannelRegistry channelRegistry(); /** * Gets the key(s) that were registered to trigger this event. * * @return key(s) registered * @since 3.0.0 */ Set registered(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/events/CarbonChatEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.List; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.event.Cancellable; import net.draycia.carbon.api.event.CarbonEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.KeyedRenderer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.chat.SignedMessage; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * Event that's called when chat components are rendered for online players. * * @since 2.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonChatEvent extends CarbonEvent, Cancellable { /** * Get the renderers used to construct components for each of the recipients. The returned list * is mutable. * * @return renderers * @since 2.0.0 */ List renderers(); /** * If the message is being previewed by the player. * * @return if the message is being previewed * @since 3.0.0 */ @MonotonicNonNull SignedMessage signedMessage(); /** * Get the sender of the message. * * @return The message sender. * @since 2.0.0 */ CarbonPlayer sender(); /** * Get the original message that was sent. * * @return The original message. * @since 2.0.0 */ Component originalMessage(); /** * Get the chat message that will be sent. * * @return The chat message. * @since 2.0.0 */ Component message(); /** * Set the chat message that will be sent. * * @param message new message * @since 2.0.0 */ void message(final Component message); /** * The chat channel the message was sent in. * * @return the chat channel * @since 2.0.0 */ @MonotonicNonNull ChatChannel chatChannel(); /** * The recipients of the message. * List is mutable and elements may be added/removed. * * @return the recipients of the message. * entries may be players, console, or other audience implementations * @since 2.0.0 */ List recipients(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/events/CarbonPrivateChatEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.draycia.carbon.api.event.Cancellable; import net.draycia.carbon.api.event.CarbonEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * Called whenever a player privately messages another player. * * @since 3.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonPrivateChatEvent extends CarbonEvent, Cancellable { /** * Sets the message that will be sent. * * @param message the new message * @throws NullPointerException if message is null * @since 3.0.0 */ void message(Component message); /** * The message that will be sent. * * @return the message * @since 3.0.0 */ Component message(); /** * The message sender. * * @return the sender of the message * @since 3.0.0 */ CarbonPlayer sender(); /** * The message recipient. * * @return the recipient of the message * @since 3.0.0 */ CarbonPlayer recipient(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/events/ChannelSwitchEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.event.CarbonEvent; import net.draycia.carbon.api.users.CarbonPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * Called when a player switches channels. * * @since 3.0.0 */ @DefaultQualifier(NonNull.class) public interface ChannelSwitchEvent extends CarbonEvent { /** * The player switching channels. * * @since 3.0.0 */ CarbonPlayer player(); /** * The channel the player is switching to. * * @since 3.0.0 */ ChatChannel channel(); /** * Sets the player's new channel. * * @param chatChannel the new channel * @since 3.0.0 */ void channel(final ChatChannel chatChannel); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/events/PartyJoinEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 3.0.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 3.0.0 */ UUID playerId(); /** * The party being joined. * *

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

* * @return party * @since 3.0.0 */ Party party(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/event/events/PartyLeaveEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 3.0.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 3.0.0 */ UUID playerId(); /** * The party being left. * *

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

* * @return party * @since 3.0.0 */ Party party(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/users/CarbonPlayer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.List; 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; import net.kyori.adventure.identity.Identified; 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; /** * Generic abstraction for players. * * @since 2.0.0 */ @DefaultQualifier(NonNull.class) public interface CarbonPlayer extends Audience, Identified { /** * Returns the distance from the other {@link CarbonPlayer}, or -1 if the players are not in the same world. * * @param other the other player * @return the distance from the other player, or -1 * @since 3.0.0 */ double distanceSquaredFrom(CarbonPlayer other); /** * Returns if both players are in the same world or server. * * @param other the other player * @return if both players are in the same world/server * @since 3.0.0 */ boolean sameWorldAs(CarbonPlayer other); /** * Gets the player's username. * * @return the player's username * @since 2.0.0 */ String username(); /** * Returns the player's display name. * *

The display name is the effective or displayed name of a player. * When the player has a nickname set, either through Carbon or the platform, * it will be reflected here. Else, a plain text component representing * the player's name may be returned.

* * @return the player's display name * @since 3.0.0 */ Component displayName(); /** * Checks if the player has a nickname set. * *

Will always return {@code false} when Carbon's nickname management is disabled.

* * @return if the player has a nickname set * @see #nickname() * @see #nickname(Component) * @since 3.0.0 */ boolean hasNickname(); /** * Gets the player's nickname, shown in places like chat and tab menu. * *

Will always return {@code null} when Carbon's nickname management is disabled.

* * @return the player's nickname * @see #hasNickname() * @see #nickname(Component) * @see #displayName() * @since 3.0.0 */ @Nullable Component nickname(); /** * Sets the player's nickname. * *

Setting {@code null} will remove any current nickname.

* *

Won't have any visible effect when Carbon's nickname management is disabled.

* * @param nickname the new nickname * @see #hasNickname() * @see #nickname() * @see #displayName() * @since 3.0.0 */ void nickname(@Nullable Component nickname); /** * The player's UUID, often used for identification purposes. * * @return the player's UUID * @since 2.0.0 */ UUID uuid(); /** * Creates the chat component for the item in the {@link InventorySlot}, or null if the slot is empty. * * @param slot the inventory slot containing the item * @return the chat component for the item in the slot, or null if the slot is empty * @since 2.0.0 */ @Nullable Component createItemHoverComponent(InventorySlot slot); /** * The player's locale. * * @return the player's locale, or null if offline * @since 2.0.0 */ @Nullable Locale locale(); /** * The player's selected channel, or null if one isn't set. * * @return the player's selected channel * @since 2.0.0 */ @Nullable ChatChannel selectedChannel(); /** * Sets the player's selected channel. * * @param chatChannel the channel * @since 2.0.0 */ void selectedChannel(@Nullable ChatChannel chatChannel); /** * Determines which channel the message should go to, and removes any channel prefixes from the message. * * @param message the message to be sent * @return the channel and message * @since 3.0.0 */ ChannelMessage channelForMessage(Component message); /** * A message and which channel it should be sent in. * * @param message The channel message without any prefixes * @param channel The channel the message should be sent to * @since 3.0.0 */ record ChannelMessage(Component message, ChatChannel channel) {} /** * Checks if the player has the specified permission. * * @param permission the permission to check * @return if the player has the permission * @since 2.0.0 */ boolean hasPermission(String permission); /** * Returns the player's primary group. * * @return the player's primary group * @since 2.0.0 */ String primaryGroup(); /** * Returns the complete list of groups the player is in. * * @return the groups the player is in * @since 2.0.0 */ List groups(); /** * Returns if the player is muted. * * @return if the player is muted * @since 2.0.0 */ boolean muted(); /** * The time the mute will expire, in epoch millis. * * @return the mute expiration time * @since 3.0.0 */ long muteExpiration(); /** * Mutes and unmutes the player. * * @param muted if the player is now muted * @since 2.0.0 */ void muted(boolean muted); /** * Sets the epoch time the player's mute will expire. * * @param epochMillis the expiration time * @since 3.0.0 */ void muteExpiration(long epochMillis); /** * Gets the ids of the players this player is currently ignoring. * * @return the players currently ignored * @since 3.0.0 */ Set ignoring(); /** * Checks if the other player is being ignored by this player. * * @param player the potential source of a message * @return if this player is ignoring the sender * @since 2.0.5 */ boolean ignoring(UUID player); /** * Checks if the other player is being ignored by this player. * * @param player the potential source of a message * @return if this player is ignoring the sender * @since 2.0.0 */ boolean ignoring(CarbonPlayer player); /** * Adds the player to and removes the player from the ignore list. * * @param player the player to be added/removed * @param nowIgnoring if the player should be ignored * @since 2.0.0 */ void ignoring(UUID player, boolean nowIgnoring); /** * Adds the player to and removes the player from the ignore list. * * @param player the player to be added/removed * @param nowIgnoring if the player should be ignored * @since 2.0.0 */ void ignoring(CarbonPlayer player, boolean nowIgnoring); /** * Returns if the player is deafened and unable to read messages. * * @return if the player is deafened * @since 2.0.0 */ boolean deafened(); /** * Deafens and undeafens the player. * * @since 2.0.0 */ void deafened(boolean deafened); /** * Returns if the player is spying on messages and able to read muted/private messages. * * @return if the player is spying on messages * @since 2.0.0 */ boolean spying(); /** * Sets and unsets the player's ability to spy. * * @since 2.0.0 */ void spying(boolean spying); /** * Controls if the player should receive direct messages or if they should be hidden. * * @return if the player is ignoring direct messages * @since 3.0.0 */ boolean ignoringDirectMessages(); /** * Sets whether the player should receive direct messages or if they should be hidden. * * @param ignoring if the player is ignoring direct messages * @since 3.0.0 */ void ignoringDirectMessages(boolean ignoring); /** * Sends the message as the player. * * @param message the message to be sent * @since 2.0.0 */ void sendMessageAsPlayer(String message); /** * Returns whether the player is online. * * @return if the player is online. * @since 2.0.0 */ boolean online(); /** * The UUID of the player that replies will be sent to. * * @return the player's reply target * @since 2.0.0 */ @Nullable UUID whisperReplyTarget(); /** * Sets the whisper reply target for this player. * * @param uuid the uuid of the reply target * @since 2.0.0 */ void whisperReplyTarget(@Nullable UUID uuid); /** * The last player this player has whispered. * * @return the player's last whisper target * @since 2.0.0 */ @Nullable UUID lastWhisperTarget(); /** * Sets the last player this player has whispered. * * @param uuid the uuid of the whisper target * @since 2.0.0 */ void lastWhisperTarget(@Nullable UUID uuid); /** * If this player is vanished in another supported plugin. * Other players will be unaware of this player. * There is no way to set this state through Carbon, we do not store this information; but merely bridge it. * * @return If this player is vanished in another plugin. * @since 2.0.0 */ boolean vanished(); /** * Whether this player can see the other player. * * @param other the other, potentially vanished, player * @return if this player is aware of the other player * @since 2.0.0 */ boolean awareOf(CarbonPlayer other); /** * A list of all the channels the player has left * using the leave command. * *

The returned collection is immutable, use * {@link #joinChannel(ChatChannel)} and {@link #leaveChannel(ChatChannel)} to mutate.

* * @return a list of the channels. * @since 3.0.0 */ List leftChannels(); /** * Join a channel for this player if they have left it. * * @param channel the channel to join. * @since 3.0.0 */ void joinChannel(ChatChannel channel); /** * Leave a channel for this player. * * @param channel the channel to leave. * @since 3.0.0 */ void leaveChannel(ChatChannel channel); /** * Get this player's current {@link Party}. * * @return party future * @since 3.0.0 */ CompletableFuture<@Nullable Party> party(); /** * Whether the optional chat filters apply to messages send to this player or not. * * @return if this player's using the optional chat filters * @since 3.0.0 */ boolean applyOptionalChatFilters(); /** * Whether the optional chat filters apply to messages send to this player or not. * * @param applyOptionalChatFilters if this player's using the optional chat filters * @since 3.0.0 */ void applyOptionalChatFilters(boolean applyOptionalChatFilters); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/users/Party.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 3.0.0 */ @DefaultQualifier(NonNull.class) public interface Party { /** * Get the name of this party. * * @return party name * @since 3.0.0 */ Component name(); /** * Get the unique id of this party. * * @return party id * @since 3.0.0 */ UUID id(); /** * Get a snapshot of the current party members. * * @return party members * @since 3.0.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 3.0.0 */ void addMember(UUID id); /** * Remove a user from this party. * * @param id user id * @since 3.0.0 */ void removeMember(UUID id); /** * Disband this party. Will remove all members and delete persistent data. * * @since 3.0.0 */ void disband(); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/users/UserManager.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.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; /** * Manager used to load/obtain and save {@link CarbonPlayer CarbonPlayers}. * * @since 2.0.0 */ @DefaultQualifier(NonNull.class) public interface UserManager { /** * Gets the {@link CarbonPlayer} for the provided player {@link UUID}, whether they are online or not. * *

Note that the returned user object/future is not guaranteed to be the same for subsequent calls.

* *

Because of this, the return value should not be cached, it should be queried each time it is needed. The implementation handles caching as is appropriate.

* * @param uuid the player's id * @return the player * @since 3.0.0 */ 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 3.0.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 3.0.0 */ CompletableFuture<@Nullable Party> party(UUID id); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/util/ChatComponentRenderer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.util; import net.draycia.carbon.api.users.CarbonPlayer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * Renderer used to construct chat components on a per-player basis. * * @since 2.0.0 */ @FunctionalInterface @DefaultQualifier(NonNull.class) public interface ChatComponentRenderer { /** * Renders a Component for the specified recipient. * * @param sender the player that sent the message * @param recipient a recipient of the message. * may be a player, console, or other Audience implementations * @param message the message being sent * @param originalMessage the original message that was sent * @return the component to be shown to the recipient, * or empty if the recipient should not receive the message * @since 2.0.0 */ Component render(CarbonPlayer sender, Audience recipient, Component message, Component originalMessage); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/util/InventorySlot.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.util; import java.util.List; /** * A slot in a player's inventory. * * @since 2.0.0 */ public final class InventorySlot { /** * An {@link InventorySlot} instance, usable in chat with the given placeholders. * * @param placeholders the placeholders that can be used in chat * @return the instance * @since 2.0.0 */ public static InventorySlot of(final String... placeholders) { return new InventorySlot(placeholders); } private final List placeholders; private InventorySlot(final String... placeholders) { this.placeholders = List.of(placeholders); } /** * Returns this slot's placeholders, which can be used in chat to show the item in said slot. * * @return this slot's placeholders * @since 2.0.0 */ public List placeholders() { return this.placeholders; } public static final InventorySlot HELMET = InventorySlot.of("helm", "helmet", "hat", "head"); public static final InventorySlot CHEST = InventorySlot.of("chest", "chestplate"); public static final InventorySlot LEGS = InventorySlot.of("legs", "leggings"); public static final InventorySlot BOOTS = InventorySlot.of("boots", "feet"); public static final InventorySlot MAIN_HAND = InventorySlot.of("main_hand", "hand", "item"); public static final InventorySlot OFF_HAND = InventorySlot.of("off_hand"); public static List SLOTS = List.of(HELMET, CHEST, LEGS, BOOTS, MAIN_HAND, OFF_HAND); } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/util/KeyedRenderer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.util; import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Keyed; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * A {@link ChatComponentRenderer chat renderer} that's identifiable by key. * * @since 2.0.0 */ @DefaultQualifier(NonNull.class) public interface KeyedRenderer extends Keyed, ChatComponentRenderer { /** * Creates a new renderer with the corresponding key. * * @param key the renderer's key * @param renderer the chat renderer * @return the keyed renderer * @since 2.0.0 */ static KeyedRenderer keyedRenderer(final Key key, final ChatComponentRenderer renderer) { return new KeyedRendererImpl(key, renderer); } } ================================================ FILE: api/src/main/java/net/draycia/carbon/api/util/KeyedRendererImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.util; import net.draycia.carbon.api.users.CarbonPlayer; import net.kyori.adventure.audience.Audience; 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; @DefaultQualifier(NonNull.class) record KeyedRendererImpl(Key key, ChatComponentRenderer renderer) implements KeyedRenderer { @Override public Component render( final CarbonPlayer sender, final Audience recipient, final Component message, final Component originalMessage ) { return this.renderer.render(sender, recipient, message, originalMessage); } } ================================================ FILE: build-logic/build.gradle.kts ================================================ plugins { `kotlin-dsl` } repositories { gradlePluginPortal() maven("https://oss.sonatype.org/content/repositories/snapshots/") { mavenContent { snapshotsOnly() } } } dependencies { implementation(libs.shadow) implementation(libs.indraCommon) implementation(libs.cloud.build.logic) implementation(libs.indraLicenseHeader) implementation(libs.mod.publish.plugin) implementation(libs.configurateYaml) implementation(libs.gremlin.gradle) implementation(libs.run.task) implementation(libs.gson) // https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } ================================================ FILE: build-logic/settings.gradle.kts ================================================ dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } } ================================================ FILE: build-logic/src/main/kotlin/CarbonPermissionsExtension.kt ================================================ import io.leangen.geantyref.TypeToken import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.listProperty import org.spongepowered.configurate.ConfigurationNode import org.spongepowered.configurate.yaml.NodeStyle import org.spongepowered.configurate.yaml.YamlConfigurationLoader import javax.inject.Inject abstract class CarbonPermissionsExtension @Inject constructor(private val objects: ObjectFactory) { abstract val yaml: RegularFileProperty val permissions: Provider> = create() private fun create(): ListProperty = objects.listProperty().also { it.set(yaml.map { file -> val loader = YamlConfigurationLoader.builder() .path(file.asFile.toPath()) .nodeStyle(NodeStyle.BLOCK) .build() loader.load().childrenMap().map { (name, child) -> loadPermission(name as String, child) } }) it.disallowChanges() it.finalizeValueOnRead() } private fun loadPermission(name: String, node: ConfigurationNode): Permission { return if (node.isMap) { Permission( name, node.node("description").string, node.node("children").takeIf { c -> !c.virtual() } ?.get(object : TypeToken>() {}) ) } else { Permission(name, node.string, null) } } data class Permission( val string: String, val description: String?, val children: Map? ) } ================================================ FILE: build-logic/src/main/kotlin/CarbonPlatformExtension.kt ================================================ import org.gradle.api.file.RegularFileProperty abstract class CarbonPlatformExtension { abstract val productionJar: RegularFileProperty } ================================================ FILE: build-logic/src/main/kotlin/ConfigurablePluginsExt.kt ================================================ import org.gradle.api.Action import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.api.artifacts.MinimalExternalModuleDependency import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Provider abstract class ConfigurablePluginsExt { data class DepPlugin( val dep: Provider, val op: Action?, val defaultEnabled: Boolean = false, val name: String = dep.get().name ) abstract val gradleDependencyBased: ListProperty fun dependency(lib: Provider, op: Action? = null) { gradleDependencyBased.add(DepPlugin(lib, op)) } fun dependency(lib: Provider, defaultEnabled: Boolean, op: Action? = null) { gradleDependencyBased.add(DepPlugin(lib, op, defaultEnabled)) } } ================================================ FILE: build-logic/src/main/kotlin/FetchLuckPermsDownloads.kt ================================================ import org.gradle.api.DefaultTask import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.UntrackedTask import java.net.URI import javax.inject.Inject @UntrackedTask(because = "Always check for new metadata") abstract class FetchLuckPermsDownloads : DefaultTask() { companion object { const val ENDPOINT: String = "https://metadata.luckperms.net/data/downloads" } @get:Inject abstract val layout: ProjectLayout @get:OutputFile abstract val outputFile: RegularFileProperty init { init() } private fun init() { outputFile.convention(layout.buildDirectory.file("luckperms/downloads.json")) } @TaskAction fun run () { val url = URI.create(ENDPOINT).toURL() val data = url.readText(Charsets.UTF_8) val outFile = outputFile.get().asFile.also { it.parentFile.mkdirs() if (it.exists()) { it.delete() } } outFile.writeText(data, Charsets.UTF_8) } } ================================================ FILE: build-logic/src/main/kotlin/FetchLuckPermsJar.kt ================================================ import com.google.gson.Gson import com.google.gson.JsonElement import org.gradle.api.DefaultTask import org.gradle.api.Project import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.register import java.net.URI import javax.inject.Inject @CacheableTask abstract class FetchLuckPermsJar : DefaultTask() { companion object { fun setup( project: Project, type: String, ): TaskProvider { val getMeta = project.tasks.register("fetchLuckPermsDownloads") return project.tasks.register("fetchLuckPermsJar") { this.type.set(type) inputFile.set(getMeta.flatMap { it.outputFile }) } } } @get:Inject abstract val layout: ProjectLayout @get:Input abstract val type: Property @get:InputFile @get:PathSensitive(PathSensitivity.NONE) abstract val inputFile: RegularFileProperty @get:OutputFile abstract val outputFile: RegularFileProperty init { init() } private fun init() { outputFile.convention(type.flatMap { layout.buildDirectory.file("luckperms/${it}.jar") }) } @TaskAction fun run () { val json = inputFile.get().asFile.readText(Charsets.UTF_8) val map = Gson().fromJson(json, JsonElement::class.java).asJsonObject.get("downloads").asJsonObject val url = map.get(type.get()).asString val data = URI.create(url).toURL().readBytes() val outFile = outputFile.get().asFile.also { it.parentFile.mkdirs() if (it.exists()) { it.delete() } } outFile.writeBytes(data) } } ================================================ FILE: build-logic/src/main/kotlin/FileCopyTask.kt ================================================ import org.gradle.api.DefaultTask import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction abstract class FileCopyTask : DefaultTask() { @InputFile val fileToCopy = project.objects.fileProperty() @OutputFile val destination = project.objects.fileProperty() @TaskAction fun copyFile() { destination.get().asFile.parentFile.mkdirs() fileToCopy.get().asFile.copyTo(destination.get().asFile, overwrite = true) } } ================================================ FILE: build-logic/src/main/kotlin/carbon.base-conventions.gradle.kts ================================================ plugins { id("net.kyori.indra") id("net.kyori.indra.git") id("net.kyori.indra.checkstyle") id("net.kyori.indra.licenser.spotless") } version = rootProject.version indra { gpl3OnlyLicense() javaVersions { target(21) } github(GITHUB_ORGANIZATION, GITHUB_REPO) } spotless { java { targetExclude( "src/main/java/net/draycia/carbon/common/messages/PrefixedDelegateIterator.java", "src/main/java/net/draycia/carbon/common/messages/StandardPlaceholderResolverStrategyButDifferent.java", "src/main/java/com/google/inject/assistedinject/**" ) } } indraSpotlessLicenser { licenseHeaderFile(rootProject.file("LICENSE_HEADER")) } tasks { withType { // disable unclaimed annotation and missing annotation warnings options.compilerArgs.add("-Xlint:-processing,-classfile") options.compilerArgs.add("-parameters") } } dependencies { checkstyle(libs.stylecheck) } ================================================ FILE: build-logic/src/main/kotlin/carbon.build-logic.gradle.kts ================================================ plugins { id("base") } ================================================ FILE: build-logic/src/main/kotlin/carbon.configurable-plugins.gradle.kts ================================================ import org.spongepowered.configurate.objectmapping.ConfigSerializable import org.spongepowered.configurate.yaml.NodeStyle import org.spongepowered.configurate.yaml.YamlConfigurationLoader import xyz.jpenilla.runtask.task.RunWithPlugins val pluginsExt = extensions.create("configurablePlugins", ConfigurablePluginsExt::class.java) afterEvaluate { val configs = pluginsExt.gradleDependencyBased.get().map { entry -> val c = configurations.register(entry.name + "Plugin") { isTransitive = false } dependencies { c.name(entry.dep) { entry.op?.execute(this) } } entry to c } tasks.withType(RunWithPlugins::class).configureEach { val cfg = readConfig() configs.forEach { (entry, configuration) -> val enabled = cfg.taskOverrides[name]?.get(entry.name) ?: cfg.defaults[entry.name] ?: false if (enabled) { pluginJars.from(configuration) } } } } @ConfigSerializable class Config { var defaults: MutableMap = mutableMapOf() var taskOverrides: MutableMap> = mutableMapOf( "someTaskName" to mutableMapOf("somePlugin" to false) ) } @Synchronized fun readConfig(): Config { val loader = YamlConfigurationLoader.builder() .file(file("run-plugins.yml")) .nodeStyle(NodeStyle.BLOCK) .defaultOptions { it.header("Enable and disable optional plugins for run tasks in this project") } .build() val n = loader.load() val c = n.get(Config::class.java) as Config var write = false for (e in pluginsExt.gradleDependencyBased.get()) { if (!c.defaults.containsKey(e.name)) { write = true c.defaults[e.name] = e.defaultEnabled } } if (write) { n.set(c) loader.save(n) } return c } ================================================ FILE: build-logic/src/main/kotlin/carbon.permissions.gradle.kts ================================================ val ext = extensions.create("carbonPermission", CarbonPermissionsExtension::class.java) ext.yaml.convention(rootProject.layout.projectDirectory.file("common/src/main/resources/carbon-permissions.yml")) ================================================ FILE: build-logic/src/main/kotlin/carbon.platform-conventions.gradle.kts ================================================ import me.modmuss50.mpp.ReleaseType plugins { id("carbon.base-conventions") id("me.modmuss50.mod-publish-plugin") id("xyz.jpenilla.gremlin-gradle") } decorateVersion() configurations.runtimeDownload { exclude("org.slf4j", "slf4j-api") exclude("com.google.errorprone", "error_prone_annotations") exclude("io.leangen.geantyref", "geantyref") } val platformExtension = extensions.create("carbonPlatform") dependencies { runtimeDownload(libs.h2) runtimeDownload(libs.postgresql) runtimeDownload(libs.mariadb) runtimeDownload(libs.zstdjni) runtimeDownload(libs.jdbiCore) runtimeDownload(libs.jdbiObject) runtimeDownload(libs.jdbiPostgres) runtimeDownload(libs.caffeine) runtimeDownload(libs.jedis) { exclude("com.google.code.gson", "gson") } runtimeDownload(libs.rabbitmq) runtimeDownload(libs.nats) runtimeDownload(libs.guice) { exclude("com.google.guava") } runtimeDownload(libs.assistedInject) { isTransitive = false } runtimeDownload(libs.flyway) { exclude("com.google.code.gson", "gson") } runtimeDownload(libs.flywayMysql) { isTransitive = false } runtimeDownload(libs.flywayPostgres) { isTransitive = false } } tasks { jar { manifest { attributes( "carbon-version" to project.version, "carbon-commit" to lastCommitHash(), "carbon-branch" to currentBranch(), ) } } val copyJar = register("copyJar") { fileToCopy = platformExtension.productionJar destination = rootProject.layout.buildDirectory.dir("libs").flatMap { it.file(fileToCopy.map { file -> file.asFile.name }) } } build { dependsOn(copyJar) } javadoc { enabled = false } } val projectVersion = project.version as String publishMods.modrinth { projectId = "QzooIsZI" type = if (projectVersion.contains("-beta.")) ReleaseType.BETA else ReleaseType.STABLE file = platformExtension.productionJar changelog = releaseNotes accessToken = providers.environmentVariable("MODRINTH_TOKEN") requires("luckperms") optional("miniplaceholders") minecraftVersions.addAll( "1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11", "26.1", "26.1.1", "26.1.2", ) } tasks.writeDependencies { outputFileName = "carbon-dependencies.txt" repos.add("https://repo.papermc.io/repository/maven-public/") repos.add("https://repo.maven.apache.org/maven2/") } gremlin { defaultJarRelocatorDependencies = false defaultGremlinRuntimeDependency = false } ================================================ FILE: build-logic/src/main/kotlin/carbon.publishing-conventions.gradle.kts ================================================ plugins { id("carbon.base-conventions") id("net.kyori.indra.publishing") id("org.incendo.cloud-build-logic.publishing") } signing { val signingKey: String? by project val signingPassword: String? by project useInMemoryPgpKeys(signingKey, signingPassword) } indra { configurePublications { pom { developers { developer { id.set("Vicarious") name.set("Josua Parks") } developer { id.set("jmp") name.set("Jason Penilla") } } } } } javadocLinks { defaultJavadocProvider = "https://www.javadocs.dev/{group}/{name}/{version}" } ================================================ FILE: build-logic/src/main/kotlin/carbon.shadow-platform.gradle.kts ================================================ plugins { id("carbon.platform-conventions") id("com.gradleup.shadow") } tasks { jar { archiveClassifier = "unshaded" } shadowJar { archiveClassifier.set(null as String?) configureShadowJar() mergeServiceFiles() // Needed for mergeServiceFiles to work properly in Shadow 9+ filesMatching("META-INF/services/**") { duplicatesStrategy = DuplicatesStrategy.INCLUDE } } } extensions.configure { productionJar = tasks.shadowJar.flatMap { it.archiveFile } } ================================================ FILE: build-logic/src/main/kotlin/constants.kt ================================================ const val GITHUB_ORGANIZATION = "Hexaoxide" const val GITHUB_REPO = "Carbon" const val GITHUB_REPO_URL = "https://github.com/$GITHUB_ORGANIZATION/$GITHUB_REPO" ================================================ FILE: build-logic/src/main/kotlin/extensions.kt ================================================ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import net.kyori.indra.git.IndraGitExtension import org.apache.tools.ant.filters.ReplaceTokens import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.filter import org.gradle.kotlin.dsl.the import org.gradle.language.jvm.tasks.ProcessResources import xyz.jpenilla.gremlin.gradle.ShadowGremlin fun ProcessResources.replace( pattern: String, tokens: Map ) { inputs.properties(tokens) filesMatching(pattern) { filter( "beginToken" to "\${", "endToken" to "}", "tokens" to tokens ) } } val Project.releaseNotes: Provider get() = providers.environmentVariable("RELEASE_NOTES") /** * Relocate a package into the `carbonchat.libs.` namespace. */ fun Task.relocateDependency(pkg: String) { ShadowGremlin.relocateWithPrefix(this, "carbonchat.libs", pkg) } fun Task.standardRuntimeRelocations() { relocateDependency("com.github.benmanes") // relocateDependency("com.github.luben.zstd") // natives don't like relocation - hopefully nothing breaks :) relocateDependency("com.google.protobuf") relocateDependency("com.mysql.cj") relocateDependency("com.mysql.jdbc") relocateDependency("com.rabbitmq") relocateDependency("io.nats") relocateDependency("net.i2p.crypto") relocateDependency("org.apache.commons.pool2") relocateDependency("org.jdbi") relocateDependency("org.mariadb.jdbc") relocateDependency("org.postgresql") relocateDependency("redis.clients.jedis") relocateDependency("org.flywaydb") relocateDependency("com.fasterxml") relocateDependency("org.h2") } /** * Relocates dependencies which we bundle and relocate on all platforms. */ fun Task.standardRelocations() { relocateDependency("org.bstats") relocateDependency("net.kyori.adventure.serializer.configurate4") relocateDependency("com.sasorio.event") relocateDependency("net.kyori.moonshine") relocateDependency("com.seiama.registry") relocateDependency("org.spongepowered.configurate") relocateDependency("com.google.thirdparty.publicsuffix") relocateDependency("com.zaxxer.hikari") relocateDependency("ninja.egg82.messenger") relocateDependency("org.antlr") relocateDependency("com.electronwill") relocateDependency("xyz.jpenilla.gremlin") } fun Task.relocateCloud() { relocateDependency("org.incendo.cloud") } fun Task.relocateGuice() { relocateDependency("com.google.inject") relocateDependency("org.aopalliance") relocateDependency("jakarta.inject") } fun ShadowJar.configureShadowJar() { //minimize() standardRelocations() dependencies { // not needed or provided by platform at runtime exclude(dependency("com.google.code.findbugs:jsr305")) exclude(dependency("com.google.errorprone:error_prone_annotations")) exclude { it.moduleGroup == "com.google.guava" } exclude(dependency("com.google.j2objc:j2objc-annotations")) exclude(dependency("io.netty:netty-all")) exclude(dependency("io.netty:netty-buffer")) exclude(dependency("it.unimi.dsi:fastutil")) exclude(dependency("org.checkerframework:checker-qual")) exclude(dependency("org.slf4j:slf4j-api")) } } fun Project.lastCommitHash(): String = the().commit().orNull?.name?.substring(0, 7) ?: error("Could not determine commit hash") fun Project.decorateVersion() { val versionString = version as String version = if (versionString.endsWith("-SNAPSHOT")) { "$versionString+${lastCommitHash()}" } else { versionString } } fun Project.currentBranch(): String { System.getenv("GITHUB_HEAD_REF")?.takeIf { it.isNotEmpty() } ?.let { return it } System.getenv("GITHUB_REF")?.takeIf { it.isNotEmpty() } ?.let { return it.replaceFirst("refs/heads/", "") } val indraGit = the().takeIf { it.isPresent } return indraGit?.branchName()?.orNull ?: "detached-head" } val Project.libs: LibrariesForLibs get() = the() ================================================ FILE: build.gradle.kts ================================================ plugins { id("carbon.build-logic") alias(libs.plugins.hangar.publish) alias(libs.plugins.cloud.buildLogic.rootProject.publishing) } val projectVersion: String by project // get from gradle.properties version = projectVersion fun Project.platformJar(): Provider = extensions.getByType().productionJar hangarPublish.publications.register("plugin") { version = projectVersion id = "Carbon" channel = if (projectVersion.contains("-beta.")) "Beta" else "Release" changelog = releaseNotes apiKey = providers.environmentVariable("HANGAR_UPLOAD_KEY") platforms.paper { jar = project(":carbonchat-paper").platformJar() platformVersions.add("1.21.4-26.1.2") dependencies { url("LuckPerms", "https://luckperms.net/") hangar("Essentials") { required = false } url("DiscordSRV", "https://www.spigotmc.org/resources/discordsrv.18494/") { required = false } url("PlaceholderAPI", "https://www.spigotmc.org/resources/placeholderapi.6245/") { required = false } hangar("MiniPlaceholders") { required = false } } } platforms.velocity { jar = project(":carbonchat-velocity").platformJar() platformVersions.add("3.5") dependencies { url("LuckPerms", "https://luckperms.net/") hangar("MiniPlaceholders") { required = false } hangar("SignedVelocity") { required = false } } } } ================================================ FILE: common/build.gradle.kts ================================================ plugins { id("carbon.base-conventions") } dependencies { api(projects.carbonchatApi) api(libs.gremlin.runtime) compileOnlyApi(platform(libs.log4jBom)) compileOnlyApi(libs.log4jApi) // Configs api(libs.configurateHocon) { // Provided at platform level (usually through adventure) exclude("net.kyori", "option") } // Bring in option for -common compile compileOnly(libs.configurateHocon) api(libs.adventureSerializerConfigurate4) { isTransitive = false } // Cloud api(platform(libs.cloudBom)) api(libs.cloudCore) api(platform(libs.cloudMinecraftBom)) api(libs.cloudMinecraftExtras) { isTransitive = false } api(libs.cloudSigned) // Other compileOnlyApi(libs.guice) { exclude("com.google.guava") } compileOnlyApi(libs.assistedInject) { isTransitive = false } compileOnlyApi(libs.luckPermsApi) compileOnlyApi(libs.event) // Storage compileOnlyApi(libs.jdbiCore) compileOnlyApi(libs.jdbiObject) compileOnlyApi(libs.jdbiPostgres) api(libs.hikariCP) compileOnlyApi(libs.flyway) { exclude("com.google.code.gson", "gson") } compileOnlyApi(libs.flywayMysql) { isTransitive = false } compileOnlyApi(libs.flywayPostgres) { isTransitive = false } // Messaging api(libs.messenger) api(libs.messengerNats) api(libs.messengerRabbitmq) api(libs.messengerRedis) compileOnlyApi(libs.netty) api(libs.event) api(libs.registry) { exclude("com.google.guava") } api(libs.kyoriMoonshine) api(libs.kyoriMoonshineCore) api(libs.kyoriMoonshineStandard) compileOnlyApi(libs.caffeine) // we shade and relocate a newer version than minecraft provides compileOnlyApi(libs.guava) // Plugins compileOnly(libs.miniplaceholders) } ================================================ FILE: common/src/main/java/com/google/inject/assistedinject/FactoryProvider3.java ================================================ /* * Copyright (C) 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.inject.assistedinject; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.getOnlyElement; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.inject.AbstractModule; import com.google.inject.Binder; import com.google.inject.Binding; import com.google.inject.ConfigurationException; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.ProvisionException; import com.google.inject.Scopes; import com.google.inject.TypeLiteral; import com.google.inject.internal.Annotations; import com.google.inject.internal.Errors; import com.google.inject.internal.ErrorsException; import com.google.inject.internal.UniqueAnnotations; import com.google.inject.internal.util.Classes; import com.google.inject.spi.BindingTargetVisitor; import com.google.inject.spi.Dependency; import com.google.inject.spi.HasDependencies; import com.google.inject.spi.InjectionPoint; import com.google.inject.spi.Message; import com.google.inject.spi.ProviderInstanceBinding; import com.google.inject.spi.ProviderWithExtensionVisitor; import com.google.inject.spi.Toolable; import com.google.inject.util.Providers; import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; /** * The newer implementation of factory provider. This implementation uses a child injector to create * values. * *

Carbon - modified from FactoryProvider2 for default method support

* * @author jessewilson@google.com (Jesse Wilson) * @author dtm@google.com (Daniel Martin) * @author schmitt@google.com (Peter Schmitt) * @author sameb@google.com (Sam Berlin) */ public final class FactoryProvider3 // Carbon - public implements InvocationHandler, ProviderWithExtensionVisitor, HasDependencies, AssistedInjectBinding { /** A constant annotation to denote the return value, instead of creating a new one each time. */ static final Annotation RETURN_ANNOTATION = UniqueAnnotations.create(); // use the logger under a well-known name, not FactoryProvider2 static final Logger logger = Logger.getLogger(AssistedInject.class.getName()); /** * A constant that determines if we allow fallback to using the JDK internals to make a "private * lookup". Typically always true, but reflectively set to false in tests. */ @SuppressWarnings("FieldCanBeFinal") // non-final for testing private static boolean allowPrivateLookupFallback = true; /** * A constant that determines if we allow fallback to using method handle workarounds (if * required). Typically always true, but reflectively set to false in tests. */ @SuppressWarnings("FieldCanBeFinal") // non-final for testing private static boolean allowMethodHandleWorkaround = true; /** if a factory method parameter isn't annotated, it gets this annotation. */ static final Assisted DEFAULT_ANNOTATION = new Assisted() { @Override public String value() { return ""; } @Override public Class annotationType() { return Assisted.class; } @Override public boolean equals(Object o) { return o instanceof Assisted && ((Assisted) o).value().isEmpty(); } @Override public int hashCode() { return 127 * "value".hashCode() ^ "".hashCode(); } @Override public String toString() { return "@" + Assisted.class.getName() + "(" + Annotations.memberValueString("value", "") + ")"; } }; /** All the data necessary to perform an assisted inject. */ private static class AssistData implements AssistedMethod { /** the constructor the implementation is constructed with. */ final Constructor constructor; /** the return type in the factory method that the constructor is bound to. */ final Key returnType; /** the parameters in the factory method associated with this data. */ final ImmutableList> paramTypes; /** the type of the implementation constructed */ final TypeLiteral implementationType; /** All non-assisted dependencies required by this method. */ final Set> dependencies; /** The factory method associated with this data */ final Method factoryMethod; /** true if {@link #isValidForOptimizedAssistedInject} returned true. */ final boolean optimized; /** the list of optimized providers, empty if not optimized. */ final List providers; /** used to perform optimized factory creations. */ volatile Binding cachedBinding; // TODO: volatile necessary? AssistData( Constructor constructor, Key returnType, ImmutableList> paramTypes, TypeLiteral implementationType, Method factoryMethod, Set> dependencies, boolean optimized, List providers) { this.constructor = constructor; this.returnType = returnType; this.paramTypes = paramTypes; this.implementationType = implementationType; this.factoryMethod = factoryMethod; this.dependencies = dependencies; this.optimized = optimized; this.providers = providers; } @Override public String toString() { return MoreObjects.toStringHelper(getClass()) .add("ctor", constructor) .add("return type", returnType) .add("param type", paramTypes) .add("implementation type", implementationType) .add("dependencies", dependencies) .add("factory method", factoryMethod) .add("optimized", optimized) .add("providers", providers) .add("cached binding", cachedBinding) .toString(); } @Override public Set> getDependencies() { return dependencies; } @Override public Method getFactoryMethod() { return factoryMethod; } @Override public Constructor getImplementationConstructor() { return constructor; } @Override public TypeLiteral getImplementationType() { return implementationType; } } /** Mapping from method to the data about how the method will be assisted. */ private final ImmutableMap assistDataByMethod; /** Mapping from method to method handle, for generated default methods. */ private final ImmutableMap methodHandleByMethod; /** the hosting injector, or null if we haven't been initialized yet */ private Injector injector; /** the factory interface, implemented and provided */ private final F factory; /** The key that this is bound to. */ private final Key factoryKey; /** The binding collector, for equality/hashing purposes. */ private final BindingCollector collector; /** * Utility class for collecting factory bindings. Used for configuring {@link FactoryProvider3}. * * @author schmitt@google.com (Peter Schmitt) */ static class BindingCollector { private final Map, TypeLiteral> bindings = new HashMap<>(); public BindingCollector addBinding(Key key, TypeLiteral target) { if (bindings.containsKey(key)) { throw new ConfigurationException( ImmutableSet.of(new Message("Only one implementation can be specified for " + key))); } bindings.put(key, target); return this; } public Map, TypeLiteral> getBindings() { return Collections.unmodifiableMap(bindings); } @Override public int hashCode() { return bindings.hashCode(); } @Override public boolean equals(Object obj) { return (obj instanceof BindingCollector) && bindings.equals(((BindingCollector) obj).bindings); } } /** * @param factoryKey a key for a Java interface that defines one or more create methods. * @param collector binding configuration that maps method return types to implementation types. * @param userLookups user provided lookups, optional. */ public FactoryProvider3( Key factoryKey, BindingCollector collector, MethodHandles.Lookup userLookups) { this.factoryKey = factoryKey; this.collector = collector == null ? new BindingCollector() : collector; collector = this.collector; // Carbon TypeLiteral factoryType = factoryKey.getTypeLiteral(); Errors errors = new Errors(); @SuppressWarnings("unchecked") // we imprecisely treat the class literal of T as a Class Class factoryRawType = (Class) (Class) factoryType.getRawType(); try { if (!factoryRawType.isInterface()) { throw errors.addMessage("%s must be an interface.", factoryRawType).toException(); } Multimap defaultMethods = HashMultimap.create(); Multimap otherMethods = HashMultimap.create(); ImmutableMap.Builder assistDataBuilder = ImmutableMap.builder(); // TODO: also grab methods from superinterfaces for (Method method : factoryRawType.getMethods()) { // Skip static methods if (Modifier.isStatic(method.getModifiers())) { continue; } // Skip default methods that java8 may have created. if (method.isDefault()/*isDefault(method) && (method.isBridge() || method.isSynthetic())*/) { // Carbon - allow non-generated default methods // Even synthetic default methods need the return type validation... // unavoidable consequence of javac8. :-( validateFactoryReturnType(errors, method.getReturnType(), factoryRawType); defaultMethods.put(method.getName(), method); continue; } otherMethods.put(method.getName(), method); TypeLiteral returnTypeLiteral = factoryType.getReturnType(method); Key returnType; try { returnType = Annotations.getKey(returnTypeLiteral, method, method.getAnnotations(), errors); } catch (ConfigurationException ce) { // If this was an error due to returnTypeLiteral not being specified, rephrase // it as our factory not being specified, so it makes more sense to users. if (isTypeNotSpecified(returnTypeLiteral, ce)) { throw errors.keyNotFullySpecified(TypeLiteral.get(factoryRawType)).toException(); } else { throw ce; } } validateFactoryReturnType(errors, returnType.getTypeLiteral().getRawType(), factoryRawType); List> params = factoryType.getParameterTypes(method); Annotation[][] paramAnnotations = method.getParameterAnnotations(); int p = 0; List> keys = Lists.newArrayList(); for (TypeLiteral param : params) { Key paramKey = Annotations.getKey(param, method, paramAnnotations[p++], errors); Class underlylingType = paramKey.getTypeLiteral().getRawType(); if (underlylingType.equals(Provider.class) || underlylingType.equals(jakarta.inject.Provider.class)) { errors.addMessage( "A Provider may not be a type in a factory method of an AssistedInject." + "\n Offending instance is parameter [%s] with key [%s] on method [%s]", p, paramKey, method); } keys.add(assistKey(method, paramKey, errors)); } ImmutableList> immutableParamList = ImmutableList.copyOf(keys); // try to match up the method to the constructor TypeLiteral implementation = collector.getBindings().get(returnType); if (implementation == null) { implementation = returnType.getTypeLiteral(); } Class scope = Annotations.findScopeAnnotation(errors, implementation.getRawType()); if (scope != null) { errors.addMessage( "Found scope annotation [%s] on implementation class " + "[%s] of AssistedInject factory [%s].\nThis is not allowed, please" + " remove the scope annotation.", scope, implementation.getRawType(), factoryType); } InjectionPoint ctorInjectionPoint; try { ctorInjectionPoint = findMatchingConstructorInjectionPoint( method, returnType, implementation, immutableParamList); } catch (ErrorsException ee) { errors.merge(ee.getErrors()); continue; } Constructor constructor = (Constructor) ctorInjectionPoint.getMember(); List providers = Collections.emptyList(); Set> deps = getDependencies(ctorInjectionPoint, implementation); boolean optimized = false; // Now go through all dependencies of the implementation and see if it is OK to // use an optimized form of assistedinject2. The optimized form requires that // all injections directly inject the object itself (and not a Provider of the object, // or an Injector), because it caches a single child injector and mutates the Provider // of the arguments in a ThreadLocal. if (isValidForOptimizedAssistedInject(deps, implementation.getRawType(), factoryType)) { ImmutableList.Builder providerListBuilder = ImmutableList.builder(); for (int i = 0; i < params.size(); i++) { providerListBuilder.add(new ThreadLocalProvider()); } providers = providerListBuilder.build(); optimized = true; } AssistData data = new AssistData( constructor, returnType, immutableParamList, implementation, method, removeAssistedDeps(deps), optimized, providers); assistDataBuilder.put(method, data); } factory = factoryRawType.cast( Proxy.newProxyInstance( factoryRawType.getClassLoader(), new Class[] {factoryRawType}, this)); // Now go back through default methods. Try to use MethodHandles to make things // work. If that doesn't work, fallback to trying to find compatible method // signatures. Map dataSoFar = assistDataBuilder.build(); ImmutableMap.Builder methodHandleBuilder = ImmutableMap.builder(); boolean warnedAboutUserLookups = false; for (Map.Entry entry : defaultMethods.entries()) { if (!warnedAboutUserLookups && userLookups == null && !Modifier.isPublic(factory.getClass().getModifiers())) { warnedAboutUserLookups = true; logger.log( Level.WARNING, "AssistedInject factory {0} is non-public and has default methods. " // Carbon - adjust message + " Please pass a `MethodHandles.lookup()` with" + " FactoryModuleBuilder.withLookups when using this factory so that Guice can" + " properly call the default methods. Guice will try to workaround this, but " + "it does not always work (depending on the method signatures of the factory).", new Object[] {factoryType}); } // Note: If the user didn't supply a valid lookup, we always try to fallback to the hacky // signature comparing workaround below. // This is because all these shenanigans are only necessary because we're implementing // AssistedInject through a Proxy. If we were to generate a subclass (which we theoretically // _could_ do), then we wouldn't inadvertantly proxy the javac-generated default methods // too (and end up with a stack overflow from infinite recursion). // As such, we try our hardest to "make things work" requiring requiring extra effort from // the user. Method defaultMethod = entry.getValue(); MethodHandle handle = null; try { handle = superMethodHandle( SuperMethodSupport.METHOD_LOOKUP, defaultMethod, factory, userLookups); } catch (ReflectiveOperationException e1) { // If the user-specified lookup failed, try again w/ the private lookup hack. // If _that_ doesn't work, try the below workaround. if (allowPrivateLookupFallback && SuperMethodSupport.METHOD_LOOKUP != SuperMethodLookup.PRIVATE_LOOKUP) { try { handle = superMethodHandle( SuperMethodLookup.PRIVATE_LOOKUP, defaultMethod, factory, userLookups); } catch (ReflectiveOperationException e2) { // ignored, use below workaround. } } } Supplier failureMsg = () -> "Unable to use non-public factory " + factoryRawType.getName() + ". Please call" + " FactoryModuleBuilder.withLookups(MethodHandles.lookup()) (with a" + " lookups that has access to the factory), or make the factory" + " public."; if (handle != null) { methodHandleBuilder.put(defaultMethod, handle); } else if (!allowMethodHandleWorkaround) { errors.addMessage(failureMsg.get()); } else { boolean foundMatch = false; for (Method otherMethod : otherMethods.get(defaultMethod.getName())) { if (dataSoFar.containsKey(otherMethod) && isCompatible(defaultMethod, otherMethod)) { if (foundMatch) { errors.addMessage(failureMsg.get()); break; } else { assistDataBuilder.put(defaultMethod, dataSoFar.get(otherMethod)); foundMatch = true; } } } // We always expect to find at least one match, because we only deal with javac-generated // default methods. If we ever allow user-specified default methods, this will need to // change. if (!foundMatch) { throw new IllegalStateException("Can't find method compatible with: " + defaultMethod); } } } // If we generated any errors (from finding matching constructors, for instance), throw an // exception. if (errors.hasErrors()) { throw errors.toException(); } assistDataByMethod = assistDataBuilder.build(); methodHandleByMethod = methodHandleBuilder.build(); } catch (ErrorsException e) { throw new ConfigurationException(e.getErrors().getMessages()); } } static boolean isDefault(Method method) { // Per the javadoc, default methods are non-abstract, public, non-static. // They're also in interfaces, but we can guarantee that already since we only act // on interfaces. return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC; } private boolean isCompatible(Method src, Method dst) { if (!src.getReturnType().isAssignableFrom(dst.getReturnType())) { return false; } Class[] srcParams = src.getParameterTypes(); Class[] dstParams = dst.getParameterTypes(); if (srcParams.length != dstParams.length) { return false; } for (int i = 0; i < srcParams.length; i++) { if (!srcParams[i].isAssignableFrom(dstParams[i])) { return false; } } return true; } @Override public F get() { return factory; } @Override public Set> getDependencies() { Set> combinedDeps = new HashSet<>(); for (AssistData data : assistDataByMethod.values()) { combinedDeps.addAll(data.dependencies); } return ImmutableSet.copyOf(combinedDeps); } @Override public Key getKey() { return factoryKey; } // Safe cast because values are typed to AssistedData, which is an AssistedMethod, and // the collection is immutable. @Override @SuppressWarnings("unchecked") public Collection getAssistedMethods() { return (Collection) (Collection) assistDataByMethod.values(); } @Override @SuppressWarnings("unchecked") public V acceptExtensionVisitor( BindingTargetVisitor visitor, ProviderInstanceBinding binding) { if (visitor instanceof AssistedInjectTargetVisitor) { return ((AssistedInjectTargetVisitor) visitor).visit((AssistedInjectBinding) this); } return visitor.visit(binding); } private void validateFactoryReturnType(Errors errors, Class returnType, Class factoryType) { if (Modifier.isPublic(factoryType.getModifiers()) && !Modifier.isPublic(returnType.getModifiers())) { errors.addMessage( "%s is public, but has a method that returns a non-public type: %s. " + "Due to limitations with java.lang.reflect.Proxy, this is not allowed. " + "Please either make the factory non-public or the return type public.", factoryType, returnType); } } /** * Returns true if the ConfigurationException is due to an error of TypeLiteral not being fully * specified. */ private boolean isTypeNotSpecified(TypeLiteral typeLiteral, ConfigurationException ce) { Collection messages = ce.getErrorMessages(); if (messages.size() == 1) { Message msg = Iterables.getOnlyElement(new Errors().keyNotFullySpecified(typeLiteral).getMessages()); return msg.getMessage().equals(Iterables.getOnlyElement(messages).getMessage()); } else { return false; } } /** * Finds a constructor suitable for the method. If the implementation contained any constructors * marked with {@link AssistedInject}, this requires all {@link Assisted} parameters to exactly * match the parameters (in any order) listed in the method. Otherwise, if no {@link * AssistedInject} constructors exist, this will default to looking for an {@literal @}{@link * Inject} constructor. */ private InjectionPoint findMatchingConstructorInjectionPoint( Method method, Key returnType, TypeLiteral implementation, List> paramList) throws ErrorsException { Errors errors = new Errors(method); if (returnType.getTypeLiteral().equals(implementation)) { errors = errors.withSource(implementation); } else { errors = errors.withSource(returnType).withSource(implementation); } Class rawType = implementation.getRawType(); if (Modifier.isInterface(rawType.getModifiers())) { errors.addMessage( "%s is an interface, not a concrete class. Unable to create AssistedInject factory.", implementation); throw errors.toException(); } else if (Modifier.isAbstract(rawType.getModifiers())) { errors.addMessage( "%s is abstract, not a concrete class. Unable to create AssistedInject factory.", implementation); throw errors.toException(); } else if (Classes.isInnerClass(rawType)) { errors.cannotInjectInnerClass(rawType); throw errors.toException(); } Constructor matchingConstructor = null; boolean anyAssistedInjectConstructors = false; // Look for AssistedInject constructors... for (Constructor constructor : rawType.getDeclaredConstructors()) { if (constructor.isAnnotationPresent(AssistedInject.class)) { anyAssistedInjectConstructors = true; if (constructorHasMatchingParams(implementation, constructor, paramList, errors)) { if (matchingConstructor != null) { errors.addMessage( "%s has more than one constructor annotated with @AssistedInject" + " that matches the parameters in method %s. Unable to create " + "AssistedInject factory.", implementation, method); throw errors.toException(); } else { matchingConstructor = constructor; } } } } if (!anyAssistedInjectConstructors) { // If none existed, use @Inject or a no-arg constructor. try { return InjectionPoint.forConstructorOf(implementation); } catch (ConfigurationException e) { errors.merge(e.getErrorMessages()); throw errors.toException(); } } else { // Otherwise, use it or fail with a good error message. if (matchingConstructor != null) { // safe because we got the constructor from this implementation. @SuppressWarnings("unchecked") InjectionPoint ip = InjectionPoint.forConstructor( (Constructor) matchingConstructor, implementation); return ip; } else { errors.addMessage( "%s has @AssistedInject constructors, but none of them match the" + " parameters in method %s. Unable to create AssistedInject factory.", implementation, method); throw errors.toException(); } } } /** * Matching logic for constructors annotated with AssistedInject. This returns true if and only if * all @Assisted parameters in the constructor exactly match (in any order) all @Assisted * parameters the method's parameter. */ private boolean constructorHasMatchingParams( TypeLiteral type, Constructor constructor, List> paramList, Errors errors) throws ErrorsException { List> params = type.getParameterTypes(constructor); Annotation[][] paramAnnotations = constructor.getParameterAnnotations(); int p = 0; List> constructorKeys = Lists.newArrayList(); for (TypeLiteral param : params) { Key paramKey = Annotations.getKey(param, constructor, paramAnnotations[p++], errors); constructorKeys.add(paramKey); } // Require that every key exist in the constructor to match up exactly. for (Key key : paramList) { // If it didn't exist in the constructor set, we can't use it. if (!constructorKeys.remove(key)) { return false; } } // If any keys remain and their annotation is Assisted, we can't use it. for (Key key : constructorKeys) { if (key.getAnnotationType() == Assisted.class) { return false; } } // All @Assisted params match up to the method's parameters. return true; } /** Calculates all dependencies required by the implementation and constructor. */ private Set> getDependencies( InjectionPoint ctorPoint, TypeLiteral implementation) { ImmutableSet.Builder> builder = ImmutableSet.builder(); builder.addAll(ctorPoint.getDependencies()); if (!implementation.getRawType().isInterface()) { for (InjectionPoint ip : InjectionPoint.forInstanceMethodsAndFields(implementation)) { builder.addAll(ip.getDependencies()); } } return builder.build(); } /** Return all non-assisted dependencies. */ private Set> removeAssistedDeps(Set> deps) { ImmutableSet.Builder> builder = ImmutableSet.builder(); for (Dependency dep : deps) { Class annotationType = dep.getKey().getAnnotationType(); if (annotationType == null || !annotationType.equals(Assisted.class)) { builder.add(dep); } } return builder.build(); } /** * Returns true if all dependencies are suitable for the optimized version of AssistedInject. The * optimized version caches the binding and uses a ThreadLocal Provider, so can only be applied if * the assisted bindings are immediately provided. This looks for hints that the values may be * lazily retrieved, by looking for injections of Injector or a Provider for the assisted values. */ private boolean isValidForOptimizedAssistedInject( Set> dependencies, Class implementation, TypeLiteral factoryType) { Set> badDeps = null; // optimization: create lazily for (Dependency dep : dependencies) { if (isInjectorOrAssistedProvider(dep)) { if (badDeps == null) { badDeps = Sets.newHashSet(); } badDeps.add(dep); } } if (badDeps != null && !badDeps.isEmpty()) { logger.log( Level.WARNING, "AssistedInject factory {0} will be slow " + "because {1} has assisted Provider dependencies or injects the Injector. " + "Stop injecting @Assisted Provider (instead use @Assisted T) " + "or Injector to speed things up. (It will be a ~6500% speed bump!) " + "The exact offending deps are: {2}", new Object[] {factoryType, implementation, badDeps}); return false; } return true; } /** * Returns true if the dependency is for {@link Injector} or if the dependency is a {@link * Provider} for a parameter that is {@literal @}{@link Assisted}. */ private boolean isInjectorOrAssistedProvider(Dependency dependency) { Class annotationType = dependency.getKey().getAnnotationType(); if (annotationType != null && annotationType.equals(Assisted.class)) { // If it's assisted.. if (dependency .getKey() .getTypeLiteral() .getRawType() .equals(Provider.class)) { // And a Provider... return true; } } else if (dependency .getKey() .getTypeLiteral() .getRawType() .equals(Injector.class)) { // If it's the Injector... return true; } return false; } /** * Returns a key similar to {@code key}, but with an {@literal @}Assisted binding annotation. This * fails if another binding annotation is clobbered in the process. If the key already has the * {@literal @}Assisted annotation, it is returned as-is to preserve any String value. */ private Key assistKey(Method method, Key key, Errors errors) throws ErrorsException { if (key.getAnnotationType() == null) { return key.withAnnotation(DEFAULT_ANNOTATION); } else if (key.getAnnotationType() == Assisted.class) { return key; } else { errors .withSource(method) .addMessage( "Only @Assisted is allowed for factory parameters, but found @%s", key.getAnnotationType()); throw errors.toException(); } } /** * At injector-creation time, we initialize the invocation handler. At this time we make sure all * factory methods will be able to build the target types. */ @Inject @Toolable void initialize(Injector injector) { if (this.injector != null) { throw new ConfigurationException( ImmutableList.of( new Message( FactoryProvider3.class, "Factories.create() factories may only be used in one Injector!"))); } this.injector = injector; for (Map.Entry entry : assistDataByMethod.entrySet()) { Method method = entry.getKey(); AssistData data = entry.getValue(); Object[] args; if (!data.optimized) { args = new Object[method.getParameterTypes().length]; Arrays.fill(args, "dummy object for validating Factories"); } else { args = null; // won't be used -- instead will bind to data.providers. } getBindingFromNewInjector( method, args, data); // throws if the binding isn't properly configured } } /** * Creates a child injector that binds the args, and returns the binding for the method's result. */ public Binding getBindingFromNewInjector( final Method method, final Object[] args, final AssistData data) { checkState( injector != null, "Factories.create() factories cannot be used until they're initialized by Guice."); final Key returnType = data.returnType; // We ignore any pre-existing binding annotation. final Key returnKey = Key.get(returnType.getTypeLiteral(), RETURN_ANNOTATION); Module assistedModule = new AbstractModule() { @Override @SuppressWarnings({ "unchecked", "rawtypes" }) // raw keys are necessary for the args array and return value protected void configure() { Binder binder = binder().withSource(method); int p = 0; if (!data.optimized) { for (Key paramKey : data.paramTypes) { // Wrap in a Provider to cover null, and to prevent Guice from injecting the // parameter binder.bind((Key) paramKey).toProvider(Providers.of(args[p++])); } } else { for (Key paramKey : data.paramTypes) { // Bind to our ThreadLocalProviders. binder.bind((Key) paramKey).toProvider(data.providers.get(p++)); } } Constructor constructor = data.constructor; // Constructor *should* always be non-null here, // but if it isn't, we'll end up throwing a fairly good error // message for the user. if (constructor != null) { binder .bind(returnKey) .toConstructor(constructor, (TypeLiteral) data.implementationType) .in(Scopes.NO_SCOPE); // make sure we erase any scope on the implementation type } } }; Injector forCreate = injector.createChildInjector(assistedModule); Binding binding = forCreate.getBinding(returnKey); // If we have providers cached in data, cache the binding for future optimizations. if (data.optimized) { data.cachedBinding = binding; } return binding; } /** * When a factory method is invoked, we create a child injector that binds all parameters, then * use that to get an instance of the return type. */ @Override public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { // If we setup a method handle earlier for this method, call it. // This is necessary for default methods that java8 creates, so we // can call the default method implementation (and not our proxied version of it). if (methodHandleByMethod.containsKey(method)) { return methodHandleByMethod.get(method).invokeWithArguments(args); } if (method.getDeclaringClass().equals(Object.class)) { if ("equals".equals(method.getName())) { return proxy == args[0]; } else if ("hashCode".equals(method.getName())) { return System.identityHashCode(proxy); } else { return method.invoke(this, args); } } AssistData data = assistDataByMethod.get(method); checkState(data != null, "No data for method: %s", method); Provider provider; if (data.cachedBinding != null) { // Try to get optimized form... provider = data.cachedBinding.getProvider(); } else { provider = getBindingFromNewInjector(method, args, data).getProvider(); } try { int p = 0; for (ThreadLocalProvider tlp : data.providers) { tlp.set(args[p++]); } return provider.get(); } catch (ProvisionException e) { // if this is an exception declared by the factory method, throw it as-is if (e.getErrorMessages().size() == 1) { Message onlyError = getOnlyElement(e.getErrorMessages()); Throwable cause = onlyError.getCause(); if (cause != null && canRethrow(method, cause)) { throw cause; } } throw e; } finally { for (ThreadLocalProvider tlp : data.providers) { tlp.remove(); } } } @Override public String toString() { return factory.getClass().getInterfaces()[0].getName(); } @Override public int hashCode() { return Objects.hashCode(factoryKey, collector); } @Override public boolean equals(Object obj) { if (!(obj instanceof FactoryProvider3)) { return false; } FactoryProvider3 other = (FactoryProvider3) obj; return factoryKey.equals(other.factoryKey) && Objects.equal(collector, other.collector); } /** Returns true if {@code thrown} can be thrown by {@code invoked} without wrapping. */ static boolean canRethrow(Method invoked, Throwable thrown) { if (thrown instanceof Error || thrown instanceof RuntimeException) { return true; } for (Class declared : invoked.getExceptionTypes()) { if (declared.isInstance(thrown)) { return true; } } return false; } // not because we'll never know and this is easier than suppressing warnings. private static class ThreadLocalProvider extends ThreadLocal implements Provider { @Override protected Object initialValue() { throw new IllegalStateException( "Cannot use optimized @Assisted provider outside the scope of the constructor." + " (This should never happen. If it does, please report it.)"); } } /** * Holder for the appropriate kind of method lookup to use. Due to bugs in Java releases, we have * to evaluate what approach to take at runtime. We do this by emulating the buggy scenarios: can * a lookup access private details that it should be able to see? If not, we fail down to using * full private access. Unfortunately, private access doesn't work in the JDK17+.... but it * shouldn't be necessary there either, because the buggy lookup checks should be fixed. */ private static class SuperMethodSupport { private static final SuperMethodLookup METHOD_LOOKUP; static { SuperMethodLookup workingLookup = null; try { Class hidden = Class.forName("com.google.inject.assistedinject.internal.LookupTester$Hidden"); Method method = hidden.getMethod("method"); Field lookupsField = hidden.getEnclosingClass().getDeclaredField("LOOKUP"); lookupsField.setAccessible(true); MethodHandles.Lookup lookups = (MethodHandles.Lookup) lookupsField.get(null); for (SuperMethodLookup attempt : SuperMethodLookup.values()) { try { attempt.superMethodHandle(method, lookups); workingLookup = attempt; break; } catch (ReflectiveOperationException ignored) { // Keep looping to find a working lookup } } } catch (ReflectiveOperationException ignored) { // Bail if our internal tests don't exist. } // If everything failed, use the worst option. if (workingLookup == null) { workingLookup = SuperMethodLookup.PRIVATE_LOOKUP; } METHOD_LOOKUP = workingLookup; } } private static MethodHandle superMethodHandle( SuperMethodLookup strategy, Method method, Object proxy, MethodHandles.Lookup userLookups) throws ReflectiveOperationException { MethodHandles.Lookup lookup = userLookups == null ? MethodHandles.lookup() : userLookups; MethodHandle handle = strategy.superMethodHandle(method, lookup); return handle != null ? handle.bindTo(proxy) : null; } private static enum SuperMethodLookup { UNREFLECT_SPECIAL { @Override MethodHandle superMethodHandle(Method method, MethodHandles.Lookup lookup) throws ReflectiveOperationException { return lookup.unreflectSpecial(method, method.getDeclaringClass()); } }, FIND_SPECIAL { @Override MethodHandle superMethodHandle(Method method, MethodHandles.Lookup lookup) throws ReflectiveOperationException { Class declaringClass = method.getDeclaringClass(); // Before JDK14, unreflectSpecial didn't work in some scenarios. // So we workaround using findSpecial. See: https://bugs.openjdk.java.net/browse/JDK-8209005 return lookup.findSpecial( declaringClass, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()), declaringClass); } }, PRIVATE_LOOKUP { @Override MethodHandle superMethodHandle(Method method, MethodHandles.Lookup unused) throws ReflectiveOperationException { // Even findSpecial fails on JDK8, so we need to manually reflect on private details. // But note that this will fail 100% of the time on JDK17+, which doesn't allow reflection // into the JDK internals. return PrivateLookup.superMethodHandle(method); } }; abstract MethodHandle superMethodHandle(Method method, MethodHandles.Lookup lookup) throws ReflectiveOperationException; }; // Note: this isn't a public API, but we need to use it in order to call default methods on (or // with) non-public types. If it doesn't exist, the code falls back to a less precise check. static class PrivateLookup { PrivateLookup() {} private static final int ALL_MODES = Modifier.PRIVATE | Modifier.STATIC /* package */ | Modifier.PUBLIC | Modifier.PROTECTED; private static final Constructor privateLookupCxtor = findPrivateLookupCxtor(); private static Constructor findPrivateLookupCxtor() { try { Constructor cxtor; try { cxtor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); } catch (NoSuchMethodException ignored) { cxtor = MethodHandles.Lookup.class.getDeclaredConstructor( Class.class, Class.class, int.class); } cxtor.setAccessible(true); return cxtor; } catch (Exception e) { // Note: we catch Exception because we want to handle InaccessibleObjectException too, // but we target JDK8. // TODO(sameb): When we drop JDK8 support, catch ReflectiveOperation|Security|Inaccessible return null; } } static MethodHandle superMethodHandle(Method method) throws ReflectiveOperationException { if (privateLookupCxtor == null) { return null; // fall back to assistDataBuilder workaround } Class declaringClass = method.getDeclaringClass(); MethodHandles.Lookup lookup; if (privateLookupCxtor.getParameterCount() == 2) { lookup = privateLookupCxtor.newInstance(declaringClass, ALL_MODES); } else { lookup = privateLookupCxtor.newInstance(declaringClass, null, ALL_MODES); } return lookup.unreflectSpecial(method, declaringClass); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/CarbonChatInternal.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Provider; import com.google.inject.TypeLiteral; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.command.CarbonCommand; import net.draycia.carbon.common.command.ExecutionCoordinatorHolder; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import net.draycia.carbon.common.listeners.Listener; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.messaging.packets.PacketFactory; import net.draycia.carbon.common.users.PlayerUtils; import net.draycia.carbon.common.users.ProfileCache; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.common.users.UserManagerInternal; import net.draycia.carbon.common.util.CloudUtils; import net.draycia.carbon.common.util.ConcurrentUtil; import net.draycia.carbon.common.util.UpdateChecker; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public abstract class CarbonChatInternal implements CarbonChat { private final Injector injector; private final Logger logger; private final ScheduledExecutorService periodicTasks; private final ProfileCache profileCache; private final ProfileResolver profileResolver; private final UserManagerInternal userManager; private final ExecutionCoordinatorHolder commandExecutor; private final CarbonServer carbonServer; private final CarbonMessages carbonMessages; private final CarbonEventHandler eventHandler; private final CarbonChannelRegistry channelRegistry; private final Provider messagingManager; protected CarbonChatInternal( final Injector injector, final Logger logger, final ScheduledExecutorService periodicTasks, final ProfileCache profileCache, final ProfileResolver profileResolver, final UserManagerInternal userManager, final ExecutionCoordinatorHolder commandExecutor, final CarbonServer carbonServer, final CarbonMessages carbonMessages, final CarbonEventHandler eventHandler, final CarbonChannelRegistry channelRegistry, final Provider messagingManagerProvider ) { this.injector = injector; this.logger = logger; this.periodicTasks = periodicTasks; this.profileCache = profileCache; this.profileResolver = profileResolver; this.userManager = userManager; this.commandExecutor = commandExecutor; this.carbonServer = carbonServer; this.carbonMessages = carbonMessages; this.eventHandler = eventHandler; this.channelRegistry = channelRegistry; this.messagingManager = messagingManagerProvider; } protected void init() { // Listeners final Set listeners = this.injector.getInstance(Key.get(new TypeLiteral>() {})); // Commands // This is a bit awkward looking final Set commands = this.injector.getInstance(Key.get(new TypeLiteral>() {})); CloudUtils.registerCommands(commands, this.injector.getInstance(ConfigManager.class).loadCommandSettings()); this.periodicTasks.scheduleAtFixedRate( () -> PlayerUtils.saveLoggedInPlayers(this.carbonServer, this.userManager, this.logger), 5, 5, TimeUnit.MINUTES ); this.periodicTasks.scheduleAtFixedRate( this.profileCache::save, 15, 15, TimeUnit.MINUTES ); this.periodicTasks.scheduleAtFixedRate( this.userManager::cleanup, 30, 30, TimeUnit.SECONDS ); this.initIntegrations(); // Load channels this.channelRegistry().loadConfigChannels(this.carbonMessages); this.messagingManager.get(); } protected void initIntegrations() { // Integration final Set integrations = this.injector().getInstance(Key.get(new TypeLiteral<>() {})); for (final Integration integration : integrations) { if (!integration.eligible()) { continue; } integration.register(); } } protected final void checkVersion() { if (!this.injector.getInstance(ConfigManager.class).primaryConfig().updateChecker()) { return; } CompletableFuture.runAsync(() -> new UpdateChecker(this.logger).checkVersion()).whenComplete(($, thr) -> { if (thr != null) { this.logger.warn("Exception fetching version information", thr); } }); } protected void shutdown() { this.messagingManager.get().queuePacket(() -> this.injector.getInstance(PacketFactory.class).clearLocalPlayersPacket()); this.messagingManager.get().onShutdown(); ConcurrentUtil.shutdownExecutor(this.periodicTasks, TimeUnit.MILLISECONDS, 500); this.profileCache.save(); this.profileResolver.shutdown(); this.userManager.shutdown(); this.commandExecutor.shutdown(); } public Logger logger() { return this.logger; } @Override public CarbonServer server() { return this.carbonServer; } @Override public UserManager userManager() { return this.userManager; } @Override public CarbonEventHandler eventHandler() { return this.eventHandler; } @Override public CarbonChannelRegistry channelRegistry() { return this.channelRegistry; } public boolean isProxy() { return false; } public Injector injector() { return this.injector; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/CarbonCommonModule.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.AbstractModule; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.Provides; import com.google.inject.Scopes; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import com.google.inject.assistedinject.FactoryModuleBuilder; import com.google.inject.assistedinject.FactoryProvider3; import com.google.inject.multibindings.Multibinder; import io.leangen.geantyref.TypeToken; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.nio.file.Path; import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.command.CarbonCommand; import net.draycia.carbon.common.command.ExecutionCoordinatorHolder; import net.draycia.carbon.common.command.ParserFactory; import net.draycia.carbon.common.command.argument.PlayerSuggestions; import net.draycia.carbon.common.command.commands.ClearChatCommand; import net.draycia.carbon.common.command.commands.ContinueCommand; import net.draycia.carbon.common.command.commands.DebugCommand; import net.draycia.carbon.common.command.commands.FilterCommand; import net.draycia.carbon.common.command.commands.HelpCommand; import net.draycia.carbon.common.command.commands.IgnoreCommand; import net.draycia.carbon.common.command.commands.IgnoreListCommand; import net.draycia.carbon.common.command.commands.JoinCommand; import net.draycia.carbon.common.command.commands.LeaveCommand; 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.RealNameCommand; import net.draycia.carbon.common.command.commands.ReloadCommand; import net.draycia.carbon.common.command.commands.ReplyCommand; import net.draycia.carbon.common.command.commands.SpyCommand; import net.draycia.carbon.common.command.commands.ToggleMessagesCommand; import net.draycia.carbon.common.command.commands.UnignoreCommand; import net.draycia.carbon.common.command.commands.UnmuteCommand; import net.draycia.carbon.common.command.commands.UpdateUsernameCommand; import net.draycia.carbon.common.command.commands.WhisperCommand; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.config.DatabaseSettings; import net.draycia.carbon.common.config.PrimaryConfig; import net.draycia.carbon.common.event.CarbonEventHandlerImpl; import net.draycia.carbon.common.listeners.DeafenHandler; import net.draycia.carbon.common.listeners.FilterHandler; import net.draycia.carbon.common.listeners.HyperlinkHandler; import net.draycia.carbon.common.listeners.IgnoreHandler; import net.draycia.carbon.common.listeners.ItemLinkHandler; import net.draycia.carbon.common.listeners.Listener; import net.draycia.carbon.common.listeners.MessagePacketHandler; import net.draycia.carbon.common.listeners.MuteHandler; import net.draycia.carbon.common.listeners.PartyChatSpyHandler; import net.draycia.carbon.common.listeners.PartyPingHandler; import net.draycia.carbon.common.listeners.PingHandler; import net.draycia.carbon.common.listeners.RadiusListener; import net.draycia.carbon.common.messages.CarbonMessageRenderer; 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.RenderForTagResolver; 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.LongPlaceholderResolver; 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; import net.draycia.carbon.common.messaging.packets.PacketFactory; import net.draycia.carbon.common.users.Backing; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.users.NetworkUsers; import net.draycia.carbon.common.users.PlatformUserManager; import net.draycia.carbon.common.users.UserManagerInternal; import net.draycia.carbon.common.users.db.DatabaseUserManager; import net.draycia.carbon.common.users.db.argument.BinaryUUIDArgumentFactory; import net.draycia.carbon.common.users.db.mapper.BinaryUUIDColumnMapper; import net.draycia.carbon.common.users.db.mapper.NativeUUIDColumnMapper; import net.draycia.carbon.common.users.json.JSONUserManager; import net.draycia.carbon.common.util.CloudUtils; import net.draycia.carbon.common.util.ConcurrentUtil; import net.draycia.carbon.common.util.Exceptions; import net.draycia.carbon.common.util.FileUtil; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.moonshine.Moonshine; import net.kyori.moonshine.exception.scan.UnscannableMethodException; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.jdbi.v3.core.h2.H2DatabasePlugin; import org.jdbi.v3.postgres.PostgresPlugin; import org.spongepowered.configurate.util.NamingSchemes; @DefaultQualifier(NonNull.class) public final class CarbonCommonModule extends AbstractModule { @Provides @Backing @Singleton public UserManagerInternal userManager( final @DataDirectory Path dataDirectory, final ConfigManager configManager, final Logger logger, final Injector injector ) throws IOException { PrimaryConfig.StorageType storageType = configManager.primaryConfig().storageType(); final String smokeTestMode = System.getProperty("carbonchat.smokeTestMode"); if (smokeTestMode != null) { logger.info("Smoke test: Overriding storage manager."); switch (smokeTestMode) { case "h2" -> storageType = PrimaryConfig.StorageType.H2; case "json" -> storageType = PrimaryConfig.StorageType.JSON; case "postgres" -> { storageType = PrimaryConfig.StorageType.PSQL; configManager.primaryConfig().databaseSettings().url("jdbc:postgresql://localhost:5432/carbon"); } case "mariadb" -> { storageType = PrimaryConfig.StorageType.MYSQL; configManager.primaryConfig().databaseSettings().url("jdbc:mariadb://localhost:3306/carbon"); } default -> throw new IllegalArgumentException("Unknown smoke test mode: " + smokeTestMode); } } logger.info("Initializing {} storage manager...", storageType); return switch (storageType) { case MYSQL -> injector.getInstance(DatabaseUserManager.Factory.class).create( "queries/migrations/mysql", jdbi -> jdbi.registerArgument(new BinaryUUIDArgumentFactory()) .registerColumnMapper(UUID.class, new BinaryUUIDColumnMapper()) ); case PSQL -> injector.getInstance(DatabaseUserManager.Factory.class).create( "queries/migrations/postgresql", jdbi -> jdbi.registerColumnMapper(UUID.class, new NativeUUIDColumnMapper()) .installPlugin(new PostgresPlugin()) ); case H2 -> injector.getInstance(DatabaseUserManager.Factory.class).create( "queries/migrations/h2", jdbi -> jdbi.installPlugin(new H2DatabasePlugin()), new DatabaseSettings("jdbc:h2:" + FileUtil.mkParentDirs(dataDirectory.resolve("users/userdata-h2")).toAbsolutePath() + ";MODE=MySQL", "", "") ); case JSON -> injector.getInstance(JSONUserManager.class); }; } @Provides @PeriodicTasks @Singleton public ScheduledExecutorService periodicTasksExecutor(final Logger logger) { return ConcurrentUtil.createPeriodicTasksPool(logger); } @Provides @Singleton public CarbonMessages carbonMessages( final SourcedReceiverResolver receiverResolver, final ComponentPlaceholderResolver componentPlaceholderResolver, final UUIDPlaceholderResolver uuidPlaceholderResolver, final StringPlaceholderResolver stringPlaceholderResolver, final IntPlaceholderResolver intPlaceholderResolver, final LongPlaceholderResolver longPlaceholderResolver, final KeyPlaceholderResolver keyPlaceholderResolver, final BooleanPlaceholderResolver booleanPlaceholderResolver, final CarbonMessageSource carbonMessageSource, final CarbonMessageSender carbonMessageSender, final CarbonMessageRenderer messageRenderer ) throws UnscannableMethodException { return Moonshine.builder(new TypeToken<>() {}) .receiverLocatorResolver(receiverResolver, 0) .sourced(carbonMessageSource) .rendered(messageRenderer) .sent(carbonMessageSender) .resolvingWithStrategy(new StandardPlaceholderResolverStrategyButDifferent<>(NamingSchemes.SNAKE_CASE)) .weightedPlaceholderResolver(Component.class, componentPlaceholderResolver, 0) .weightedPlaceholderResolver(UUID.class, uuidPlaceholderResolver, 0) .weightedPlaceholderResolver(String.class, stringPlaceholderResolver, 0) .weightedPlaceholderResolver(Integer.class, intPlaceholderResolver, 0) .weightedPlaceholderResolver(Long.class, longPlaceholderResolver, 0) .weightedPlaceholderResolver(Key.class, keyPlaceholderResolver, 0) .weightedPlaceholderResolver(Boolean.class, booleanPlaceholderResolver, 0) .weightedPlaceholderResolver(Option.class, new OptionPlaceholderResolver<>(), 0) .create(this.getClass().getClassLoader()); } @Provides @Singleton public ExecutionCoordinatorHolder executionCoordinatorHolder(final Logger logger) { return ExecutionCoordinatorHolder.create(logger); } @Override protected void configure() { this.install(new FactoryModuleBuilder().build(ParserFactory.class)); this.install(new FactoryModuleBuilder().build(RenderForTagResolver.Factory.class)); this.install(factoryModule(PacketFactory.class)); this.bind(ServerId.KEY).toInstance(UUID.randomUUID()); this.bind(ChannelRegistry.class).to(CarbonChannelRegistry.class); this.bind(CarbonEventHandler.class).to(CarbonEventHandlerImpl.class); this.bind(PlayerSuggestions.class).to(NetworkUsers.class); this.bind(new TypeLiteral>() {}).to(PlatformUserManager.class); this.bind(new TypeLiteral>() {}).to(PlatformUserManager.class); this.configureListeners(); this.configureCommands(); } private void configureListeners() { final Multibinder listeners = Multibinder.newSetBinder(this.binder(), Listener.class); listeners.addBinding().to(DeafenHandler.class); listeners.addBinding().to(FilterHandler.class); listeners.addBinding().to(HyperlinkHandler.class); listeners.addBinding().to(IgnoreHandler.class); listeners.addBinding().to(ItemLinkHandler.class); listeners.addBinding().to(MessagePacketHandler.class); listeners.addBinding().to(MuteHandler.class); listeners.addBinding().to(PartyChatSpyHandler.class); listeners.addBinding().to(PartyPingHandler.class); listeners.addBinding().to(PingHandler.class); listeners.addBinding().to(RadiusListener.class); } private void configureCommands() { this.requestStaticInjection(CloudUtils.class); final Multibinder commands = Multibinder.newSetBinder(this.binder(), CarbonCommand.class); commands.addBinding().to(ClearChatCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(ContinueCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(DebugCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(FilterCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(HelpCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(IgnoreCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(MuteCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(MuteInfoCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(NicknameCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(ReloadCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(RealNameCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(ReplyCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(SpyCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(ToggleMessagesCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(UnignoreCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(UnmuteCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(UpdateUsernameCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(WhisperCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(JoinCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(LeaveCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(IgnoreListCommand.class).in(Scopes.SINGLETON); commands.addBinding().to(PartyCommands.class).in(Scopes.SINGLETON); } // Helper to create a FactoryProvider3 module private static Module factoryModule(final Class factoryInterface) { return new AbstractModule() { @Override protected void configure() { try { final Provider factoryProvider = new FactoryProvider3<>( com.google.inject.Key.get(TypeLiteral.get(factoryInterface)), null, MethodHandles.privateLookupIn(factoryInterface, MethodHandles.lookup()) ); this.binder().bind(factoryInterface).toProvider(factoryProvider); } catch (final Exception e) { throw Exceptions.rethrow(e); } } }; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/CarbonPlatformModule.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.AbstractModule; import com.google.inject.multibindings.Multibinder; import net.draycia.carbon.common.integration.Integration; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public abstract class CarbonPlatformModule extends AbstractModule { @Override protected final void configure() { this.configurePlatform(); this.configureIntegrations( Multibinder.newSetBinder(this.binder(), Integration.class), Multibinder.newSetBinder(this.binder(), Integration.ConfigMeta.class) ); } protected abstract void configurePlatform(); protected void configureIntegrations( final Multibinder integrations, final Multibinder configs ) { integrations.addBinding().to(MiniPlaceholdersIntegration.class); configs.addBinding().toInstance(MiniPlaceholdersIntegration.configMeta()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/DataDirectory.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.BindingAnnotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.nio.file.Path; /** * Injection binding annotation for the {@link Path} which is Carbon's data directory. */ @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.FIELD}) public @interface DataDirectory { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/PeriodicTasks.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.BindingAnnotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.ScheduledExecutorService; /** * Injection binding annotation for the {@link ScheduledExecutorService} used for * periodic tasks. */ @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) public @interface PeriodicTasks { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/PlatformScheduler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.Inject; import com.google.inject.Singleton; import net.draycia.carbon.api.users.CarbonPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public interface PlatformScheduler { void scheduleForPlayer(CarbonPlayer carbonPlayer, Runnable runnable); @Singleton final class RunImmediately implements PlatformScheduler { @Inject private RunImmediately() { } @Override public void scheduleForPlayer(final CarbonPlayer carbonPlayer, final Runnable runnable) { runnable.run(); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/RawChat.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.BindingAnnotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Injection binding annotation for the raw chat type key. */ @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) public @interface RawChat { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/channels/CarbonChannelRegistry.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; 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; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChannelRegisterEvent; import net.draycia.carbon.api.event.events.ChannelSwitchEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.DataDirectory; import net.draycia.carbon.common.RawChat; 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.event.events.CarbonChatEventImpl; import net.draycia.carbon.common.event.events.CarbonEarlyChatEvent; import net.draycia.carbon.common.event.events.CarbonReloadEvent; import net.draycia.carbon.common.event.events.ChannelRegisterEventImpl; import net.draycia.carbon.common.event.events.ChannelSwitchEventImpl; import net.draycia.carbon.common.listeners.ChatListenerInternal; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.draycia.carbon.common.util.Exceptions; import net.draycia.carbon.common.util.FileUtil; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.chat.ChatType; import net.kyori.adventure.chat.SignedMessage; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; 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; import org.incendo.cloud.Command; import org.incendo.cloud.CommandManager; import org.incendo.cloud.minecraft.signed.SignedString; import org.incendo.cloud.permission.Permission; import org.incendo.cloud.permission.PredicatePermission; import org.spongepowered.configurate.ConfigurateException; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.loader.ConfigurationLoader; import org.spongepowered.configurate.transformation.ConfigurationTransformation; import static org.incendo.cloud.minecraft.signed.SignedGreedyStringParser.signedGreedyStringParser; @Singleton @DefaultQualifier(NonNull.class) public class CarbonChannelRegistry extends ChatListenerInternal implements ChannelRegistry { private final Path configChannelDir; private final Injector injector; private final Logger logger; private final ConfigManager config; private @MonotonicNonNull Key defaultKey; private final CarbonMessages carbonMessages; private final CarbonEventHandler eventHandler; private final Key rawChatKey; private final Map> handlers = new HashMap<>(); private record SpecialHandler(Class cls, Supplier defaultSupplier) {} public void registerSpecialConfigChannel(final String fileName, final Class type) { if (this.handlers.containsKey(fileName)) { throw new IllegalStateException("Attempting to register duplicate entry (existing: " + this.handlers.get(fileName) + ", new: " + type + ") for key " + fileName); } this.handlers.put(fileName, new SpecialHandler<>(type, () -> this.injector.getInstance(type))); } private volatile Registry channelRegistry = Registry.create(); private final Set configChannels = ConcurrentHashMap.newKeySet(); // // private final BiMap channelMap = Maps.synchronizedBiMap(HashBiMap.create()); @Inject public CarbonChannelRegistry( @DataDirectory final Path dataDirectory, final Injector injector, final Logger logger, final ConfigManager config, final CarbonMessages carbonMessages, final CarbonEventHandler events, @RawChat final Key rawChatKey ) { super(events, carbonMessages, config); this.configChannelDir = dataDirectory.resolve("channels"); this.injector = injector; this.logger = logger; this.config = config; this.carbonMessages = carbonMessages; this.eventHandler = events; this.rawChatKey = rawChatKey; if (config.primaryConfig().partyChat().enabled) { this.registerSpecialConfigChannel(PartyChatChannel.FILE_NAME, PartyChatChannel.class); } events.subscribe(CarbonReloadEvent.class, -99, true, event -> this.reloadConfigChannels()); } public static ConfigurationTransformation.Versioned configChatChannelUpgrader() { // final ConfigurationTransformation initial; return ConfigurationTransformation.versionedBuilder() .versionKey(ConfigManager.CONFIG_VERSION_KEY) // .addVersion(0, initial) .build(); } public static N upgradeConfigChatChannelNode(final N node) throws ConfigurateException { if (true) { // No transformations yet! return node; } if (!node.virtual()) { // we only want to migrate existing data final ConfigurationTransformation.Versioned upgrader = configChatChannelUpgrader(); final int from = upgrader.version(node); upgrader.apply(node); final int to = upgrader.version(node); ConfigManager.configVersionComment(node, upgrader); if (from != to) { // we might not have made any changes // TODO: use logger //CarbonChatProvider.carbonChat().logger().info("Updated config schema from " + from + " to " + to); } } return node; } public void reloadConfigChannels() { final Registry newRegistry = Registry.create(); // Copy API registrations over for (final Key registered : this.channelRegistry.keys()) { if (!this.configChannels.contains(registered)) { newRegistry.register(registered, this.channelRegistry.getHolder(registered).valueOrThrow()); } } final Set oldConfigChannels = Set.copyOf(this.configChannels); this.configChannels.clear(); final Registry oldRegistry = this.channelRegistry; this.channelRegistry = newRegistry; this.loadConfigChannels_(this.carbonMessages); // Re-add any deleted channels; users must restart for them to be removed // (don't want to leave behind commands that just error, or confuse addons) for (final Key old : oldConfigChannels) { if (!this.configChannels.contains(old)) { this.configChannels.add(old); this.channelRegistry.register(old, oldRegistry.getHolder(old).valueOrThrow()); this.logger.warn("The config file for channel [{}] was deleted, but removing " + "channels at runtime is not currently supported. You must restart the plugin " + "for the removal to take effect.", old); } } // Determine new channels and fire event if needed final Set newConfigChannels = new HashSet<>(); for (final Key configChannel : this.configChannels) { if (!oldConfigChannels.contains(configChannel)) { newConfigChannels.add(configChannel); } } if (!newConfigChannels.isEmpty()) { this.eventHandler.emit(new ChannelRegisterEventImpl(this, Set.copyOf(newConfigChannels))); } } public void loadConfigChannels(final CarbonMessages messages) { this.loadConfigChannels_(messages); this.eventHandler.emit(new ChannelRegisterEventImpl(this, Set.copyOf(this.configChannels))); } private void loadConfigChannels_(final CarbonMessages messages) { this.logger.info("Loading config channels..."); this.defaultKey = this.config.primaryConfig().defaultChannel(); this.saveSpecialDefaults(); List channelConfigs = FileUtil.listDirectoryEntries(this.configChannelDir, "*.conf"); final Set channelConfigFileNames = channelConfigs .stream() .map(Path::getFileName) .map(Path::toString) .collect(Collectors.toSet()); final Set expectedHandlerFileNames = this.handlers.keySet(); if (channelConfigs.size() == this.handlers.size() && channelConfigFileNames.containsAll(expectedHandlerFileNames)) { this.saveDefaultChannelConfig(); channelConfigs = FileUtil.listDirectoryEntries(this.configChannelDir, "*.conf"); } for (final Path channelConfigFile : channelConfigs) { final String fileName = channelConfigFile.getFileName().toString(); if (!fileName.endsWith(".conf")) { continue; } final @Nullable ChatChannel chatChannel = this.loadChannel(channelConfigFile); if (chatChannel == null) { continue; } final Key channelKey = chatChannel.key(); if (this.defaultKey.equals(channelKey)) { this.logger.info("Default channel is [{}]", channelKey); } if (this.channelRegistry.keys().contains(channelKey)) { this.logger.warn("Channel with key [{}] has already been registered, skipping {}", channelKey, channelConfigFile); continue; } this.injector.injectMembers(chatChannel); this.configChannels.add(chatChannel.key()); this.register(chatChannel, false); } if (this.channel(this.defaultKey) == null) { this.logger.warn("No default channel found! Default channel key: [{}]", this.defaultKey()); } final List channelList = new ArrayList<>(); for (final Key key : this.keys()) { channelList.add(key.asString()); } final String channels = String.join(", ", channelList); this.logger.info("Registered channels: [{}]", channels); } private void saveSpecialDefaults() { for (final Map.Entry> e : this.handlers.entrySet()) { final Path configFile = this.configChannelDir.resolve(e.getKey()); if (Files.isRegularFile(configFile)) { continue; } try { final ConfigChatChannel configChannel = e.getValue().defaultSupplier().get(); final ConfigurationLoader loader = this.config.configurationLoader(FileUtil.mkParentDirs(configFile), ConfigManager.extractHeader(e.getValue().cls())); final ConfigurationNode node = loader.createNode(); node.set(e.getValue().cls(), configChannel); loader.save(node); } catch (final IOException exception) { throw Exceptions.rethrow(exception); } } } private void saveDefaultChannelConfig() { try { final Path configFile = this.configChannelDir.resolve("global.conf"); final ConfigChatChannel configChannel = this.injector.getInstance(ConfigChatChannel.class); final ConfigurationLoader loader = this.config.configurationLoader(FileUtil.mkParentDirs(configFile), ConfigManager.extractHeader(ConfigChatChannel.class)); final ConfigurationNode node = loader.createNode(); node.set(ConfigChatChannel.class, configChannel); loader.save(node); } catch (final IOException exception) { throw Exceptions.rethrow(exception); } } private @Nullable ChatChannel loadChannel(final Path channelFile) { try { final @Nullable SpecialHandler special = this.handlers.get(channelFile.getFileName().toString()); final Class type = special == null ? ConfigChatChannel.class : special.cls(); final ConfigurationLoader loader = this.config.configurationLoader(channelFile, ConfigManager.extractHeader(type)); final ConfigurationNode loaded = upgradeConfigChatChannelNode(loader.load()); final @Nullable ConfigChatChannel channel = loaded.get(type); if (channel == null) { throw new ConfigurateException("Config deserialized to null."); } loaded.set(type, channel); loader.save(loaded); return channel; } catch (final ConfigurateException exception) { this.logger.warn("Failed to load channel from file '{}'", channelFile, exception); } return null; } private void sendMessageInChannelAsConsole( final Audience sender, final ChatChannel channel, final String plainMessage ) { this.sendMessageInChannel(new ConsoleCarbonPlayer(sender), channel, SignedString.unsigned(plainMessage)); } private void sendMessageInChannel( final CarbonPlayer sender, final ChatChannel channel, final SignedString message ) { final @Nullable SignedMessage signedMessage = message.signedMessage(); final Component originalMessage; if (signedMessage == null) { originalMessage = Component.text(message.string()); } else { originalMessage = Objects.requireNonNullElse( signedMessage.unsignedContent(), Component.text(signedMessage.message()) ); } final @Nullable CarbonEarlyChatEvent earlyChatEvent = this.prepareAndEmitPreChatEvent(sender, originalMessage); if (earlyChatEvent == null || earlyChatEvent.cancelled()) { return; } final Component parsedMessage = this.parseTags(sender, earlyChatEvent.message()); if (parsedMessage == null) { return; } final @Nullable CarbonChatEventImpl chatEvent = this.prepareAndEmitChatEvent(sender, parsedMessage, signedMessage, channel); if (chatEvent == null || chatEvent.cancelled()) { return; } for (final Audience recipient : chatEvent.recipients()) { message.sendMessage(recipient, ChatType.chatType(this.rawChatKey), chatEvent.renderFor(recipient)); } } private void registerChannelCommands(final ChatChannel channel) { final CommandManager commandManager = this.injector.getInstance(com.google.inject.Key.get(new TypeLiteral>() {})); if (!commandManager.isCommandRegistrationAllowed() || commandManager.commandTree().getNamedNode(channel.commandName()) != null) { return; } Command.Builder builder = commandManager.commandBuilder(channel.commandName(), channel.commandAliases(), commandManager.createDefaultCommandMeta()) .optional("message", signedGreedyStringParser()); if (!channel.permissions().dynamic()) { builder = builder.permission(PredicatePermission.of(sender -> { if (!(sender instanceof PlayerCommander player)) { return true; } return channel.permissions().joinPermitted(player.carbonPlayer()).permitted(); })); } final Key channelKey = channel.key(); final Command command = builder.senderType(Commander.class) .handler(handler -> { final Commander commander = handler.sender(); @Nullable ChatChannel chatChannel = this.channel(channelKey); if (!(commander instanceof PlayerCommander playerCommander)) { if (chatChannel != null && handler.contains("message")) { final SignedString message = handler.get("message"); // TODO: trigger platform events related to chat this.sendMessageInChannelAsConsole(commander, chatChannel, message.string()); } return; } final var player = playerCommander.carbonPlayer(); if (player.muted()) { this.carbonMessages.muteCannotSpeak(player); return; } if (player.leftChannels().contains(channelKey) && chatChannel != null) { player.joinChannel(chatChannel); this.carbonMessages.channelJoined(player); } if (handler.contains("message")) { final SignedString message = handler.get("message"); // TODO: trigger platform events related to chat this.sendMessageInChannel(player, chatChannel, message); } else { final @Nullable ChatChannel fromChannel = player.selectedChannel(); if (this.config.primaryConfig().returnToDefaultChannel() && fromChannel != null && fromChannel.key().equals(channelKey)) { chatChannel = this.defaultChannel(); } final ChannelSwitchEvent switchEvent = new ChannelSwitchEventImpl(player, chatChannel); this.eventHandler.emit(switchEvent); player.selectedChannel(switchEvent.channel()); this.carbonMessages.changedChannels(player, chatChannel.key().value()); } }) .build(); commandManager.command(command); Command.Builder proxyBuilder = commandManager.commandBuilder("channel", "ch"); if (!channel.permissions().dynamic() && channel.permissions() instanceof ChannelPermissionsImpl permissions) { proxyBuilder = proxyBuilder.permission(Permission.allOf(Permission.of("carbon.channel"), Permission.of(permissions.permission()))); } commandManager.command(proxyBuilder.literal(channelKey.value()).proxies(command).build()); } @Override public void register(final ChatChannel channel) { this.register(channel, true); } public void register(final ChatChannel channel, final boolean fireRegisterEvent) { this.channelRegistry.register(channel.key(), channel); if (channel.shouldRegisterCommands()) { this.registerChannelCommands(channel); } if (fireRegisterEvent) { this.eventHandler.emit(new ChannelRegisterEventImpl(this, Set.of(channel.key()))); } } @Override public @Nullable ChatChannel channel(final Key key) { final @Nullable Holder holder = this.channelRegistry.getHolder(key); return holder == null ? null : holder.value(); } public @Nullable ChatChannel channelByValue(final String value) { if (value.contains(":")) { return this.channel(Key.key(value)); } for (final Key key : this.keys()) { if (key.value().equalsIgnoreCase(value)) { return this.channel(key); } } return null; } @Override public @NonNull Set keys() { return Collections.unmodifiableSet(this.channelRegistry.keys()); } @Override public ChatChannel defaultChannel() { return Objects.requireNonNull(this.channel(this.defaultKey)); } @Override public Key defaultKey() { return this.defaultKey; } @Override public ChatChannel channelOrDefault(final Key key) { final @Nullable ChatChannel channel = this.channel(key); if (channel != null) { return channel; } return this.defaultChannel(); } @Override public ChatChannel channelOrThrow(final Key key) { final @Nullable ChatChannel channel = this.channel(key); if (channel != null) { return channel; } throw new NoSuchElementException("No channel registered with key '" + key.asString() + "'"); } @Override public void allKeys(final Consumer action) { for (final Key key : this.channelRegistry.keys()) { action.accept(key); } this.eventHandler.subscribe( CarbonChannelRegisterEvent.class, event -> { for (final Key key : event.registered()) { action.accept(key); } } ); } @Override public ChannelPermissions permission(final String permission) { return new ChannelPermissionsImpl(permission, this.carbonMessages); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/channels/ChannelPermissionsImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.draycia.carbon.api.channels.ChannelPermissionResult; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.messages.CarbonMessages; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; public record ChannelPermissionsImpl(String permission, CarbonMessages messages) implements ChannelPermissions { @Override public ChannelPermissionResult joinPermitted(final CarbonPlayer player) { return channelPermissionResult( player.hasPermission(this.permission()), () -> this.messages.channelNoPermission(player) ); } @Override public ChannelPermissionResult speechPermitted(final CarbonPlayer player) { return channelPermissionResult( player.hasPermission(this.permission() + ".speak"), () -> this.messages.channelNoPermission(player) ); } @Override public ChannelPermissionResult hearingPermitted(final CarbonPlayer player) { return channelPermissionResult( player.hasPermission(this.permission() + ".see"), () -> this.messages.channelNoPermission(player) ); } @Override public boolean dynamic() { return false; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/channels/ConfigChannelSettings.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.Collections; import java.util.List; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Setting; @ConfigSerializable public final class ConfigChannelSettings { @Comment(""" The channel's key, used to track the channel. 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 final @Nullable Key key = Key.key("carbon", "basic"); @Comment(""" The permission required to use the channel. To read messages you must have the permission carbon.channel.basic.see To send messages you must have the permission carbon.channel.basic.speak If you want to give both, grant carbon.channel.basic or carbon.channel.basic.* """) private final @Nullable String permission = "carbon.channel.basic"; @Setting("format") @Comment("The chat formats for this channel.") private final @Nullable ConfigChannelMessageSource messageSource = new ConfigChannelMessageSource(); @Comment("Messages will be sent in this channel if they start with this prefix.") private final @Nullable String quickPrefix = ""; private final @Nullable Boolean shouldRegisterCommands = true; private final @Nullable String commandName = null; private final @Nullable List commandAliases = Collections.emptyList(); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/channels/ConfigChatChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import io.leangen.geantyref.TypeToken; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.draycia.carbon.common.channels.messages.ConfigChannelMessages; import net.draycia.carbon.common.messages.CarbonMessageRenderer; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.messages.SourcedAudience; import net.draycia.carbon.common.messages.SourcedMessageSender; import net.draycia.carbon.common.messages.SourcedReceiverResolver; 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.LongPlaceholderResolver; import net.draycia.carbon.common.messages.placeholders.StringPlaceholderResolver; import net.draycia.carbon.common.messages.placeholders.UUIDPlaceholderResolver; import net.draycia.carbon.common.util.Exceptions; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.moonshine.Moonshine; import net.kyori.moonshine.exception.scan.UnscannableMethodException; import net.kyori.moonshine.strategy.StandardPlaceholderResolverStrategy; import net.kyori.moonshine.strategy.supertype.StandardSupertypeThenInterfaceSupertypeStrategy; 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; import org.jetbrains.annotations.NotNull; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Setting; import static java.util.Objects.requireNonNull; @ConfigSerializable @DefaultQualifier(NonNull.class) public class ConfigChatChannel implements ChatChannel { protected transient @MonotonicNonNull @Inject CarbonServer server; private transient @MonotonicNonNull @Inject CarbonMessageRenderer renderer; protected transient @MonotonicNonNull @Inject CarbonMessages messages; @Comment(""" The channel's key, used to track the channel. 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.""") protected @Nullable Key key = Key.key("carbon", "global"); @Comment(""" The permission required to use the /channel and / commands. Assuming permission = "carbon.channel.global" To read messages you must have the permission carbon.channel.global.see To send messages you must have the permission carbon.channel.global.speak""") private @Nullable String permission = null; @Setting("format") @Comment("The chat formats for this channel.") protected @Nullable ConfigChannelMessageSource messageSource = new ConfigChannelMessageSource(); @Comment("Messages will be sent in this channel if they start with this prefix. (Leave empty/blank to disable quick prefix for this channel)") private @Nullable String quickPrefix = ""; private @Nullable Boolean shouldRegisterCommands = true; private @Nullable String commandName = null; protected @Nullable List commandAliases = Collections.emptyList(); private transient @Nullable ConfigChannelMessages carbonMessages = null; @Comment(""" The distance players must be within to see each other's messages. A value of '0' requires that both players are in the same world. On velocity, '0' requires that both players are in the same server.""") private int radius = -1; @Comment(""" If true, players will be able to see if they're not sending messages to anyone because they're out of range from the radius.""") private boolean emptyRadiusRecipientsMessage = true; private Map cooldowns = new HashMap<>(); private long cooldown = -1; @Comment("Whether this channel's messages should be sent cross-server.") private boolean crossServer = true; @Override public @Nullable String quickPrefix() { if (this.quickPrefix == null || this.quickPrefix.isBlank()) { return null; } return this.quickPrefix; } @Override public boolean shouldRegisterCommands() { return Objects.requireNonNullElse(this.shouldRegisterCommands, true); } @Override public String commandName() { return Objects.requireNonNullElse(this.commandName, this.key.value()); } @Override public List commandAliases() { return Objects.requireNonNullElse(this.commandAliases, Collections.emptyList()); } @Override public @NotNull Component render( final CarbonPlayer sender, final Audience recipient, final Component message, final Component originalMessage ) { return this.carbonMessages().chatFormat( SourcedAudience.of(sender, recipient), sender.uuid(), this.key(), sender.displayName(), sender.username(), message, Component.text("null") ); } @Override public ChannelPermissions permissions() { return new ChannelPermissionsImpl(this.permission(), this.messages); } @Override public List recipients(final CarbonPlayer sender) { final List recipients = new ArrayList<>(); for (final CarbonPlayer player : this.server.players()) { if (this.permissions().hearingPermitted(player).permitted() && !player.leftChannels().contains(this.key)) { recipients.add(player); } } // console too! recipients.add(this.server.console()); return recipients; } @Override public long cooldown() { return this.cooldown * 1000; // Seconds to millis } @Override public long playerCooldown(final CarbonPlayer player) { if (this.cooldown() <= 0) { return 0; } return Objects.requireNonNullElse(this.cooldowns.get(player.uuid()), 0L); } public long startCooldown(final CarbonPlayer player) { final long expiresAt = System.currentTimeMillis() + this.cooldown(); return Objects.requireNonNullElse(this.cooldowns.put(player.uuid(), expiresAt), 0L); } @Override public @NonNull Key key() { return Objects.requireNonNull(this.key); } public String messageFormat(final CarbonPlayer sender) { return this.messageSource.messageOf(SourcedAudience.of(sender, sender), ""); } private ConfigChannelMessages loadMessages() { final SourcedReceiverResolver serverReceiverResolver = new SourcedReceiverResolver(); final ComponentPlaceholderResolver componentPlaceholderResolver = new ComponentPlaceholderResolver<>(); final UUIDPlaceholderResolver uuidPlaceholderResolver = new UUIDPlaceholderResolver<>(); final StringPlaceholderResolver stringPlaceholderResolver = new StringPlaceholderResolver<>(); final KeyPlaceholderResolver keyPlaceholderResolver = new KeyPlaceholderResolver<>(); final BooleanPlaceholderResolver booleanPlaceholderResolver = new BooleanPlaceholderResolver<>(); final SourcedMessageSender carbonMessageSender = new SourcedMessageSender(); try { return Moonshine.builder(new TypeToken() {}) .receiverLocatorResolver(serverReceiverResolver, 0) .sourced(this.messageSource) .rendered(this.renderer.asSourced()) .sent(carbonMessageSender) .resolvingWithStrategy(new StandardPlaceholderResolverStrategy<>(new StandardSupertypeThenInterfaceSupertypeStrategy(false))) .weightedPlaceholderResolver(Component.class, componentPlaceholderResolver, 0) .weightedPlaceholderResolver(UUID.class, uuidPlaceholderResolver, 0) .weightedPlaceholderResolver(String.class, stringPlaceholderResolver, 0) .weightedPlaceholderResolver(Integer.class, new IntPlaceholderResolver<>(), 0) .weightedPlaceholderResolver(Long.class, new LongPlaceholderResolver<>(), 0) .weightedPlaceholderResolver(Key.class, keyPlaceholderResolver, 0) .weightedPlaceholderResolver(Boolean.class, booleanPlaceholderResolver, 0) .create(this.getClass().getClassLoader()); } catch (final UnscannableMethodException e) { throw Exceptions.rethrow(e); } } protected ConfigChannelMessages carbonMessages() { if (this.carbonMessages == null) { this.carbonMessages = this.loadMessages(); } return requireNonNull(this.carbonMessages, "Channel message service must not be null!"); } private String permission() { if (this.permission == null) { return "carbon.channel." + this.key().value(); } return this.permission; } @Override public double radius() { return this.radius; } @Override public boolean emptyRadiusRecipientsMessage() { return this.emptyRadiusRecipientsMessage; } @Override public boolean shouldCrossServer() { return this.crossServer; } @Override public boolean equals(final Object other) { if (!(other instanceof ConfigChatChannel otherChannel)) { return false; } return otherChannel.key().equals(this.key()); } @Override public int hashCode() { return Objects.hash(this.key()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/channels/PartyChatChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.Map; import java.util.Objects; import java.util.UUID; import net.draycia.carbon.api.channels.ChannelPermissions; 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 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; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @ConfigSerializable @DefaultQualifier(NonNull.class) public class PartyChatChannel extends ConfigChatChannel { public static final String FILE_NAME = "partychat.conf"; public PartyChatChannel() { this.key = Key.key("carbon", "partychat"); this.commandAliases = List.of("pc"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(party: ) : ", "console", "[party: ] : " ); } @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( player.party().join() != null, () -> this.messages.cannotUsePartyChannel(player) )); } @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(this.messages.cannotUsePartyChannel(sender)); } 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() ); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessageSource.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.messages; import java.util.Locale; import java.util.Map; import java.util.Objects; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.messages.SourcedAudience; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.draycia.carbon.common.util.DiscordRecipient; import net.kyori.adventure.audience.Audience; import net.kyori.moonshine.message.IMessageSource; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Setting; @ConfigSerializable @DefaultQualifier(NonNull.class) public class ConfigChannelMessageSource implements IMessageSource { // Map -> Map // "default" key will be configurable but let's not worry about that for now @Setting("basic") @Comment(""" Basic chat formats. The "default_format" format is the main one you want to edit. The "console" format is what's shown to console. The "discord" format is what's shown to supported discord integrations. If PlaceholderAPI is installed, PAPI placeholders (with %) are supported. If MiniPlaceholders is installed, its placeholders (with <>) are supported. The keys are group names, the values are chat formats (MiniMessage). For example: basic { default_format="<> " vip="[VIP] <> " admin="[Prefix] : " discord="" } """) public Map defaults = Map.of( "default_format", ": ", "console", "[] : ", "discord", "" ); @Comment(""" Per-Language chat formats. You can safely leave this section empty if you don't want to use this feature. Each locale section can be configured in the same way as the above 'basic' section. Will fall back to the 'basic' section if no format was found for the player's locale.""") public Map> locales = Map.of(Locale.getDefault(), Map.of()); private static final String FALLBACK_FORMAT = "<> "; // TODO: Remove DiscordRecipient and use key instead (Couldn't figure out how to do it) @Override public String messageOf(final SourcedAudience sourcedAudience, final String ignored) { if (sourcedAudience.recipient() instanceof CarbonPlayer && !(sourcedAudience.recipient() instanceof ConsoleCarbonPlayer)) { return this.forPlayer(sourcedAudience); } else if (sourcedAudience.recipient() instanceof DiscordRecipient) { return this.defaults.getOrDefault("discord", FALLBACK_FORMAT); } else { return this.defaults.getOrDefault("console", FALLBACK_FORMAT); } } private String forPlayer(final SourcedAudience sourcedAudience) { final var sender = (CarbonPlayer) sourcedAudience.sender(); final var recipient = (CarbonPlayer) sourcedAudience.recipient(); if (recipient.locale() != null) { final var formats = this.locales.get(recipient.locale()); if (formats != null) { final @Nullable String format = formats.get(sender.primaryGroup()); if (format != null) { return format; } for (final var groupEntry : sender.groups()) { final @Nullable String groupFormat = formats.get(groupEntry); if (groupFormat != null) { return groupFormat; } } } } final @Nullable String format = this.defaults.get(sender.primaryGroup()); if (format != null) { return format; } for (final var groupEntry : sender.groups()) { final @Nullable String groupFormat = this.defaults.get(groupEntry); if (groupFormat != null) { return groupFormat; } } return Objects.requireNonNullElse(this.defaults.get("default_format"), FALLBACK_FORMAT); } private String forAudience(final Audience audience) { return Objects.requireNonNullElse(this.defaults.get("console"), FALLBACK_FORMAT); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessages.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.messages; import java.util.UUID; import net.draycia.carbon.common.messages.SourcedAudience; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.moonshine.annotation.Message; import net.kyori.moonshine.annotation.Placeholder; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public interface ConfigChannelMessages { // TODO: locale placeholders? @Message("channel.format") Component chatFormat( SourcedAudience audience, @Placeholder UUID uuid, @Placeholder Key channel, @Placeholder("display_name") Component displayName, @Placeholder String username, @Placeholder Component message, @Placeholder("party_name") Component partyName ); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/CarbonCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import java.util.Objects; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public abstract class CarbonCommand { private @Nullable CommandSettings commandSettings = null; public CommandSettings commandSettings() { return Objects.requireNonNullElseGet(this.commandSettings, this::defaultCommandSettings); } public void commandSettings(final @NonNull CommandSettings commandSettings) { this.commandSettings = commandSettings; } public abstract void init(); public abstract CommandSettings defaultCommandSettings(); public abstract Key key(); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/CommandSettings.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @ConfigSerializable public class CommandSettings { private boolean enabled = true; private String name = ""; private String[] aliases = new String[0]; public CommandSettings() { } public CommandSettings(final boolean enabled, final String name, final String... aliases) { this.enabled = enabled; this.name = name; this.aliases = aliases; } public CommandSettings(final String name, final String... aliases) { this(true, name, aliases); } public boolean enabled() { return this.enabled; } public String name() { return this.name; } public String[] aliases() { return this.aliases; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/Commander.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import net.kyori.adventure.audience.Audience; public interface Commander extends Audience { boolean hasPermission(String permission); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/ExecutionCoordinatorHolder.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import net.draycia.carbon.common.util.ConcurrentUtil; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.execution.ExecutionCoordinator; @DefaultQualifier(NonNull.class) public record ExecutionCoordinatorHolder( ExecutionCoordinator executionCoordinator, ExecutorService executorService ) { public void shutdown() { ConcurrentUtil.shutdownExecutor(this.executorService, TimeUnit.MILLISECONDS, 50); } public static ExecutionCoordinatorHolder create(final Logger logger) { final ExecutorService executorService = Executors.newFixedThreadPool(4, ConcurrentUtil.carbonThreadFactory(logger, "Commands")); return new ExecutionCoordinatorHolder( ExecutionCoordinator.coordinatorFor(executorService), executorService ); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/ParserFactory.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import net.draycia.carbon.common.command.argument.CarbonPlayerParser; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public interface ParserFactory { CarbonPlayerParser carbonPlayer(); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/PlayerCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import net.draycia.carbon.api.users.CarbonPlayer; import org.checkerframework.checker.nullness.qual.NonNull; public interface PlayerCommander extends Commander { @NonNull CarbonPlayer carbonPlayer(); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/argument/CarbonPlayerParser.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.argument; import com.google.inject.Inject; import io.leangen.geantyref.TypeToken; import java.util.concurrent.CompletableFuture; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.command.Commander; import net.draycia.carbon.common.command.exception.ComponentException; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.users.ProfileResolver; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.context.CommandContext; import org.incendo.cloud.context.CommandInput; import org.incendo.cloud.parser.ArgumentParseResult; import org.incendo.cloud.parser.ArgumentParser; import org.incendo.cloud.parser.ParserDescriptor; import org.incendo.cloud.suggestion.SuggestionProvider; @DefaultQualifier(NonNull.class) public final class CarbonPlayerParser implements ArgumentParser.FutureArgumentParser, ParserDescriptor { private final PlayerSuggestions suggestions; private final UserManager userManager; private final ProfileResolver profileResolver; private final CarbonMessages messages; @Inject private CarbonPlayerParser( final PlayerSuggestions suggestions, final UserManager userManager, final ProfileResolver profileResolver, final CarbonMessages messages ) { this.suggestions = suggestions; this.userManager = userManager; this.profileResolver = profileResolver; this.messages = messages; } @Override public CompletableFuture> parseFuture( final CommandContext commandContext, final CommandInput commandInput ) { final String input = commandInput.readString(); return this.profileResolver.resolveUUID(input, commandContext.isSuggestions()).thenCompose(uuid -> { if (uuid == null) { return ArgumentParseResult.failureFuture(new ParseException(input, this.messages)); } return this.userManager.user(uuid).thenApply(ArgumentParseResult::success); }); } @Override public @NonNull SuggestionProvider suggestionProvider() { return this.suggestions; } @Override public @NonNull TypeToken valueType() { return TypeToken.get(CarbonPlayer.class); } @Override public @NonNull ArgumentParser parser() { return this; } public static final class ParseException extends ComponentException { private static final long serialVersionUID = -8331761537951077684L; private final String input; public ParseException(final String input, final CarbonMessages messages) { super(messages.errorCommandInvalidPlayer(input)); this.input = input; } public String input() { return this.input; } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/argument/PlayerSuggestions.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.argument; import net.draycia.carbon.common.command.Commander; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.suggestion.SuggestionProvider; @DefaultQualifier(NonNull.class) public interface PlayerSuggestions extends SuggestionProvider { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/ClearChatCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.CarbonServer; 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.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.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; @DefaultQualifier(NonNull.class) public final class ClearChatCommand extends CarbonCommand { private final CarbonServer server; private final CommandManager commandManager; private final ConfigManager configManager; private final CarbonMessages carbonMessages; @Inject public ClearChatCommand( final CarbonServer server, final CommandManager commandManager, final ConfigManager configManager, final CarbonMessages carbonMessages ) { this.server = server; this.commandManager = commandManager; this.configManager = configManager; this.carbonMessages = carbonMessages; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("clearchat", "chatclear", "cc"); } @Override public Key key() { return Key.key("carbon", "clearchat"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .permission("carbon.clearchat.clear") .commandDescription(richDescription(this.carbonMessages.commandClearChatDescription())) .handler(handler -> { // Not fond of having to send 50 messages to each player // Are we not able to just paste in 50 newlines and call it a day? for (int i = 0; i < this.configManager.primaryConfig().clearChatSettings().iterations(); i++) { for (final var player : this.server.players()) { if (!player.hasPermission("carbon.clearchat.exempt")) { player.sendMessage(this.configManager.primaryConfig().clearChatSettings().message()); } } } final Component senderName; final String username; if (handler.sender() instanceof PlayerCommander player) { senderName = player.carbonPlayer().displayName(); username = player.carbonPlayer().username(); } else { senderName = Component.text("Console"); username = "Console"; } this.server.sendMessage(this.configManager.primaryConfig().clearChatSettings() .broadcast(senderName, username)); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/ContinueCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.UUID; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.messages.CarbonMessages; import net.kyori.adventure.key.Key; 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; import org.incendo.cloud.CommandManager; import org.incendo.cloud.minecraft.signed.SignedString; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.minecraft.signed.SignedGreedyStringParser.signedGreedyStringParser; @DefaultQualifier(NonNull.class) public final class ContinueCommand extends CarbonCommand { private final UserManager users; private final CommandManager commandManager; private final CarbonMessages messages; private final WhisperCommand.WhisperHandler whisper; @Inject public ContinueCommand( final UserManager userManager, final CommandManager commandManager, final CarbonMessages messages, final WhisperCommand.WhisperHandler whisper ) { this.users = userManager; this.commandManager = commandManager; this.messages = messages; this.whisper = whisper; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("continue", "c"); } @Override public Key key() { return Key.key("carbon", "continue"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .required("message", signedGreedyStringParser(), richDescription(this.messages.commandContinueArgumentMessage())) .permission("carbon.whisper.continue") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.messages.commandContinueDescription())) .handler(ctx -> { final CarbonPlayer sender = ctx.sender().carbonPlayer(); if (sender.muted()) { this.messages.muteCannotSpeak(sender); return; } final SignedString message = ctx.get("message"); final @Nullable UUID whisperTarget = sender.lastWhisperTarget(); if (whisperTarget == null) { this.messages.whisperTargetNotSet(sender, sender.displayName()); return; } final @MonotonicNonNull CarbonPlayer recipient = this.users.user(whisperTarget).join(); this.whisper.whisper(sender, recipient, message); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/DebugCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.ArrayList; import net.draycia.carbon.api.users.CarbonPlayer; 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.ParserFactory; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.format.NamedTextColor; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; @DefaultQualifier(NonNull.class) public final class DebugCommand extends CarbonCommand { private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final ParserFactory parserFactory; @Inject public DebugCommand( final CommandManager commandManager, final CarbonMessages carbonMessages, final ParserFactory parserFactory ) { this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.parserFactory = parserFactory; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("carbondebug", "cdebug"); } @Override public Key key() { return Key.key("carbon", "debug"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .optional("player", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandDebugArgumentPlayer())) .permission("carbon.debug") .senderType(Commander.class) .commandDescription(richDescription(this.carbonMessages.commandDebugDescription())) .handler(handler -> { final Commander sender = handler.sender(); final CarbonPlayer target; if (handler.contains("player")) { target = handler.get("player"); } else if (sender instanceof PlayerCommander playerCommander) { target = playerCommander.carbonPlayer(); } else { return; } sender.sendMessage( Component.join(JoinConfiguration.noSeparators(), Component.text("Primary Group: ", NamedTextColor.GOLD), Component.text(target.primaryGroup(), NamedTextColor.GREEN)) ); final var groups = new ArrayList(); for (final var group : target.groups()) { groups.add(Component.text(group, NamedTextColor.GREEN)); } final var formattedGroupsList = Component.join(JoinConfiguration.separator( Component.text(", ", NamedTextColor.YELLOW)), groups ); sender.sendMessage( Component.join(JoinConfiguration.noSeparators(), Component.text("Groups: ", NamedTextColor.GOLD), formattedGroupsList ) ); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/FilterCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.users.CarbonPlayer; 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.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.BooleanParser.booleanParser; @DefaultQualifier(NonNull.class) public final class FilterCommand extends CarbonCommand { private final CommandManager commandManager; private final CarbonMessages carbonMessages; @Inject public FilterCommand( final CommandManager commandManager, final CarbonMessages carbonMessages ) { this.commandManager = commandManager; this.carbonMessages = carbonMessages; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("filter"); } @Override public Key key() { return Key.key("carbon", "filter"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .optional("enabled", booleanParser()) .permission("carbon.filter") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandOptionalFilterDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); boolean enabled = !sender.applyOptionalChatFilters(); if (handler.contains("enabled")) { enabled = handler.get("enabled"); } sender.applyOptionalChatFilters(enabled); if (enabled) { this.carbonMessages.commandOptionalFilterEnabled(sender); } else { this.carbonMessages.commandOptionalFilterDisabled(sender); } }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/HelpCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.Map; import java.util.concurrent.CompletableFuture; 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.messages.CarbonMessageSource; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.context.CommandContext; import org.incendo.cloud.context.CommandInput; import org.incendo.cloud.help.result.CommandEntry; import org.incendo.cloud.minecraft.extras.AudienceProvider; import org.incendo.cloud.minecraft.extras.MinecraftHelp; import org.incendo.cloud.suggestion.Suggestion; import org.intellij.lang.annotations.Subst; import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; import static net.kyori.adventure.text.format.NamedTextColor.GRAY; import static net.kyori.adventure.text.format.NamedTextColor.WHITE; import static net.kyori.adventure.text.format.TextColor.color; import static org.incendo.cloud.minecraft.extras.MinecraftHelp.helpColors; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; @DefaultQualifier(NonNull.class) public final class HelpCommand extends CarbonCommand { private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final MinecraftHelp minecraftHelp; @Inject public HelpCommand( final CommandManager commandManager, final CarbonMessageSource messageSource, final CarbonMessages carbonMessages ) { this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.minecraftHelp = createHelp(commandManager, messageSource); } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("carbon"); } @Override public Key key() { return Key.key("carbon", "help"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .literal("help") .optional("query", greedyStringParser(), richDescription(this.carbonMessages.commandHelpArgumentQuery()), this::suggestQueries) .permission("carbon.help") .commandDescription(richDescription(this.carbonMessages.commandHelpDescription())) .handler(this::execute) .build(); this.commandManager.command(command); } private void execute(final CommandContext ctx) { this.minecraftHelp.queryCommands(ctx.getOrDefault("query", ""), ctx.sender()); } private CompletableFuture> suggestQueries(final CommandContext ctx, final CommandInput input) { final var result = this.commandManager.createHelpHandler().queryRootIndex(ctx.sender()); return CompletableFuture.completedFuture(result.entries().stream().map(CommandEntry::syntax).map(Suggestion::suggestion).toList()); } private static MinecraftHelp createHelp( final CommandManager manager, final CarbonMessageSource messageSource ) { return MinecraftHelp.builder() .commandManager(manager) .audienceProvider(AudienceProvider.nativeAudience()) .commandPrefix("/carbon help") .colors(helpColors( color(0xE099FF), WHITE, color(0xDD1BC4), GRAY, DARK_GRAY )) .messageProvider((sender, key, args) -> { final String messageKey = "command.help.misc." + key; final TagResolver.Builder tagResolver = TagResolver.builder(); for (final Map.Entry entry : args.entrySet()) { @Subst("key") final String k = entry.getKey(); tagResolver.resolver(Placeholder.parsed(k, entry.getValue())); } return MiniMessage.miniMessage().deserialize(messageSource.messageOf(sender, messageKey), tagResolver.build()); }) .build(); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/IgnoreCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.ParserFactory; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.UUIDParser.uuidParser; @DefaultQualifier(NonNull.class) public final class IgnoreCommand extends CarbonCommand { private final UserManager users; private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final ParserFactory parserFactory; @Inject public IgnoreCommand( final UserManager userManager, final CommandManager commandManager, final CarbonMessages carbonMessages, final ParserFactory parserFactory ) { this.users = userManager; this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.parserFactory = parserFactory; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("ignore", "block"); } @Override public Key key() { return Key.key("carbon", "ignore"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .optional("player", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandIgnoreArgumentPlayer())) .flag(this.commandManager.flagBuilder("uuid") .withAliases("u") .withDescription(richDescription(this.carbonMessages.commandIgnoreArgumentUUID())) .withComponent(uuidParser()) ) .permission("carbon.ignore") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandIgnoreDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); final CarbonPlayer target; if (handler.contains("player")) { target = handler.get("player"); } else if (handler.flags().contains("uuid")) { target = this.users.user(handler.flags().get("uuid")).join(); } else { this.carbonMessages.ignoreTargetInvalid(sender); return; } if (target.hasPermission("carbon.ignore.exempt")) { this.carbonMessages.ignoreExempt(sender, target.displayName()); return; } if (sender.ignoring(target)) { this.carbonMessages.alreadyIgnored(sender, target.displayName()); return; } sender.ignoring(target, true); this.carbonMessages.nowIgnoring(sender, target.displayName()); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/IgnoreListCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.function.Supplier; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.messages.CarbonMessages; import net.draycia.carbon.common.util.Pagination; import net.draycia.carbon.common.util.PaginationHelper; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.component.DefaultValue; import org.incendo.cloud.context.CommandContext; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.IntegerParser.integerParser; @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 PaginationHelper pagination ) { this.users = userManager; this.commandManager = commandManager; this.messages = messages; this.pagination = pagination; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("ignorelist", "listignores"); } @Override public Key key() { return Key.key("carbon", "ignorelist"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .permission("carbon.ignore") .senderType(PlayerCommander.class) .optional("page", integerParser(1), DefaultValue.constant(1)) .commandDescription(richDescription(this.messages.commandIgnoreListDescription())) .handler(this::execute) .build(); this.commandManager.command(command); } private void execute(final CommandContext ctx) { final CarbonPlayer sender = ctx.sender().carbonPlayer(); final var elements = sender.ignoring().stream() .sorted() // this way page numbers make sense .map(id -> (Supplier) () -> this.users.user(id).join()) .toList(); if (elements.isEmpty()) { this.messages.commandIgnoreListNoneIgnored(sender); return; } final Pagination> pagination = Pagination.>builder() .header(this.messages::commandIgnoreListPaginationHeader) .item((e, lastOfPage) -> { final CarbonPlayer p = e.get(); return this.messages.commandIgnoreListPaginationElement(p.displayName(), p.username()); }) .footer(this.pagination.footerRenderer(p -> "/" + this.commandSettings().name() + " " + p)) .pageOutOfRange(this.messages::paginationOutOfRange) .build(); final int page = ctx.get("page"); pagination.render(elements, page, 6).forEach(sender::sendMessage); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/JoinCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.Objects; import net.draycia.carbon.api.channels.ChannelPermissionResult; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.CarbonChannelRegistry; 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.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.suggestion.Suggestion; import org.incendo.cloud.suggestion.SuggestionProvider; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; @DefaultQualifier(NonNull.class) public final class JoinCommand extends CarbonCommand { private final CarbonChannelRegistry channelRegistry; private final CommandManager commandManager; private final CarbonMessages carbonMessages; @Inject public JoinCommand( final CarbonChannelRegistry channelRegistry, final CommandManager commandManager, final CarbonMessages carbonMessages ) { this.channelRegistry = channelRegistry; this.commandManager = commandManager; this.carbonMessages = carbonMessages; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("join"); } @Override public Key key() { return Key.key("carbon", "join"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .required("channel", greedyStringParser(), SuggestionProvider.blocking((context, s) -> { final CarbonPlayer sender = ((PlayerCommander) context.sender()).carbonPlayer(); return sender.leftChannels().stream() .map(this.channelRegistry::channel) .filter(Objects::nonNull) .filter(channel -> channel.permissions().joinPermitted(sender).permitted() || channel.permissions().hearingPermitted(sender).permitted() || channel.permissions().speechPermitted(sender).permitted()) .map(channel -> channel.key().value()) .map(Suggestion::suggestion) .toList(); })) .permission("carbon.join") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandJoinDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); final @Nullable ChatChannel channel = this.channelRegistry.channelByValue(handler.get("channel")); if (channel == null) { this.carbonMessages.channelNotFound(sender); return; } final ChannelPermissionResult permitted = channel.permissions().joinPermitted(sender); if (!permitted.permitted()) { sender.sendMessage(permitted.reason()); return; } if (!sender.leftChannels().contains(channel.key())) { this.carbonMessages.channelNotLeft(sender); return; } sender.joinChannel(channel); this.carbonMessages.channelJoined(sender); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/LeaveCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.Objects; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.CarbonChannelRegistry; 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.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.suggestion.Suggestion; import org.incendo.cloud.suggestion.SuggestionProvider; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; @DefaultQualifier(NonNull.class) public final class LeaveCommand extends CarbonCommand { private final CarbonChannelRegistry channelRegistry; private final CommandManager commandManager; private final CarbonMessages carbonMessages; @Inject public LeaveCommand( final CarbonChannelRegistry channelRegistry, final CommandManager commandManager, final CarbonMessages carbonMessages ) { this.channelRegistry = channelRegistry; this.commandManager = commandManager; this.carbonMessages = carbonMessages; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("leave"); } @Override public Key key() { return Key.key("carbon", "leave"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .required("channel", greedyStringParser(), SuggestionProvider.blocking((context, s) -> { final CarbonPlayer sender = ((PlayerCommander) context.sender()).carbonPlayer(); return this.channelRegistry.keys().stream() .map(this.channelRegistry::channel) .filter(Objects::nonNull) .filter(x -> !sender.leftChannels().contains(x.key()) && (x.permissions().joinPermitted(sender).permitted() || x.permissions().hearingPermitted(sender).permitted() || x.permissions().speechPermitted(sender).permitted())) .map(x -> x.key().value()) .map(Suggestion::suggestion) .toList(); })) .permission("carbon.leave") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandLeaveDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); final @Nullable ChatChannel channel = this.channelRegistry.channelByValue(handler.get("channel")); if (channel == null) { this.carbonMessages.channelNotFound(sender); return; } if (sender.leftChannels().contains(channel.key())) { this.carbonMessages.channelAlreadyLeft(sender); return; } sender.leaveChannel(channel); this.carbonMessages.channelLeft(sender); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/MuteCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.time.Duration; import java.time.Instant; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.ParserFactory; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.messages.CarbonMessages; 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; import org.incendo.cloud.CommandManager; import org.incendo.cloud.parser.standard.DurationParser; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.UUIDParser.uuidParser; @DefaultQualifier(NonNull.class) public final class MuteCommand extends CarbonCommand { private final CarbonServer server; private final UserManager users; private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final ParserFactory parserFactory; @Inject public MuteCommand( final UserManager userManager, final CarbonServer server, final CommandManager commandManager, final CarbonMessages carbonMessages, final ParserFactory parserFactory ) { this.server = server; this.users = userManager; this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.parserFactory = parserFactory; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("mute"); } @Override public Key key() { return Key.key("carbon", "mute"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .optional("player", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandMuteArgumentPlayer())) .flag(this.commandManager.flagBuilder("uuid") .withAliases("u") .withDescription(richDescription(this.carbonMessages.commandMuteArgumentUUID())) .withComponent(uuidParser()) ) .flag(this.commandManager.flagBuilder("duration") .withAliases("d") .withDescription(richDescription(this.carbonMessages.commandMuteArgumentDuration())) .withComponent(DurationParser.durationParser()) ) .permission("carbon.mute") .senderType(Commander.class) .commandDescription(richDescription(this.carbonMessages.commandMuteDescription())) .handler(handler -> { final Commander sender = handler.sender(); final CarbonPlayer target; if (handler.contains("player")) { target = handler.get("player"); } else if (handler.flags().contains("uuid")) { target = this.users.user(handler.flags().get("uuid")).join(); } else { this.carbonMessages.muteNoTarget(sender); // TODO: send command syntax return; } if (target.hasPermission("carbon.mute.exempt")) { this.carbonMessages.muteExempt(sender); return; } if (sender instanceof PlayerCommander playerCommander && playerCommander.carbonPlayer().equals(target)) { this.carbonMessages.muteExempt(playerCommander); return; } if (handler.flags().contains("duration")) { this.handleTempMute(handler.flags().get("duration"), target); return; } this.carbonMessages.muteAlertRecipient(target); this.carbonMessages.muteAlertRecipient(this.server.console()); for (final var player : this.server.players()) { if (player.equals(target)) { continue; } if (player.hasPermission("carbon.mute.alert")) { this.carbonMessages.muteAlertPlayers(player, target.displayName()); } } target.muteExpiration(0); target.muted(true); }) .build(); this.commandManager.command(command); } private void handleTempMute(final Duration duration, final CarbonPlayer target) { final @Nullable Component formattedDuration; if (duration.toDaysPart() > 0) { formattedDuration = this.carbonMessages.durationDays(duration.toDaysPart(), duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()); } else { formattedDuration = this.carbonMessages.durationHours(duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()); } this.carbonMessages.tempMuteAlertRecipient(target, formattedDuration); this.carbonMessages.tempMuteAlertRecipient(this.server.console(), formattedDuration); for (final var player : this.server.players()) { if (player.equals(target)) { continue; } if (player.hasPermission("carbon.mute.alert")) { this.carbonMessages.tempMuteAlertPlayers(player, target.displayName(), formattedDuration); } } target.muted(true); target.muteExpiration(Instant.now().plus(duration).toEpochMilli()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/MuteInfoCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.time.Duration; import java.time.Instant; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.ParserFactory; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.messages.CarbonMessages; 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; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.UUIDParser.uuidParser; @DefaultQualifier(NonNull.class) public final class MuteInfoCommand extends CarbonCommand { private final UserManager users; private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final ParserFactory parserFactory; @Inject public MuteInfoCommand( final UserManager userManager, final CommandManager commandManager, final CarbonMessages carbonMessages, final ParserFactory parserFactory ) { this.users = userManager; this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.parserFactory = parserFactory; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("muteinfo", "muted"); } @Override public Key key() { return Key.key("carbon", "muteinfo"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .optional("player", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandMuteInfoArgumentPlayer())) .flag(this.commandManager.flagBuilder("uuid") .withAliases("u") .withDescription(richDescription(this.carbonMessages.commandMuteInfoArgumentUUID())) .withComponent(uuidParser()) ) .permission("carbon.mute.info") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandMuteInfoDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); final CarbonPlayer target; if (handler.contains("player")) { target = handler.get("player"); } else if (handler.flags().contains("uuid")) { target = this.users.user(handler.flags().get("uuid")).join(); } else { target = sender; } if (!target.muted()) { if (sender.equals(target)) { this.carbonMessages.muteInfoSelfNotMuted(sender); } else { this.carbonMessages.muteInfoNotMuted(sender, target.displayName()); } } else { if (sender.equals(target)) { this.carbonMessages.muteInfoSelfMuted(sender); } else if (target.muteExpiration() > Instant.now().toEpochMilli()) { final Duration duration = Duration.ofMillis(target.muteExpiration() - System.currentTimeMillis()); final @Nullable Component formattedDuration; if (duration.toDaysPart() > 0) { formattedDuration = this.carbonMessages.durationDays(duration.toDaysPart(), duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()); } else { formattedDuration = this.carbonMessages.durationHours(duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()); } this.carbonMessages.muteInfoMutedDuration(sender, target.displayName(), formattedDuration); } else { this.carbonMessages.muteInfoMuted(sender, target.displayName(), target.muted()); } } }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/NicknameCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.users.CarbonPlayer; 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.ParserFactory; 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.TagPermissions; import net.draycia.carbon.common.util.CloudUtils; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; @DefaultQualifier(NonNull.class) public final class NicknameCommand extends CarbonCommand { private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final ParserFactory parserFactory; private final ConfigManager config; @Inject public NicknameCommand( final CommandManager commandManager, final CarbonMessages carbonMessages, final ParserFactory parserFactory, final ConfigManager config ) { this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.parserFactory = parserFactory; this.config = config; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("nickname", "nick"); } @Override public Key key() { return Key.key("carbon", "nickname"); } @Override public void init() { if (!this.config.primaryConfig().nickname().useCarbonNicknames()) { return; } // TODO: Allow UUID input for target player final var selfRoot = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()); final var othersRoot = selfRoot.literal("player") .required("player", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandNicknameArgumentPlayer())); // Check nickname this.commandManager.command(selfRoot.permission("carbon.nickname") .commandDescription(richDescription(this.carbonMessages.commandNicknameDescription())) .handler(ctx -> this.checkOwnNickname(CloudUtils.nonPlayerMustProvidePlayer(this.carbonMessages, ctx.sender())))); this.commandManager.command(othersRoot.permission("carbon.nickname.others") .commandDescription(richDescription(this.carbonMessages.commandNicknameOthersDescription())) .handler(ctx -> this.checkOthersNickname(ctx.sender(), ctx.get("player")))); // Set nickname this.commandManager.command(selfRoot.permission("carbon.nickname.set") .commandDescription(richDescription(this.carbonMessages.commandNicknameSetDescription())) .required("nickname", greedyStringParser(), richDescription(this.carbonMessages.commandNicknameArgumentNickname())) .handler(ctx -> this.applyNickname(ctx.sender(), CloudUtils.nonPlayerMustProvidePlayer(this.carbonMessages, ctx.sender()), ctx.get("nickname")))); this.commandManager.command(othersRoot.permission("carbon.nickname.others.set") .commandDescription(richDescription(this.carbonMessages.commandNicknameOthersSetDescription())) .required("nickname", greedyStringParser(), richDescription(this.carbonMessages.commandNicknameArgumentNickname())) .handler(ctx -> this.applyNickname(ctx.sender(), ctx.get("player"), ctx.get("nickname")))); // Reset/remove nickname this.commandManager.command(selfRoot.permission("carbon.nickname.set") .commandDescription(richDescription(this.carbonMessages.commandNicknameResetDescription())) .literal("reset") .handler(ctx -> this.resetNickname(ctx.sender(), CloudUtils.nonPlayerMustProvidePlayer(this.carbonMessages, ctx.sender())))); this.commandManager.command(othersRoot.permission("carbon.nickname.others.set") .commandDescription(richDescription(this.carbonMessages.commandNicknameOthersResetDescription())) .literal("reset") .handler(ctx -> this.resetNickname(ctx.sender(), ctx.get("player")))); } private void resetNickname(final Commander sender, final CarbonPlayer target) { target.nickname(null); if (sender instanceof PlayerCommander playerCommander && playerCommander.carbonPlayer().uuid().equals(target.uuid())) { this.carbonMessages.nicknameReset(target); } else { this.carbonMessages.nicknameResetOthers(sender, target.username()); } } private void applyNickname(final Commander sender, final CarbonPlayer target, final String nick) { final Component parsedNick = parseNickname(sender, nick); final String plainNick = PlainTextComponentSerializer.plainText().serialize(parsedNick); // If the nickname is caught in the character limit, return without setting a nickname. final int minLength = this.config.primaryConfig().nickname().minLength(); final int maxLength = this.config.primaryConfig().nickname().maxLength(); if (plainNick.length() < minLength || maxLength < plainNick.length()) { this.carbonMessages.nicknameErrorCharacterLimit(sender, parsedNick, minLength, maxLength); return; } if (this.config.primaryConfig().nickname().blackList().stream().anyMatch(plainNick::equalsIgnoreCase)) { this.carbonMessages.nicknameErrorBlackList(sender, parsedNick); return; } if (!sender.hasPermission("carbon.nickname.filter") && !plainNick.matches(this.config.primaryConfig().nickname().filter())) { this.carbonMessages.nicknameErrorFilter(sender, parsedNick); return; } target.nickname(parsedNick); if (sender instanceof PlayerCommander playerCommander && playerCommander.carbonPlayer().uuid().equals(target.uuid())) { // Setting own nickname this.carbonMessages.nicknameSet(sender, parsedNick); } else { // Setting other player's nickname this.carbonMessages.nicknameSet(target, parsedNick); this.carbonMessages.nicknameSetOthers(sender, target.username(), parsedNick); } } private void checkOwnNickname(final CarbonPlayer sender) { if (sender.nickname() != null) { this.carbonMessages.nicknameShow(sender, sender.username(), sender.nickname()); } else { this.carbonMessages.nicknameShowUnset(sender, sender.username()); } } private void checkOthersNickname(final Audience sender, final CarbonPlayer target) { if (target.nickname() != null) { this.carbonMessages.nicknameShowOthers(sender, target.username(), target.nickname()); } else { this.carbonMessages.nicknameShowOthersUnset(sender, target.username()); } } private static Component parseNickname(final Commander sender, final String nick) { // trim one level of quotes, to allow for nicknames which collide with command literals return TagPermissions.parseTags(sender, TagPermissions.NICKNAME, trimQuotes(nick), sender::hasPermission); } private static String trimQuotes(final String string) { if (string.length() < 3) { return string; } final char first = string.charAt(0); if ((first == '\'' || first == '"') && string.endsWith(String.valueOf(first))) { return string.substring(1, string.length() - 1); } return string; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/PartyCommands.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 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.CarbonCommand; import net.draycia.carbon.common.command.CommandSettings; import net.draycia.carbon.common.command.Commander; import net.draycia.carbon.common.command.ParserFactory; 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.messages.TagPermissions; 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.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; import org.incendo.cloud.CommandManager; import org.incendo.cloud.component.DefaultValue; import org.incendo.cloud.context.CommandContext; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.IntegerParser.integerParser; import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser; @DefaultQualifier(NonNull.class) public final class PartyCommands extends CarbonCommand { private final CommandManager commandManager; private final ParserFactory parserFactory; 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 ParserFactory parserFactory, final UserManagerInternal userManager, final PartyInvites partyInvites, final ConfigManager config, final CarbonMessages messages, final PaginationHelper pagination, final NetworkUsers network ) { this.commandManager = commandManager; this.parserFactory = parserFactory; 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().enabled) { return; } final var root = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .senderType(PlayerCommander.class) .permission("carbon.parties"); final var info = root.commandDescription(richDescription(this.messages.partyDesc())).handler(this::info); this.commandManager.command(info); this.commandManager.command(info.literal("page") .optional("page", integerParser(1), DefaultValue.constant(1))); this.commandManager.command( root.literal("create") .commandDescription(richDescription(this.messages.partyCreateDesc())) .optional("name", greedyStringParser()) .handler(this::createParty) ); this.commandManager.command( root.literal("invite") .commandDescription(richDescription(this.messages.partyInviteDesc())) .required("player", this.parserFactory.carbonPlayer()) .handler(this::invitePlayer) ); this.commandManager.command( root.literal("accept") .commandDescription(richDescription(this.messages.partyAcceptDesc())) .optional("sender", this.parserFactory.carbonPlayer()) .handler(this::acceptInvite) ); this.commandManager.command( root.literal("leave") .commandDescription(richDescription(this.messages.partyLeaveDesc())) .handler(this::leaveParty) ); this.commandManager.command( root.literal("disband") .commandDescription(richDescription(this.messages.partyDisbandDesc())) .handler(this::disbandParty) ); } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("party", "group"); } @Override public Key key() { return Key.key("carbon", "party"); } private void info(final CommandContext ctx) { final CarbonPlayer player = ctx.sender().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 = ctx.sender().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 = TagPermissions.parseTags(player, TagPermissions.PARTY_NAME, name, player::hasPermission); 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 = ctx.sender().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(), player.username(), 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 = ctx.sender().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 = ctx.sender().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 = ctx.sender().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) {} } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/RealNameCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.Locale; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; 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.messages.CarbonMessages; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.parser.standard.StringParser; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; @DefaultQualifier(NonNull.class) public final class RealNameCommand extends CarbonCommand { private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final CarbonServer server; @Inject public RealNameCommand( final CommandManager commandManager, final CarbonMessages carbonMessages, final CarbonServer server ) { this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.server = server; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("realname", "rn"); } @Override public Key key() { return Key.key("carbon", "realname"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .required("player", StringParser.greedyStringParser(), richDescription(this.carbonMessages.commandRealNameArgumentPlayer())) .permission("carbon.realname") .senderType(Commander.class) .commandDescription(richDescription(this.carbonMessages.commandRealNameDescription())) .handler(handler -> { final String input = handler.get("player").split(" ")[0].toLowerCase(Locale.ENGLISH); boolean found = false; for (final CarbonPlayer player : this.server.players()) { if (player.vanished() && !handler.sender().hasPermission("carbon.realname.vanished")) { continue; } final String plainName = PlainTextComponentSerializer.plainText().serialize(player.displayName()).toLowerCase(Locale.ENGLISH); if (plainName.contains(input)) { found = true; this.carbonMessages.realName(handler.sender(), player.displayName(), player.username()); } } if (!found) { this.carbonMessages.realNameTargetInvalid(handler.sender(), input); } }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/ReloadCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.event.CarbonEventHandler; 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.event.events.CarbonReloadEvent; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; @DefaultQualifier(NonNull.class) public final class ReloadCommand extends CarbonCommand { private final CarbonEventHandler events; private final CommandManager commandManager; private final CarbonMessages carbonMessages; @Inject public ReloadCommand( final CarbonEventHandler eventHandler, final CommandManager commandManager, final CarbonMessages carbonMessages ) { this.events = eventHandler; this.commandManager = commandManager; this.carbonMessages = carbonMessages; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("carbon"); } @Override public Key key() { return Key.key("carbon", "reload"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .literal("reload") .permission("carbon.reload") .senderType(Commander.class) .commandDescription(richDescription(this.carbonMessages.commandReloadDescription())) .handler(handler -> { // TODO: Check if all listeners succeeded this.events.emit(new CarbonReloadEvent()); this.carbonMessages.configReloaded(handler.sender()); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/ReplyCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.UUID; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.messages.CarbonMessages; import net.kyori.adventure.key.Key; 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; import org.incendo.cloud.CommandManager; import org.incendo.cloud.minecraft.signed.SignedString; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.minecraft.signed.SignedGreedyStringParser.signedGreedyStringParser; @DefaultQualifier(NonNull.class) public final class ReplyCommand extends CarbonCommand { private final UserManager users; private final CommandManager commandManager; private final CarbonMessages messages; private final WhisperCommand.WhisperHandler whisper; @Inject public ReplyCommand( final UserManager userManager, final CommandManager commandManager, final CarbonMessages messages, final WhisperCommand.WhisperHandler whisper ) { this.users = userManager; this.commandManager = commandManager; this.messages = messages; this.whisper = whisper; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("reply", "r"); } @Override public Key key() { return Key.key("carbon", "reply"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .required("message", signedGreedyStringParser(), richDescription(this.messages.commandReplyArgumentMessage())) .permission("carbon.whisper.reply") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.messages.commandReplyDescription())) .handler(ctx -> { final CarbonPlayer sender = ctx.sender().carbonPlayer(); if (sender.muted()) { this.messages.muteCannotSpeak(sender); return; } final SignedString message = ctx.get("message"); final @Nullable UUID replyTarget = sender.whisperReplyTarget(); if (replyTarget == null) { this.messages.replyTargetNotSet(sender, sender.displayName()); return; } final @MonotonicNonNull CarbonPlayer recipient = this.users.user(replyTarget).join(); this.whisper.whisper(sender, recipient, message); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/SpyCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.users.CarbonPlayer; 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.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.BooleanParser.booleanParser; @DefaultQualifier(NonNull.class) public final class SpyCommand extends CarbonCommand { private final CommandManager commandManager; private final CarbonMessages carbonMessages; @Inject public SpyCommand( final CommandManager commandManager, final CarbonMessages carbonMessages ) { this.commandManager = commandManager; this.carbonMessages = carbonMessages; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("spy"); } @Override public Key key() { return Key.key("carbon", "spy"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .optional("enabled", booleanParser()) .permission("carbon.spy") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandSpyDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); boolean enabled = !sender.spying(); if (handler.contains("enabled")) { enabled = handler.get("enabled"); } sender.spying(enabled); if (enabled) { this.carbonMessages.commandSpyEnabled(sender); } else { this.carbonMessages.commandSpyDisabled(sender); } }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/ToggleMessagesCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.users.CarbonPlayer; 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.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; public class ToggleMessagesCommand extends CarbonCommand { private final CommandManager commandManager; private final CarbonMessages carbonMessages; @Inject public ToggleMessagesCommand( final CommandManager commandManager, final CarbonMessages carbonMessages ) { this.commandManager = commandManager; this.carbonMessages = carbonMessages; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("togglemsg", "togglepm"); } @Override public Key key() { return Key.key("carbon", "togglemsg"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .permission("carbon.togglemsg") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandToggleMsgDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); final boolean nowIgnoring = !sender.ignoringDirectMessages(); sender.ignoringDirectMessages(nowIgnoring); if (nowIgnoring) { this.carbonMessages.whispersToggledOff(sender); } else { this.carbonMessages.whispersToggledOn(sender); } }) .build(); this.commandManager.command(command); final var toggleOn = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .permission("carbon.togglemsg") .literal("on", "allow") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandToggleMsgDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); sender.ignoringDirectMessages(false); this.carbonMessages.whispersToggledOn(sender); }) .build(); this.commandManager.command(toggleOn); final var toggleOff = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .permission("carbon.togglemsg") .literal("off", "ignore") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandToggleMsgDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); sender.ignoringDirectMessages(true); this.carbonMessages.whispersToggledOff(sender); }) .build(); this.commandManager.command(toggleOff); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/UnignoreCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.ParserFactory; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.UUIDParser.uuidParser; @DefaultQualifier(NonNull.class) public final class UnignoreCommand extends CarbonCommand { private final UserManager users; private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final ParserFactory parserFactory; @Inject public UnignoreCommand( final UserManager userManager, final CommandManager commandManager, final CarbonMessages carbonMessages, final ParserFactory parserFactory ) { this.users = userManager; this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.parserFactory = parserFactory; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("unignore", "unblock"); } @Override public Key key() { return Key.key("carbon", "unignore"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) // TODO: Filter, and only show muted players, but allow inputting any player name. .optional("player", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandUnignoreArgumentPlayer())) .flag(this.commandManager.flagBuilder("uuid") .withAliases("u") .withDescription(richDescription(this.carbonMessages.commandUnignoreArgumentUUID())) .withComponent(uuidParser()) ) .permission("carbon.ignore.unignore") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandUnignoreDescription())) .handler(handler -> { final CarbonPlayer sender = handler.sender().carbonPlayer(); final CarbonPlayer target; if (handler.contains("player")) { target = handler.get("player"); } else if (handler.flags().contains("uuid")) { target = this.users.user(handler.flags().get("uuid")).join(); } else { this.carbonMessages.ignoreTargetInvalid(sender); return; } if (!sender.ignoring(target)) { this.carbonMessages.notIgnored(sender, target.displayName()); return; } sender.ignoring(target, false); this.carbonMessages.noLongerIgnoring(sender, target.displayName()); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/UnmuteCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.ParserFactory; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.UUIDParser.uuidParser; @DefaultQualifier(NonNull.class) public final class UnmuteCommand extends CarbonCommand { private final UserManager users; private final CarbonServer server; private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final ParserFactory parserFactory; @Inject public UnmuteCommand( final UserManager userManager, final CarbonServer server, final CommandManager commandManager, final CarbonMessages carbonMessages, final ParserFactory parserFactory ) { this.users = userManager; this.server = server; this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.parserFactory = parserFactory; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("unmute"); } @Override public Key key() { return Key.key("carbon", "unmute"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .optional("player", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandUnmuteArgumentPlayer())) .flag(this.commandManager.flagBuilder("uuid") .withAliases("u") .withDescription(richDescription(this.carbonMessages.commandUnmuteArgumentUUID())) .withComponent(uuidParser()) ) .permission("carbon.mute.unmute") .senderType(Commander.class) .commandDescription(richDescription(this.carbonMessages.commandUnmuteDescription())) .handler(handler -> { final Commander sender = handler.sender(); final CarbonPlayer target; if (handler.contains("player")) { target = handler.get("player"); } else if (handler.flags().contains("uuid")) { target = this.users.user(handler.flags().get("uuid")).join(); } else { this.carbonMessages.unmuteNoTarget(sender); // TODO: send command syntax return; } this.carbonMessages.unmuteAlertRecipient(target); this.carbonMessages.unmuteAlertPlayers(this.server.console(), target.displayName()); for (final var player : this.server.players()) { if (player.equals(target)) { continue; } if (!player.hasPermission("carbon.mute.notify")) { continue; } this.carbonMessages.unmuteAlertPlayers(player, target.displayName()); } target.muteExpiration(0); target.muted(false); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/UpdateUsernameCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import java.util.Objects; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; 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.ParserFactory; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.common.users.WrappedCarbonPlayer; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.parser.standard.UUIDParser.uuidParser; @DefaultQualifier(NonNull.class) public final class UpdateUsernameCommand extends CarbonCommand { private final UserManager userManager; private final CommandManager commandManager; private final CarbonMessages messageService; private final ParserFactory parserFactory; private final ProfileResolver profileResolver; @Inject public UpdateUsernameCommand( final UserManager userManager, final CommandManager commandManager, final CarbonMessages messageService, final ParserFactory parserFactory, final ProfileResolver profileResolver ) { this.userManager = userManager; this.commandManager = commandManager; this.messageService = messageService; this.parserFactory = parserFactory; this.profileResolver = profileResolver; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("updateusername", "updatename"); } @Override public Key key() { return Key.key("carbon", "updateusername"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .optional("player", this.parserFactory.carbonPlayer(), richDescription(this.messageService.commandUpdateUsernameArgumentPlayer())) .flag(this.commandManager.flagBuilder("uuid") .withAliases("u") .withDescription(richDescription(this.messageService.commandUpdateUsernameArgumentUUID())) .withComponent(uuidParser()) ) .permission("carbon.updateusername") .senderType(Commander.class) .commandDescription(richDescription(this.messageService.commandUpdateUsernameDescription())) .handler(handler -> { final CarbonPlayer sender = ((PlayerCommander) handler.sender()).carbonPlayer(); CarbonPlayer target; if (handler.contains("player")) { target = handler.get("player"); } else if (handler.flags().contains("uuid")) { target = this.userManager.user(handler.flags().get("uuid")).join(); } else { target = sender; } if (target instanceof WrappedCarbonPlayer wrappedPlayer) { target = wrappedPlayer.carbonPlayerCommon(); } else if (!(target instanceof CarbonPlayerCommon)) { this.messageService.usernameNotUpdated(sender); return; } this.messageService.usernameFetching(sender); final CarbonPlayer finalTarget = target; this.profileResolver.resolveName(target.uuid()).thenAccept(name -> { Objects.requireNonNull(name, "Unable to fetch username for player."); ((CarbonPlayerCommon) finalTarget).username(name); this.messageService.usernameUpdated(sender, name); }); }) .build(); this.commandManager.command(command); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/commands/WhisperCommand.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import com.google.inject.Provider; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonPrivateChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.RawChat; 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.ParserFactory; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.command.argument.CarbonPlayerParser; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.event.events.CarbonPrivateChatEventImpl; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.messages.SourcedAudience; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.messaging.packets.PacketFactory; import net.draycia.carbon.common.messaging.packets.WhisperPacket; import net.draycia.carbon.common.users.NetworkUsers; import net.draycia.carbon.common.util.CloudUtils; import net.kyori.adventure.chat.ChatType; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; 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; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.minecraft.signed.SignedString; import static org.incendo.cloud.minecraft.extras.RichDescription.richDescription; import static org.incendo.cloud.minecraft.signed.SignedGreedyStringParser.signedGreedyStringParser; @DefaultQualifier(NonNull.class) public final class WhisperCommand extends CarbonCommand { private final CommandManager commandManager; private final CarbonMessages carbonMessages; private final ParserFactory parserFactory; private final WhisperHandler whisper; @Inject public WhisperCommand( final CommandManager commandManager, final CarbonMessages carbonMessages, final ParserFactory parserFactory, final WhisperHandler whisper ) { this.commandManager = commandManager; this.carbonMessages = carbonMessages; this.parserFactory = parserFactory; this.whisper = whisper; } @Override public CommandSettings defaultCommandSettings() { return new CommandSettings("whisper", "w", "message", "msg", "m", "tell"); } @Override public Key key() { return Key.key("carbon", "whisper"); } @Override public void init() { final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases()) .required("player", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandWhisperArgumentPlayer())) .required("message", signedGreedyStringParser(), richDescription(this.carbonMessages.commandWhisperArgumentMessage())) .permission("carbon.whisper.message") .senderType(PlayerCommander.class) .commandDescription(richDescription(this.carbonMessages.commandWhisperDescription())) .handler(ctx -> { final CarbonPlayer sender = ctx.sender().carbonPlayer(); if (sender.muted()) { this.carbonMessages.muteCannotSpeak(sender); return; } final SignedString message = ctx.get("message"); final CarbonPlayer recipient = ctx.get("player"); this.whisper.whisper(sender, recipient, message, ctx.parsingContext("player").consumedInput()); }) .build(); this.commandManager.command(command); } public static final class WhisperHandler { private final Logger logger; private final CarbonMessages messages; private final ConfigManager configManager; private final Provider messaging; private final PacketFactory packetFactory; private final UserManager userManager; private final CarbonServer server; private final CarbonEventHandler events; private final NetworkUsers network; private final Key rawChatKey; @Inject private WhisperHandler( final Logger logger, final CarbonMessages messages, final ConfigManager configManager, final Provider messaging, final PacketFactory packetFactory, final UserManager userManager, final CarbonServer server, final CarbonEventHandler events, final NetworkUsers network, @RawChat final Key rawChatKey ) { this.logger = logger; this.messages = messages; this.configManager = configManager; this.messaging = messaging; this.packetFactory = packetFactory; this.userManager = userManager; this.server = server; this.events = events; this.network = network; this.rawChatKey = rawChatKey; } public void whisper( final CarbonPlayer sender, final CarbonPlayer recipient, final SignedString message ) { this.whisper(sender, recipient, message, null); } public void whisper( final CarbonPlayer sender, final CarbonPlayer recipient, final SignedString message, final @Nullable String recipientInputString ) { if (sender.equals(recipient)) { this.messages.whisperSelfError(sender, sender.displayName()); return; } if (sender.ignoringDirectMessages() && !sender.hasPermission("carbon.togglemsg.exempt")) { this.messages.whisperIgnoringAll(sender); return; } if (!sender.hasPermission("carbon.whisper.send")) { this.messages.whisperNoPermissionSend(sender); return; } final String recipientUsername = recipient.username(); if (!this.network.online(recipient) || !sender.awareOf(recipient) && !sender.hasPermission("carbon.whisper.vanished")) { final var exception = new CarbonPlayerParser.ParseException( recipientInputString == null ? recipientUsername : recipientInputString, this.messages ); this.messages.errorCommandArgumentParsing(sender, CloudUtils.message(exception)); return; } final boolean localRecipient = recipient.online(); if (sender.ignoring(recipient)) { this.messages.whisperIgnoringTarget(sender, recipient.displayName()); return; } if (recipient.ignoring(sender)) { this.messages.whisperTargetIgnoring(sender, recipient.displayName()); return; } if (recipient.ignoringDirectMessages() && !sender.hasPermission("carbon.togglemsg.exempt")) { this.messages.whisperTargetIgnoringDMs(sender, recipient.displayName()); return; } final Component senderDisplayName = sender.displayName(); final Component recipientDisplayName = recipient.displayName(); final CarbonPrivateChatEvent privateChatEvent = new CarbonPrivateChatEventImpl(sender, recipient, Component.text(message.string())); this.events.emit(privateChatEvent); if (privateChatEvent.cancelled()) { this.messages.whisperError(sender, sender.displayName(), recipient.displayName()); return; } final String senderUsername = sender.username(); message.sendMessage( sender, ChatType.chatType(this.rawChatKey), this.messages.whisperSender(SourcedAudience.of(sender, sender), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, recipient.uuid(), privateChatEvent.message()) ); if (localRecipient) { if (!recipient.hasPermission("carbon.whisper.receive")) { this.messages.whisperNoPermissionReceive(sender); return; } message.sendMessage( recipient, ChatType.chatType(this.rawChatKey), this.messages.whisperRecipient(SourcedAudience.of(sender, recipient), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, recipient.uuid(), privateChatEvent.message()) ); } WhisperCommand.broadcastWhisperSpy(this.server, this.messages, senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, privateChatEvent.message()); this.messages.whisperConsoleLog(this.server.console(), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, privateChatEvent.message()); final @Nullable Sound messageSound = this.configManager.primaryConfig().messageSound(); if (localRecipient && messageSound != null && recipient.hasPermission("carbon.whisper.ping_sounds")) { recipient.playSound(messageSound, Sound.Emitter.self()); } sender.lastWhisperTarget(recipient.uuid()); sender.whisperReplyTarget(recipient.uuid()); if (localRecipient) { recipient.whisperReplyTarget(sender.uuid()); } else { this.messaging.get().queuePacket(() -> this.packetFactory.whisperPacket(sender.uuid(), recipient.uuid(), privateChatEvent.message())); } } public void handlePacket(final WhisperPacket packet) { final @Nullable CarbonPlayer recipient = this.server.players().stream() .filter(p -> p.uuid().equals(packet.to())) .findFirst() .orElse(null); if (recipient == null) { return; } this.userManager.user(packet.from()).thenAccept(sender -> { final String senderUsername = sender.username(); final Component senderDisplayName = sender.displayName(); final String recipientUsername = recipient.username(); final Component recipientDisplayName = recipient.displayName(); if (!recipient.hasPermission("carbon.whisper.receive")) { this.messages.whisperNoPermissionReceive(sender); return; } final CarbonPrivateChatEvent privateChatEvent = new CarbonPrivateChatEventImpl(sender, recipient, packet.message()); this.events.emit(privateChatEvent); if (privateChatEvent.cancelled()) { this.messages.whisperError(sender, sender.displayName(), recipient.displayName()); return; } recipient.whisperReplyTarget(sender.uuid()); SourcedAudience.of(sender, recipient).sendMessage( this.messages.whisperRecipient(SourcedAudience.of(sender, recipient), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, recipient.uuid(), privateChatEvent.message()) ); WhisperCommand.broadcastWhisperSpy(this.server, this.messages, senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, privateChatEvent.message()); this.messages.whisperConsoleLog(this.server.console(), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, privateChatEvent.message()); final @Nullable Sound messageSound = this.configManager.primaryConfig().messageSound(); if (messageSound != null && recipient.hasPermission("carbon.whisper.ping_sounds")) { recipient.playSound(messageSound, Sound.Emitter.self()); } }).exceptionally(ex -> { this.logger.warn("Failed to handle whisper packet {}", packet, ex); return null; }); } } public static void broadcastWhisperSpy( final CarbonServer server, final CarbonMessages messages, final String senderUsername, final Component senderDisplayName, final String recipientUsername, final Component recipientDisplayName, final Component message ) { for (final CarbonPlayer player : server.players()) { if (player.spying() && !player.username().equals(senderUsername) && !player.username().equals(recipientUsername)) { messages.whisperRecipientSpy(player, senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, message); } } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/exception/CommandCompleted.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.exception; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; 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 CommandCompleted extends ComponentException { private static final long serialVersionUID = 1352215898395889299L; private CommandCompleted(final @Nullable Component message) { super(message); } public static CommandCompleted withoutMessage() { return new CommandCompleted(null); } public static CommandCompleted withMessage(final ComponentLike message) { return new CommandCompleted(message.asComponent()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/command/exception/ComponentException.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.exception; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.util.ComponentMessageThrowable; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class ComponentException extends RuntimeException implements ComponentMessageThrowable { private static final long serialVersionUID = 132203031250316968L; private final @Nullable Component message; protected ComponentException(final @Nullable Component message) { this.message = message; } public static ComponentException withoutMessage() { return new ComponentException(null); } public static ComponentException withMessage(final ComponentLike message) { return new ComponentException(message.asComponent()); } @Override public @Nullable Component componentMessage() { return this.message; } @Override public String getMessage() { return PlainTextComponentSerializer.plainText().serializeOr(this.message, "No message."); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/ClearChatSettings.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; 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.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @ConfigSerializable @DefaultQualifier(NonNull.class) // TODO: Config versioning. This isn't automatically added to existing configs otherwise. public class ClearChatSettings { @Comment("The message that will be sent to each player.") private String message = ""; @Comment("The number of times the message will be sent to each player.") private int iterations = 50; @Comment("The message to be sent after chat is cleared.") private String broadcast = "Chat has been cleared by ."; private @MonotonicNonNull Component messageComponent = null; public Component message() { if (this.messageComponent == null) { this.messageComponent = MiniMessage.miniMessage().deserialize(this.message); } return this.messageComponent; } public int iterations() { return this.iterations; } public Component broadcast(final Component displayName, final String username) { return MiniMessage.miniMessage().deserialize(this.broadcast, TagResolver.builder() .tag("display_name", Tag.selfClosingInserting(displayName)) .tag("username", Tag.selfClosingInserting(Component.text(username))) .build()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/CommandConfig.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; import java.util.Map; import net.draycia.carbon.common.command.CommandSettings; import net.draycia.carbon.common.util.CloudUtils; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @ConfigSerializable @DefaultQualifier(MonotonicNonNull.class) public class CommandConfig { private Map settings = CloudUtils.defaultCommandSettings(); public CommandConfig() { } public CommandConfig(final Map settings) { this.settings = settings; } public Map settings() { return this.settings; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/ConfigHeader.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Tells Carbon's config loading logic to use {@link #value()} * as the header in {@link org.spongepowered.configurate.ConfigurationOptions} * when loading/saving this type as the root node of a document. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ConfigHeader { String value(); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/ConfigManager.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; import com.google.inject.Inject; import com.google.inject.Singleton; import io.leangen.geantyref.GenericTypeReflector; import java.io.IOException; import java.lang.reflect.Type; import java.nio.file.Path; import java.util.Locale; import java.util.Map; import java.util.Set; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.common.DataDirectory; import net.draycia.carbon.common.command.CommandSettings; import net.draycia.carbon.common.event.events.CarbonReloadEvent; import net.draycia.carbon.common.integration.Integration; import net.draycia.carbon.common.serialisation.gson.LocaleSerializerConfigurate; import net.draycia.carbon.common.util.FileUtil; import net.kyori.adventure.key.Key; import net.kyori.adventure.serializer.configurate4.ConfigurateComponentSerializer; 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; import org.spongepowered.configurate.CommentedConfigurationNode; import org.spongepowered.configurate.CommentedConfigurationNodeIntermediary; import org.spongepowered.configurate.ConfigurateException; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.hocon.HoconConfigurationLoader; import org.spongepowered.configurate.loader.ConfigurationLoader; import org.spongepowered.configurate.objectmapping.ObjectMapper; import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Processor; import org.spongepowered.configurate.transformation.ConfigurationTransformation; @DefaultQualifier(NonNull.class) @Singleton public final class ConfigManager { public static final String CONFIG_VERSION_KEY = "config-version"; private static final String PRIMARY_CONFIG_FILE_NAME = "config.conf"; private static final String COMMAND_SETTINGS_FILE_NAME = "command-settings.conf"; private final Path dataDirectory; private final LocaleSerializerConfigurate locale; private final Logger logger; private final Set integrations; private volatile @MonotonicNonNull PrimaryConfig primaryConfig = null; @Inject private ConfigManager( final CarbonEventHandler events, @DataDirectory final Path dataDirectory, final LocaleSerializerConfigurate locale, final Logger logger, final Set integrations ) { this.dataDirectory = dataDirectory; this.locale = locale; this.logger = logger; this.integrations = integrations; events.subscribe(CarbonReloadEvent.class, -100, true, event -> this.reloadPrimaryConfig()); } public static @Nullable String extractHeader(final Type type) { if (type instanceof Class cls) { final @Nullable ConfigHeader h = cls.getAnnotation(ConfigHeader.class); if (h == null) { return null; } return h.value(); } else { return extractHeader(GenericTypeReflector.erase(type)); } } public void reloadPrimaryConfig() { this.logger.info("Reloading configuration...."); final @Nullable PrimaryConfig load = this.load(PrimaryConfig.class, PRIMARY_CONFIG_FILE_NAME); if (load != null) { this.primaryConfig = load; } else { this.logger.error("Failed to reload primary config, see above for further details"); } } public PrimaryConfig primaryConfig() { if (this.primaryConfig == null) { synchronized (this) { if (this.primaryConfig == null) { this.logger.info("Loading configuration...."); final @Nullable PrimaryConfig load = this.load(PrimaryConfig.class, PRIMARY_CONFIG_FILE_NAME); if (load == null) { throw new RuntimeException("Failed to initialize primary config, see above for further details"); } this.primaryConfig = load; } } } return this.primaryConfig; } public Map loadCommandSettings() { final @Nullable CommandConfig load = this.load(CommandConfig.class, COMMAND_SETTINGS_FILE_NAME); if (load == null) { throw new RuntimeException("Failed to initialize command settings, see above for further details"); } return load.settings(); } public ConfigurationLoader configurationLoader(final Path file, final @Nullable String header) { return HoconConfigurationLoader.builder() .prettyPrinting(true) .defaultOptions(opts -> { final ConfigurateComponentSerializer serializer = ConfigurateComponentSerializer.configurate(); return opts.shouldCopyDefaults(true) .header(header) .serializers(serializerBuilder -> serializerBuilder.registerAll(serializer.serializers()) .register(Locale.class, this.locale) .register(IntegrationConfigContainer.class, new IntegrationConfigContainer.Serializer(this.integrations)) .registerAnnotatedObjects(ObjectMapper.factoryBuilder() .addProcessor(Comment.class, overrideComments()) .build())); }) .path(file) .build(); } private static Processor.Factory overrideComments() { return (data, fieldType) -> (value, destination) -> { if (destination instanceof final CommentedConfigurationNodeIntermediary commented) { commented.comment(data.value()); } }; } public @Nullable T load(final Class clazz, final String fileName) { final Path file = this.dataDirectory.resolve(fileName); try { FileUtil.mkParentDirs(file); } catch (final IOException ex) { this.logger.error("Failed to create parent directories for '{}'", file, ex); return null; } final var loader = this.configurationLoader(file, extractHeader(clazz)); try { final var node = loader.load(); try { clazz.getDeclaredMethod("upgrade", ConfigurationNode.class).invoke(null, node); } catch (final NoSuchMethodException ignore) { } final @Nullable T config = node.get(clazz); if (config == null) { throw new ConfigurateException(node, "Failed to deserialize " + clazz.getName() + " from node"); } node.set(clazz, config); loader.save(node); return config; } catch (final ConfigurateException | ReflectiveOperationException exception) { this.logger.error("Failed to load config '{}'", file, exception); return null; } } public static void configVersionComment( final N rootNode, final ConfigurationTransformation.Versioned versionedTransformation ) { final ConfigurationNode versionNode = rootNode.node(versionedTransformation.versionKey()); if (!versionNode.virtual() && versionNode instanceof CommentedConfigurationNode commented) { commented.comment("Used internally to track changes to the config. Do not edit manually!"); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/DatabaseSettings.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; import java.util.concurrent.TimeUnit; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @DefaultQualifier(Nullable.class) @ConfigSerializable public class DatabaseSettings { public DatabaseSettings() { } public DatabaseSettings(final String url, final String username, final String password) { this.url = url; this.username = username; this.password = password; } @Comment(""" JDBC URL. Suggested defaults for each DB: MySQL: jdbc:mysql://host:3306/DB MariaDB: jdbc:mariadb://host:3306/DB PostgreSQL: jdbc:postgresql://host:5432/database""") private String url = "jdbc:mysql://localhost:3306/carbon"; @Comment("The connection username.") private String username = "username"; @Comment("The connection password.") private String password = "password"; @Comment("Settings for the connection pool. This is an advanced configuration that most users won't need to touch.") private ConnectionPool connectionPool = new ConnectionPool(); public String url() { return this.url; } public String url(final String url) { return this.url = url; } public String username() { return this.username; } public String password() { return this.password; } public ConnectionPool connectionPool() { return this.connectionPool; } @ConfigSerializable public static class ConnectionPool { public int maximumPoolSize = 8; public int minimumIdle = 8; public long maximumLifetime = TimeUnit.MINUTES.toMillis(30); public long keepaliveTime = 0L; public long connectionTimeout = TimeUnit.SECONDS.toMillis(30); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/IntegrationConfigContainer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; import java.lang.reflect.Type; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import net.draycia.carbon.common.integration.Integration; 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; import org.spongepowered.configurate.BasicConfigurationNode; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationOptions; import org.spongepowered.configurate.serialize.SerializationException; import org.spongepowered.configurate.serialize.TypeSerializer; @DefaultQualifier(NonNull.class) public final class IntegrationConfigContainer { private final Map map = new HashMap<>(); @SuppressWarnings("unchecked") public C config(final Integration.ConfigMeta meta) { return (C) Objects.requireNonNull(this.map.get(meta.name())); } public static final class Serializer implements TypeSerializer { private final List sections; public Serializer(final Set integrations) { this.sections = integrations.stream() .sorted(Comparator.comparing(Integration.ConfigMeta::name)) .toList(); } @Override public IntegrationConfigContainer deserialize(final Type type, final ConfigurationNode node) throws SerializationException { final IntegrationConfigContainer container = new IntegrationConfigContainer(); for (final Integration.ConfigMeta section : this.sections) { final @Nullable Object value = node.node(section.name()).get(section.type()); Objects.requireNonNull(value); container.map.put(section.name(), value); } return container; } @Override public void serialize(final Type type, final @Nullable IntegrationConfigContainer obj, final ConfigurationNode node) throws SerializationException { Objects.requireNonNull(obj); for (final Object key : node.childrenMap().keySet()) { node.removeChild(key); } for (final Integration.ConfigMeta section : this.sections) { node.node(section.name()).set(section.type(), obj.map.get(section.name())); } } @Override public IntegrationConfigContainer emptyValue(final Type specificType, final ConfigurationOptions options) { final IntegrationConfigContainer container = new IntegrationConfigContainer(); for (final Integration.ConfigMeta section : this.sections) { final @Nullable Object value; try { value = options.serializers().get(section.type()) .deserialize(section.type(), BasicConfigurationNode.root()); } catch (final Exception e) { throw Exceptions.rethrow(e); } Objects.requireNonNull(value); container.map.put(section.name(), value); } return container; } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/MessagingSettings.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; import net.draycia.carbon.common.messaging.MessagingManager; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @DefaultQualifier(MonotonicNonNull.class) @ConfigSerializable public class MessagingSettings { @Comment("Whether cross-server messaging is enabled") private boolean enabled = false; @Comment("One of: RABBITMQ, NATS, REDIS") private MessagingManager.@NonNull BrokerType brokerType = MessagingManager.BrokerType.NONE; private String url = "127.0.0.1"; private int port = 5672; // RabbitMQ 5672, NATS 4222, Redis 6379 @Comment("RabbitMQ VHost") private String vhost = "/"; // RabbitMQ only @Comment("NATS credentials file") private String credentialsFile = ""; // NATS only @Comment("RabbitMQ username") private String username = "username"; // RabbitMQ only @Comment("RabbitMQ and Redis password") private String password = "password"; // RabbitMQ and Redis only public boolean enabled() { return this.enabled; } public MessagingManager.@NonNull BrokerType brokerType() { return this.brokerType; } public String url() { return this.url; } public int port() { return this.port; } public String vhost() { return this.vhost; } public String credentialsFile() { return this.credentialsFile; } public String username() { return this.username; } public String password() { return this.password; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/PingSettings.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @ConfigSerializable public class PingSettings { @Comment("The color your name will be when another player mentions you.") private TextColor highlightTextColor = NamedTextColor.YELLOW; private String prefix = "@"; private boolean playSound = false; private Key name = Key.key("block.anvil.use"); private Sound.Source source = Sound.Source.MASTER; private float volume = 1.0f; // 0.0 -> infinity private float pitch = 1.0f; // 0.0 -> 2.0 public TextColor highlightTextColor() { return this.highlightTextColor; } public boolean playSound() { return this.playSound; } public Key name() { return this.name; } public String prefix() { return this.prefix; } public Sound.Source source() { return this.source; } public float volume() { return this.volume; } public float pitch() { return this.pitch; } public Sound sound() { return Sound.sound(this.name, this.source, this.volume, this.pitch); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/config/PrimaryConfig.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config; import io.github.miniplaceholders.api.MiniPlaceholders; import java.util.List; import java.util.Locale; import java.util.Map; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil; import net.draycia.carbon.common.util.Exceptions; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.ConfigurateException; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.NodePath; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.transformation.ConfigurationTransformation; @ConfigSerializable @DefaultQualifier(NonNull.class) public class PrimaryConfig { @Comment("The default locale for plugin messages.") private Locale defaultLocale = Locale.US; @Comment(""" The default channel that new players will be in when they join. If the channel is not found or the player cannot use the channel, they will speak in basic non-channel chat.""") private Key defaultChannel = Key.key("carbon", "global"); @Comment("Returns you to the default channel when you use a channel's command while you have that channel active.") private boolean returnToDefaultChannel = false; @Comment(""" The service that will be used to store and load player information. One of: JSON, H2, MYSQL, PSQL Note: If you choose MYSQL or PSQL make sure you configure the "database-settings" section of this file!""") private StorageType storageType = StorageType.JSON; @Comment(""" When "storage-type" is set to MYSQL or PSQL, this section configures the database connection. If JSON or H2 storage is used, this section can be ignored.""") private DatabaseSettings databaseSettings = new DatabaseSettings(); @Comment("Settings for cross-server messaging") private MessagingSettings messagingSettings = new MessagingSettings(); private NicknameSettings nicknameSettings = new NicknameSettings(); @Comment(""" Plugin-wide custom placeholders. These will be parsed in all messages rendered and sent by Carbon. This includes chat, command feedback, and others. Make sure to close your tags so they do not bleed into other formats. Only a single pass is done so custom placeholders will not work within each other.""") private Map customPlaceholders = Map.of(); @Comment("The suggestions shown when using the TAB key in chat.") private List customChatSuggestions = List.of(); @Comment("The placeholders replaced in chat messages, this WILL work with chat previews.") private Map chatPlaceholders = Map.of(); @Comment("Basic regex based chat filter.") private Map chatFilter = Map.of(); @Comment("Player toggled chat filter. Useful for more mild profanity.") private Map optionalChatFilter = Map.of(); @Comment("Various settings related to pinging players in channels.") private PingSettings pingSettings = new PingSettings(); private PartySettings partyChat = new PartySettings(); @Comment("Sound for receiving a direct message") // TODO migrate to a field name that makes more sense private @Nullable Sound messageSound = Sound.sound( Key.key("entity.experience_orb.pickup"), Sound.Source.MASTER, 1.0F, 1.0F ); @Comment("Settings for the clear chat command") private ClearChatSettings clearChatSettings = new ClearChatSettings(); @Comment("Disables spying when the user doesn't have spy permissions") private boolean spyPermissionRequired = true; @Comment("Alerts the user when they can no longer spy due to lacking permissions") private boolean spyDisabledMessage = false; @Comment("Settings for integrations with other plugins/mods. Settings only apply when the relevant plugin/mod is present.") private IntegrationConfigContainer integrations; @Comment("Whether Carbon should check for updates using the GitHub API on startup.") private boolean updateChecker = true; public NicknameSettings nickname() { return this.nicknameSettings; } public Locale defaultLocale() { return this.defaultLocale; } public Key defaultChannel() { return this.defaultChannel; } public boolean returnToDefaultChannel() { return this.returnToDefaultChannel; } public StorageType storageType() { return this.storageType; } public DatabaseSettings databaseSettings() { return this.databaseSettings; } public MessagingSettings messagingSettings() { return this.messagingSettings; } private String applyPlaceholders(String message, final Map placeholders) { for (final var entry : placeholders.entrySet()) { message = message.replace("<" + entry.getKey() + ">", entry.getValue()); } return message; } public String applyCustomPlaceholders(final String message) { return this.applyPlaceholders(message, this.customPlaceholders); } public @Nullable List customChatSuggestions() { return this.customChatSuggestions; } // Maybe we only need the two chat filters? Having 4 placeholder systems seems excessive. public String applyChatPlaceholders(final String message) { return this.applyPlaceholders(message, this.chatPlaceholders); } private Component applyFilters(Component message, final Map filters) { final TagResolver.Builder resolver = TagResolver.builder(); if (MiniPlaceholdersUtil.miniPlaceholdersLoaded()) { resolver.resolver(MiniPlaceholders.globalPlaceholders()); } for (final Map.Entry entry : filters.entrySet()) { message = message.replaceText(TextReplacementConfig.builder() .match(entry.getKey()).replacement(MiniMessage.miniMessage().deserialize(entry.getValue(), resolver.build())).build()); } return message; } public Component applyChatFilters(final Component message) { return this.applyFilters(message, this.chatFilter); } public Component applyOptionalChatFilters(final Component message) { return this.applyFilters(message, this.optionalChatFilter); } public PingSettings pings() { return this.pingSettings; } public PartySettings partyChat() { return this.partyChat; } public @Nullable Sound messageSound() { return this.messageSound; } public ClearChatSettings clearChatSettings() { return this.clearChatSettings; } public boolean spyPermissionRequired() { return this.spyPermissionRequired; } public boolean spyDisabledMessage() { return this.spyDisabledMessage; } public IntegrationConfigContainer integrations() { return this.integrations; } public boolean updateChecker() { return this.updateChecker; } @SuppressWarnings("unused") public static void upgrade(final ConfigurationNode node) { final ConfigurationTransformation.VersionedBuilder builder = ConfigurationTransformation.versionedBuilder() .versionKey(ConfigManager.CONFIG_VERSION_KEY); final ConfigurationTransformation initial = ConfigurationTransformation.builder() .addAction(NodePath.path("use-carbon-nicknames"), (path, value) -> new Object[]{"nickname-settings", "use-carbon-nicknames"}) .build(); builder.addVersion(0, initial); final ConfigurationTransformation one = ConfigurationTransformation.builder() .addAction(NodePath.path("party-chat"), (path, value) -> new Object[]{"party-chat", "enabled"}) .build(); builder.addVersion(1, one); final ConfigurationTransformation.Versioned upgrader = builder.build(); final int from = upgrader.version(node); try { upgrader.apply(node); } catch (final ConfigurateException e) { Exceptions.rethrow(e); } ConfigManager.configVersionComment(node, upgrader); } @ConfigSerializable public static final class NicknameSettings { @Comment("Whether Carbon's nickname management should be used. Disable this if you wish to have another plugin manage nicknames.") private boolean useCarbonNicknames = true; @Comment("Paper only. Updates the player's display name in the tab list to match their nickname.") private boolean updateTabList = true; @Comment("Minimum number of characters in nickname (excluding formatting).") private int minLength = 3; @Comment("Maximum number of characters in nickname (excluding formatting).") private int maxLength = 16; private List blackList = List.of("notch", "admin"); @Comment("Regex pattern nicknames must match in order to be applied, can be bypassed with the permission 'carbon.nickname.filter'.") private String filter = "^[a-zA-Z0-9_]*$"; @Comment("Format used when displaying nicknames.") public String format = "@'>~"; @Comment("Whether to skip applying 'format' when a nickname matches a players username, only differing in decoration.") public boolean skipFormatWhenNameMatches = true; public boolean useCarbonNicknames() { return this.useCarbonNicknames; } public boolean updateTabList() { return this.updateTabList; } public List blackList() { return this.blackList; } public String filter() { return this.filter; } public int minLength() { return this.minLength; } public int maxLength() { return this.maxLength; } } @ConfigSerializable public static final class PartySettings { @Comment("Whether party chat is enabled") public boolean enabled = true; public int expireInvitesAfterSeconds = 45; public boolean playSound = false; @Comment("Sound for receiving a party message") public @Nullable Sound messageSound = Sound.sound( Key.key("entity.experience_orb.pickup"), Sound.Source.MASTER, 1.0F, 1.0F ); } public enum StorageType { JSON, MYSQL, PSQL, H2 } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/CancellableImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event; import net.draycia.carbon.api.event.Cancellable; public class CancellableImpl implements Cancellable, com.sasorio.event.Cancellable { private boolean cancelled = false; @Override public boolean cancelled() { return this.cancelled; } @Override public void cancelled(final boolean cancelled) { this.cancelled = cancelled; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/CarbonEventHandlerImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event; import com.google.inject.Inject; import com.google.inject.Singleton; import com.sasorio.event.EventConfig; import com.sasorio.event.EventSubscriber; import com.sasorio.event.EventSubscription; import com.sasorio.event.bus.EventBus; import com.sasorio.event.bus.SimpleEventBus; import com.sasorio.event.registry.EventRegistry; import com.sasorio.event.registry.SimpleEventRegistry; import net.draycia.carbon.api.event.Cancellable; import net.draycia.carbon.api.event.CarbonEvent; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.CarbonEventSubscriber; import net.draycia.carbon.api.event.CarbonEventSubscription; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * Event handler for listening to and emitting carbon events. * * @since 1.0.0 */ @DefaultQualifier(NonNull.class) @Singleton public final class CarbonEventHandlerImpl implements CarbonEventHandler { private final Logger logger; @Inject private CarbonEventHandlerImpl(final Logger logger) { this.logger = logger; } private final EventRegistry eventRegistry = new SimpleEventRegistry<>(CarbonEvent.class); private final EventBus eventBus = new SimpleEventBus<>(this.eventRegistry, this::onException); private void onException(final EventBus eventBus, final EventSubscription subscription, final E event, final Throwable throwable) { final Object subscriber = subscription.subscriber() instanceof SubscriberWrapper wrapped ? wrapped.carbon : subscription.subscriber(); this.logger.warn("Exception posting event '{}' to subscriber '{}'", event, subscriber, throwable); } @Override public CarbonEventSubscription subscribe( final Class eventClass, final CarbonEventSubscriber subscriber ) { return new CarbonEventSubscriptionImpl<>( eventClass, subscriber, this.eventRegistry.subscribe(eventClass, new SubscriberWrapper<>(subscriber, true)) ); } // TODO: support EventConfig#exact() @Override public CarbonEventSubscription subscribe( final Class eventClass, final int order, final boolean acceptsCancelled, final CarbonEventSubscriber subscriber ) { final EventConfig eventConfig = EventConfig.defaults().order(order).acceptsCancelled(acceptsCancelled); return new CarbonEventSubscriptionImpl<>( eventClass, subscriber, this.eventRegistry.subscribe(eventClass, eventConfig, new SubscriberWrapper<>(subscriber, acceptsCancelled)) ); } @Override public void emit(final T event) { this.eventBus.post(event); } private record SubscriberWrapper( CarbonEventSubscriber carbon, boolean acceptsCancelled ) implements EventSubscriber { @Override public void on(final T event) throws Throwable { // Our events implement seiama Cancellable; but API consumers won't be able to do that if (!this.acceptsCancelled && event instanceof Cancellable cancellable && cancellable.cancelled()) { return; } this.carbon.on(event); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/CarbonEventSubscriptionImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event; import com.sasorio.event.EventSubscription; import net.draycia.carbon.api.event.CarbonEvent; import net.draycia.carbon.api.event.CarbonEventSubscriber; import net.draycia.carbon.api.event.CarbonEventSubscription; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) record CarbonEventSubscriptionImpl( Class event, CarbonEventSubscriber subscriber, EventSubscription backingSubscription ) implements CarbonEventSubscription { @Override public void dispose() { this.backingSubscription.dispose(); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/events/CarbonChatEventImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event.events; import java.util.List; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.KeyedRenderer; import net.draycia.carbon.common.event.CancellableImpl; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.chat.SignedMessage; import net.kyori.adventure.text.Component; 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; /** * Event that's called when chat components are rendered for online players. * * @since 2.0.0 */ @DefaultQualifier(NonNull.class) public class CarbonChatEventImpl extends CancellableImpl implements CarbonChatEvent { private final List renderers; private final CarbonPlayer sender; private final Component originalMessage; private final List recipients; private final @MonotonicNonNull ChatChannel chatChannel; private final @MonotonicNonNull SignedMessage signedMessage; public final boolean origin; private Component message; public CarbonChatEventImpl( final CarbonPlayer sender, final Component originalMessage, final List recipients, final List renderers, final @Nullable ChatChannel chatChannel, final @Nullable SignedMessage signedMessage ) { this(sender, originalMessage, recipients, renderers, chatChannel, signedMessage, true); } public CarbonChatEventImpl( final CarbonPlayer sender, final Component originalMessage, final List recipients, final List renderers, final @Nullable ChatChannel chatChannel, final @Nullable SignedMessage signedMessage, final boolean origin ) { this.sender = sender; this.originalMessage = originalMessage; this.message = originalMessage; this.recipients = recipients; this.renderers = renderers; this.chatChannel = chatChannel; this.signedMessage = signedMessage; this.origin = origin; } @Override public List renderers() { return this.renderers; } @Override public @MonotonicNonNull SignedMessage signedMessage() { return this.signedMessage; } @Override public CarbonPlayer sender() { return this.sender; } @Override public Component originalMessage() { return this.originalMessage; } @Override public Component message() { return this.message; } @Override public void message(final Component message) { this.message = message; } @Override public @MonotonicNonNull ChatChannel chatChannel() { return this.chatChannel; } @Override public List recipients() { return this.recipients; } public Component renderFor(final Audience viewer) { Component renderedMessage = this.message(); for (final var renderer : this.renderers()) { renderedMessage = renderer.render(this.sender, viewer, renderedMessage, this.message()); } return renderedMessage; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/events/CarbonEarlyChatEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event.events; import net.draycia.carbon.api.event.Cancellable; import net.draycia.carbon.api.event.CarbonEvent; import net.draycia.carbon.api.users.CarbonPlayer; public class CarbonEarlyChatEvent implements CarbonEvent, Cancellable { private final CarbonPlayer sender; private String message; private boolean cancelled = false; public CarbonEarlyChatEvent(final CarbonPlayer sender, final String message) { this.sender = sender; this.message = message; } public CarbonPlayer sender() { return this.sender; } public String message() { return this.message; } public void message(final String message) { this.message = message; } @Override public boolean cancelled() { return this.cancelled; } @Override public void cancelled(final boolean cancelled) { this.cancelled = cancelled; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/events/CarbonPrivateChatEventImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event.events; import java.util.Objects; import net.draycia.carbon.api.event.events.CarbonPrivateChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.event.CancellableImpl; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * Called whenever a player privately messages another player. * * @since 3.0.0 */ @DefaultQualifier(NonNull.class) public class CarbonPrivateChatEventImpl extends CancellableImpl implements CarbonPrivateChatEvent { private final CarbonPlayer sender; private final CarbonPlayer recipient; private Component message; public CarbonPrivateChatEventImpl(final CarbonPlayer sender, final CarbonPlayer recipient, final Component message) { this.sender = sender; this.recipient = recipient; this.message = message; } public void message(final Component message) { this.message = Objects.requireNonNull(message); } public Component message() { return this.message; } public CarbonPlayer sender() { return this.sender; } public CarbonPlayer recipient() { return this.recipient; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/events/CarbonReloadEvent.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event.events; import net.draycia.carbon.api.event.CarbonEvent; public class CarbonReloadEvent implements CarbonEvent { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/events/ChannelRegisterEventImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event.events; import java.util.Set; import net.draycia.carbon.api.event.events.CarbonChannelRegisterEvent; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public record ChannelRegisterEventImpl( CarbonChannelRegistry channelRegistry, Set registered ) implements CarbonChannelRegisterEvent { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/event/events/ChannelSwitchEventImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.event.events; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.event.events.ChannelSwitchEvent; import net.draycia.carbon.api.users.CarbonPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class ChannelSwitchEventImpl implements ChannelSwitchEvent { private final CarbonPlayer player; private ChatChannel chatChannel; public ChannelSwitchEventImpl(final CarbonPlayer player, final ChatChannel chatChannel) { this.player = player; this.chatChannel = chatChannel; } @Override public CarbonPlayer player() { return this.player; } @Override public ChatChannel channel() { return this.chatChannel; } @Override public void channel(final ChatChannel chatChannel) { this.chatChannel = chatChannel; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/integration/Integration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.integration; import java.lang.reflect.Type; import net.draycia.carbon.common.config.ConfigManager; public interface Integration { boolean eligible(); void register(); interface ConfigMeta { Type type(); String name(); record ConfigMetaRecord(Type type, String name) implements ConfigMeta {} } static ConfigMeta configMeta(final String name, final Type type) { return new ConfigMeta.ConfigMetaRecord(type, name); } default C config(final ConfigManager configManager, final ConfigMeta meta) { return configManager.primaryConfig().integrations().config(meta); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/integration/miniplaceholders/MiniPlaceholdersExpansion.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.integration.miniplaceholders; import com.google.inject.Inject; import io.github.miniplaceholders.api.Expansion; import java.util.UUID; 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.users.UserManager; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; 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 MiniPlaceholdersExpansion { private final UserManager userManager; private final ChannelRegistry channels; @Inject private MiniPlaceholdersExpansion( final UserManager userManager, final ChannelRegistry channels ) { this.userManager = userManager; this.channels = channels; } public void registerExpansion() { final Expansion expansion = Expansion.builder("carbonchat") .audiencePlaceholder("party", (audience, queue, ctx) -> { if (!hasId(audience)) { return null; } return Tag.selfClosingInserting(this.partyName(id(audience))); }) .audiencePlaceholder("nickname", (audience, queue, ctx) -> { if (!hasId(audience)) { return null; } if (queue.hasNext() && queue.pop().lowerValue().equals("plain")) { return Tag.selfClosingInserting(this.toPlain(this.nickname(id(audience)))); } return Tag.selfClosingInserting(this.nickname(id(audience))); }) .audiencePlaceholder("displayname", (audience, queue, ctx) -> { if (!hasId(audience)) { return null; } if (queue.hasNext() && queue.pop().lowerValue().equals("plain")) { return Tag.selfClosingInserting(this.toPlain(this.displayName(id(audience)))); } return Tag.selfClosingInserting(this.displayName(id(audience))); }) .audiencePlaceholder("channel_key", (audience, queue, ctx) -> { if (!hasId(audience)) { return null; } return Tag.preProcessParsed(this.selectedChannelKey(id(audience))); }) .build(); expansion.register(); } private static boolean hasId(final Audience audience) { return audience.get(Identity.UUID).isPresent(); } private static UUID id(final Audience audience) { return audience.get(Identity.UUID).orElseThrow(); } private Component partyName(final UUID id) { final @Nullable Party party = this.userManager.user(id).thenCompose(CarbonPlayer::party).join(); return party == null ? Component.empty() : party.name(); } private Component displayName(final UUID id) { final CarbonPlayer carbonPlayer = this.userManager.user(id).join(); return carbonPlayer.displayName(); } private Component nickname(final UUID id) { final CarbonPlayer carbonPlayer = this.userManager.user(id).join(); final @Nullable Component nickname = carbonPlayer.nickname(); return nickname == null ? Component.text(carbonPlayer.username()) : nickname; } private String selectedChannelKey(final UUID id) { final CarbonPlayer carbonPlayer = this.userManager.user(id).join(); final @Nullable ChatChannel selected = carbonPlayer.selectedChannel(); if (selected != null) { return selected.key().asString(); } return this.channels.defaultKey().asString(); } private Component toPlain(final Component input) { return Component.text(PlainTextComponentSerializer.plainText().serialize(input)); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/integration/miniplaceholders/MiniPlaceholdersIntegration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.integration.miniplaceholders; import com.google.inject.Inject; import com.google.inject.Provider; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @DefaultQualifier(NonNull.class) public final class MiniPlaceholdersIntegration implements Integration { private final Provider expansionProvider; private final Config config; @Inject private MiniPlaceholdersIntegration( final ConfigManager configManager, final Provider expansionProvider ) { this.expansionProvider = expansionProvider; this.config = this.config(configManager, configMeta()); } @Override public boolean eligible() { return MiniPlaceholdersUtil.miniPlaceholdersLoaded(); } @Override public void register() { this.expansionProvider.get().registerExpansion(); } public static ConfigMeta configMeta() { return Integration.configMeta("miniplaceholders", MiniPlaceholdersIntegration.Config.class); } @ConfigSerializable public static final class Config { @Comment("Enabling relational placeholders may require reworking your format configs due to the way MiniPlaceholders v3 works.") public boolean relationalPlaceholders = false; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/integration/miniplaceholders/MiniPlaceholdersUtil.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.integration.miniplaceholders; import io.github.miniplaceholders.api.MiniPlaceholders; import io.github.miniplaceholders.api.types.RelationalAudience; import java.util.Objects; import net.kyori.adventure.audience.Audience; import org.checkerframework.checker.nullness.qual.Nullable; public final class MiniPlaceholdersUtil { private static byte miniPlaceholdersLoaded = -1; private MiniPlaceholdersUtil() { } public static boolean miniPlaceholdersLoaded() { if (miniPlaceholdersLoaded == -1) { try { final String name = MiniPlaceholders.class.getName(); Objects.requireNonNull(name); miniPlaceholdersLoaded = 1; } catch (final NoClassDefFoundError error) { miniPlaceholdersLoaded = 0; } } return miniPlaceholdersLoaded == 1; } public static Audience wrapAudiences(final MiniPlaceholdersIntegration.@Nullable Config config, final @Nullable Audience recipient, final Audience sender) { if (!miniPlaceholdersLoaded() || config == null || !config.relationalPlaceholders) { return sender; } return wrapAudiences_(recipient, sender); } private static Audience wrapAudiences_(final @Nullable Audience recipient, final Audience sender) { if (recipient == null) { return sender; } return RelationalAudience.from(recipient, sender); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/ChatListenerInternal.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import net.draycia.carbon.api.channels.ChannelPermissionResult; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.KeyedRenderer; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.event.events.CarbonChatEventImpl; import net.draycia.carbon.common.event.events.CarbonEarlyChatEvent; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.messages.TagPermissions; import net.draycia.carbon.common.users.WrappedCarbonPlayer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.chat.SignedMessage; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentIteratorType; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public abstract class ChatListenerInternal { private final ConfigManager configManager; private final CarbonMessages carbonMessages; private final CarbonEventHandler carbonEventHandler; protected ChatListenerInternal( final CarbonEventHandler carbonEventHandler, final CarbonMessages carbonMessages, final ConfigManager configManager ) { this.configManager = configManager; this.carbonMessages = carbonMessages; this.carbonEventHandler = carbonEventHandler; } protected @Nullable CarbonChatEventImpl prepareAndEmitChatEvent(final CarbonPlayer sender, final Component originalMessage, final @Nullable SignedMessage signedMessage) { final CarbonPlayer.ChannelMessage channelMessage = sender.channelForMessage(originalMessage); final ChatChannel channel = channelMessage.channel(); return this.prepareAndEmitChatEvent(sender, channelMessage.message(), signedMessage, channel); } protected @Nullable CarbonChatEventImpl prepareAndEmitChatEvent(final CarbonPlayer sender, final Component message, final @Nullable SignedMessage signedMessage, final ChatChannel channel) { if (!sender.hasPermission("carbon.cooldown.exempt") && channel.cooldown() > 0) { final long currentMillis = System.currentTimeMillis(); final long expiresAt = channel.playerCooldown(sender); if (currentMillis < expiresAt) { // Round up, or the player can be told they have 0 seconds remaining final long remaining = (long) Math.ceil((double) (expiresAt - currentMillis) / 1000); this.carbonMessages.channelCooldown(sender, remaining); return null; } channel.startCooldown(sender); } if (sender.leftChannels().contains(channel.key())) { sender.joinChannel(channel); this.carbonMessages.channelJoined(sender); } final List renderers = new ArrayList<>(); renderers.add(KeyedRenderer.keyedRenderer(Key.key("carbon", "default"), channel)); final List recipients = channel.recipients(sender); final var chatEvent = new CarbonChatEventImpl(sender, message, recipients, renderers, channel, signedMessage); this.carbonEventHandler.emit(chatEvent); return chatEvent; } protected @Nullable CarbonEarlyChatEvent prepareAndEmitPreChatEvent(final CarbonPlayer sender, final Component originalMessage) { final CarbonPlayer.ChannelMessage channelMessage = sender.channelForMessage(originalMessage); final ChatChannel channel = channelMessage.channel(); final ChannelPermissionResult permitted = channel.permissions().speechPermitted(sender); if (!permitted.permitted()) { sender.sendMessage(permitted.reason()); return null; } final String plainContent = PlainTextComponentSerializer.plainText().serialize(channelMessage.message()); final String content = this.configManager.primaryConfig().applyChatPlaceholders(plainContent); final CarbonEarlyChatEvent earlyChatEvent = new CarbonEarlyChatEvent(sender, content); this.carbonEventHandler.emit(earlyChatEvent); return earlyChatEvent; } protected @Nullable Component parseTags(final CarbonPlayer sender, final String format) { final Component message; if (sender instanceof WrappedCarbonPlayer wrapped) { message = wrapped.parseMessageTags(format); } else { message = TagPermissions.parseTags(sender, TagPermissions.MESSAGE, format, sender::hasPermission); } if (probablyBlank(message)) { return null; } return message; } private static boolean probablyBlank(final Component component) { final Iterator it = component.iterator(ComponentIteratorType.DEPTH_FIRST); while (it.hasNext()) { final Component c = it.next(); if (!(c instanceof TextComponent text)) { // Assume non-text components probably aren't blank return false; } else if (!text.content().isBlank()) { // Found some content, definitely not blank return false; } } // Likely blank return true; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/DeafenHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class DeafenHandler implements Listener { @Inject public DeafenHandler(final CarbonEventHandler events) { events.subscribe(CarbonChatEvent.class, -10, false, event -> { if (!event.sender().deafened()) { return; } event.recipients().removeIf(entry -> entry instanceof CarbonPlayer carbonPlayer && carbonPlayer.deafened()); }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/FilterHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.event.events.CarbonPrivateChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.KeyedRenderer; import net.draycia.carbon.common.config.ConfigManager; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; public class FilterHandler implements Listener { private final ConfigManager configManager; @Inject public FilterHandler( final CarbonEventHandler events, final ConfigManager configManager ) { this.configManager = configManager; events.subscribe(CarbonPrivateChatEvent.class, -9, false, event -> { Component message = this.configManager.primaryConfig().applyChatFilters(event.message()); if (event.recipient().applyOptionalChatFilters()) { message = this.configManager.primaryConfig().applyOptionalChatFilters(message); } event.message(message); }); events.subscribe(CarbonChatEvent.class, -9, false, event -> { event.message(this.configManager.primaryConfig().applyChatFilters(event.message())); event.renderers().add(KeyedRenderer.keyedRenderer(Key.key("carbon", "filter"), ($, recipient, message, $$$) -> { if (recipient instanceof CarbonPlayer carbonPlayer) { if (carbonPlayer.applyOptionalChatFilters()) { return this.configManager.primaryConfig().applyOptionalChatFilters(message); } } return message; })); }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/HyperlinkHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import static net.draycia.carbon.common.util.Strings.URL_REPLACEMENT_CONFIG; @DefaultQualifier(NonNull.class) public class HyperlinkHandler implements Listener { @Inject public HyperlinkHandler(final CarbonEventHandler events) { events.subscribe(CarbonChatEvent.class, -7, false, event -> { if (event.sender().hasPermission("carbon.chatlinks")) { event.message(event.message().replaceText(URL_REPLACEMENT_CONFIG.get())); } }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/IgnoreHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class IgnoreHandler implements Listener { @Inject public IgnoreHandler(final CarbonEventHandler events) { events.subscribe(CarbonChatEvent.class, -8, false, event -> { event.recipients().removeIf(entry -> entry instanceof CarbonPlayer carbonPlayer && carbonPlayer.ignoring(event.sender())); }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/ItemLinkHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.event.events.CarbonPrivateChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.InventorySlot; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; public class ItemLinkHandler implements Listener { @Inject public ItemLinkHandler(final CarbonEventHandler events) { events.subscribe(CarbonChatEvent.class, -8, false, event -> { event.message(this.handleChatEvent(event.sender(), event.message())); }); events.subscribe(CarbonPrivateChatEvent.class, -8, false, event -> { event.message(this.handleChatEvent(event.sender(), event.message())); }); } private Component handleChatEvent(final CarbonPlayer sender, Component message) { if (!sender.hasPermission("carbon.itemlink")) { return message; } for (final var slot : InventorySlot.SLOTS) { for (final var placeholder : slot.placeholders()) { message = message .replaceText(TextReplacementConfig.builder() .matchLiteral("<" + placeholder + ">") .replacement(builder -> { final Component itemComponent = sender.createItemHoverComponent(slot); return itemComponent == null ? builder : itemComponent; }) .build()); } } return message; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/Listener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; public interface Listener { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/MessagePacketHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.UUID; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.event.events.CarbonChatEventImpl; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.messaging.ServerId; import net.draycia.carbon.common.messaging.packets.ChatMessagePacket; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class MessagePacketHandler implements Listener { @Inject public MessagePacketHandler( final CarbonEventHandler events, final @ServerId UUID serverId, final Provider messaging ) { events.subscribe(CarbonChatEvent.class, 100, false, event -> { if (!(event instanceof CarbonChatEventImpl e) || !e.origin) { return; } if (event.sender() instanceof ConsoleCarbonPlayer) { return; } if (!event.chatChannel().shouldCrossServer()) { return; } messaging.get().queuePacket(() -> { final CarbonPlayer sender = event.sender(); final Component networkMessage = e.renderFor(sender); return new ChatMessagePacket(serverId, sender.uuid(), event.chatChannel().key(), sender.username(), networkMessage); }); }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/MuteHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.KeyedRenderer; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import static net.draycia.carbon.api.util.KeyedRenderer.keyedRenderer; import static net.kyori.adventure.key.Key.key; @DefaultQualifier(NonNull.class) public class MuteHandler implements Listener { private final Key muteKey = key("carbon", "mute"); private CarbonMessages carbonMessages; private final KeyedRenderer renderer = keyedRenderer(this.muteKey, (sender, recipient, message, originalMessage) -> { // This is an annoying side effect of the RenderedComponent change final var prefix = this.carbonMessages.muteSpyPrefix(recipient); return prefix.append(message); }); @Inject public MuteHandler(final CarbonEventHandler events, final CarbonMessages carbonMessages) { this.carbonMessages = carbonMessages; events.subscribe(CarbonChatEvent.class, -10, false, event -> { if (!event.sender().muted()) { return; } event.renderers().add(this.renderer); event.recipients().removeIf(entry -> entry instanceof CarbonPlayer carbonPlayer && !carbonPlayer.spying() && !(entry instanceof ConsoleCarbonPlayer)); }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/PartyChatSpyHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import java.util.Set; import java.util.UUID; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.Party; import net.draycia.carbon.common.channels.PartyChatChannel; import net.draycia.carbon.common.messages.CarbonMessages; 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 class PartyChatSpyHandler implements Listener { @Inject public PartyChatSpyHandler(final CarbonEventHandler events, final CarbonMessages messages, final CarbonServer server) { events.subscribe(CarbonChatEvent.class, -1, false, event -> { if (!(event.chatChannel() instanceof PartyChatChannel)) { return; } final @Nullable Party party = event.sender().party().get(); final Set members = party == null ? Set.of() : party.members(); final Component partyName = party == null ? Component.empty() : party.name(); for (final CarbonPlayer player : server.players()) { if (player.spying() && !members.contains(player.uuid())) { messages.partySpy(player, event.sender().uuid(), event.sender().displayName(), event.sender().username(), event.message(), partyName); } } }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/PartyPingHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.PartyChatChannel; import net.draycia.carbon.common.config.ConfigManager; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.sound.Sound; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class PartyPingHandler implements Listener { @Inject public PartyPingHandler(final CarbonEventHandler events, final ConfigManager configManager) { events.subscribe(CarbonChatEvent.class, -6, false, event -> { if (!(event.chatChannel() instanceof PartyChatChannel)) { return; } final @Nullable Sound sound = configManager.primaryConfig().partyChat().messageSound; if (configManager.primaryConfig().partyChat().playSound && sound != null) { for (final Audience recipient : event.recipients()) { // Don't ping the message sender if (event.sender().uuid().equals(recipient.get(Identity.UUID).orElse(null))) { continue; } if (recipient instanceof CarbonPlayer player && !player.hasPermission("carbon.parties.ping_sound")) { continue; } recipient.playSound(sound, Sound.Emitter.self()); } } }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/PingHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.regex.Pattern; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.KeyedRenderer; import net.draycia.carbon.common.config.ConfigManager; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import static net.draycia.carbon.api.util.KeyedRenderer.keyedRenderer; import static net.kyori.adventure.key.Key.key; @DefaultQualifier(NonNull.class) @Singleton public class PingHandler implements Listener { private final Key pingKey = key("carbon", "pings"); private final KeyedRenderer renderer; private final ConfigManager configManager; @Inject public PingHandler(final CarbonEventHandler events, final ConfigManager configManager) { this.configManager = configManager; this.renderer = keyedRenderer(this.pingKey, (sender, recipient, message, originalMessage) -> { if (!(recipient instanceof CarbonPlayer recipientPlayer)) { return message; } return this.convertPings(recipientPlayer, message); }); events.subscribe(CarbonChatEvent.class, -9, false, event -> { event.renderers().add(0, this.renderer); }); } public Component convertPings(final CarbonPlayer recipient, final Component message) { final String prefix = this.configManager.primaryConfig().pings().prefix(); final String plainDisplayName = PlainTextComponentSerializer.plainText().serialize(recipient.displayName()); return message.replaceText(TextReplacementConfig.builder() // \B(@Username|@Displayname)\b .match(Pattern.compile( String.format( "\\B%1$s(%2$s|%3$s)\\b", Pattern.quote(prefix), Pattern.quote(recipient.username()), Pattern.quote(plainDisplayName)), Pattern.CASE_INSENSITIVE)) .replacement(matchedText -> { if (this.configManager.primaryConfig().pings().playSound() && recipient.hasPermission("carbon.ping_sounds")) { recipient.playSound(this.configManager.primaryConfig().pings().sound(), Sound.Emitter.self()); } return Component.text(matchedText.content()).color(this.configManager.primaryConfig().pings().highlightTextColor()); }) .build()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/listeners/RadiusListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.listeners; import com.google.inject.Inject; import java.util.ArrayList; import java.util.List; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class RadiusListener implements Listener { @Inject public RadiusListener( final CarbonEventHandler events, final CarbonMessages carbonMessages ) { events.subscribe(CarbonChatEvent.class, -5, false, event -> { if (event.chatChannel() == null) { return; } final double radius = event.chatChannel().radius(); if (radius < 0) { return; } final List spyingPlayers = new ArrayList<>(); if (radius == 0) { event.recipients().removeIf(audience -> { if (audience.equals(event.sender()) || audience instanceof ConsoleCarbonPlayer) { return false; } if (audience instanceof CarbonPlayer carbonPlayer) { final boolean sameWorld = carbonPlayer.sameWorldAs(event.sender()); if (!sameWorld && carbonPlayer.spying()) { spyingPlayers.add(carbonPlayer); } if (sameWorld && carbonPlayer.vanished()) { spyingPlayers.add(carbonPlayer); return true; } return !sameWorld; } return false; }); } else { event.recipients().removeIf(audience -> { if (audience.equals(event.sender()) || audience instanceof ConsoleCarbonPlayer) { return false; } if (audience instanceof CarbonPlayer carbonPlayer) { if (!event.sender().sameWorldAs(carbonPlayer)) { if (carbonPlayer.spying()) { spyingPlayers.add(carbonPlayer); } return true; } final double distance = carbonPlayer.distanceSquaredFrom(event.sender()); final boolean outOfRange = distance > (radius * radius); if (outOfRange && carbonPlayer.spying()) { spyingPlayers.add(carbonPlayer); } if (!outOfRange && carbonPlayer.vanished()) { spyingPlayers.add(carbonPlayer); return true; } return outOfRange; } return false; }); } if (event.recipients().size() <= 2 && event.chatChannel().emptyRadiusRecipientsMessage()) { // the player and cosole carbonMessages.emptyRecipients(event.sender()); return; } for (final CarbonPlayer player : spyingPlayers) { carbonMessages.radiusSpy(player, event.sender().uuid(), event.chatChannel().key(), event.sender().displayName(), event.sender().username(), event.message()); } }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageRenderer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.lang.reflect.Method; import java.lang.reflect.Type; 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; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public abstract class CarbonMessageRenderer implements IMessageRenderer { private final RenderForTagResolver.Factory renderForTagResolver; protected CarbonMessageRenderer(final RenderForTagResolver.Factory renderForTagResolver) { this.renderForTagResolver = renderForTagResolver; } public final IMessageRenderer asSourced() { return this::render; } @Override public final Component render( final Audience receiver, final String intermediateMessage, final Map resolvedPlaceholders, final @Nullable Method method, final @Nullable Type owner ) { final TagResolver.Builder builder = TagResolver.builder(); addResolved(builder, resolvedPlaceholders); builder.resolver(this.renderForTagResolver.create(resolvedPlaceholders)); return this.render(receiver, intermediateMessage, builder); } protected abstract Component render( Audience receiver, String intermediateMessage, TagResolver.Builder resolverBuilder ); @SuppressWarnings("PatternValidation") private 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()); } } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageSender.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.moonshine.message.IMessageSender; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class CarbonMessageSender implements IMessageSender { @Override public void send(final Audience receiver, final Component renderedMessage) { if (receiver instanceof SourcedAudience sourcedAudience) { sourcedAudience.recipient().sendMessage(renderedMessage); } else { receiver.sendMessage(renderedMessage); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageSource.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.Writer; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.DataDirectory; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.event.events.CarbonReloadEvent; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.translation.Translator; import net.kyori.moonshine.message.IMessageSource; 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; @Singleton @DefaultQualifier(NonNull.class) public final class CarbonMessageSource implements IMessageSource { private final Locale defaultLocale; private volatile Map locales = Map.of(); private final Path pluginJar; private final Logger logger; private final Path dataDirectory; @Inject private CarbonMessageSource( final CarbonEventHandler events, final @DataDirectory Path dataDirectory, final ConfigManager configManager, final Logger logger ) throws IOException { this.dataDirectory = dataDirectory; this.pluginJar = pluginJar(); this.logger = logger; this.defaultLocale = configManager.primaryConfig().defaultLocale(); this.reloadTranslations(); events.subscribe(CarbonReloadEvent.class, -99, true, event -> { this.reloadTranslations(); }); } private static @NonNull Path pluginJar() { try { URL sourceUrl = CarbonMessageSource.class.getProtectionDomain().getCodeSource().getLocation(); // Some class loaders give the full url to the class, some give the URL to its jar. // We want the containing jar, so we will unwrap jar-schema code sources. if (sourceUrl.getProtocol().equals("jar")) { final int exclamationIdx = sourceUrl.getPath().lastIndexOf('!'); if (exclamationIdx != -1) { sourceUrl = URI.create(sourceUrl.getPath().substring(0, exclamationIdx)).toURL(); } } return Paths.get(sourceUrl.toURI()); } catch (final URISyntaxException | MalformedURLException ex) { throw new RuntimeException("Could not locate plugin jar", ex); } } private void reloadTranslations() throws IOException { final Map map = new HashMap<>(); final Path localeDirectory = this.dataDirectory.resolve("locale"); // Create locale directory if (!Files.exists(localeDirectory)) { Files.createDirectories(localeDirectory); } this.walkPluginJar(stream -> stream.filter(Files::isRegularFile) .filter(it -> { final String pathString = it.toString(); return pathString.startsWith("/locale/messages-") && pathString.endsWith(".properties"); }) .forEach(localeFile -> { final String localeString = localeString(localeFile); final @Nullable Locale locale = parseLocale(localeString); if (locale == null) { this.logger.warn("Unknown locale '{}'?", localeString); return; } this.tryLoadLocale(map, localeDirectory, localeFile, locale); })); try (final Stream paths = Files.list(localeDirectory)) { paths.filter(Files::isRegularFile).forEach(localeFile -> { final String localeString = localeString(localeFile); final @Nullable Locale locale = parseLocale(localeString); if (locale == null) { this.logger.warn("Unknown locale '{}'?", localeString); return; } if (map.containsKey(locale)) { return; } this.tryLoadLocale(map, localeDirectory, localeFile, locale); }); } this.logger.info("Loaded {} locales: [{}]", map.size(), map.keySet().stream().map(Locale::toString).collect(Collectors.joining(", "))); this.locales = Map.copyOf(map); } private void tryLoadLocale(final Map map, final Path localeDirectory, final Path localeFile, final Locale locale) { final @Nullable Properties properties = this.readLocale(localeDirectory, localeFile, locale); if (properties != null) { map.put(locale, properties); } } private @Nullable Properties readLocale(final Path localeDirectory, final Path localeFile, final Locale locale) { this.logger.debug("Found locale {} ({}) in: {}", locale.getDisplayName(), locale, localeFile); final Properties properties = new Properties() { @Override public synchronized Set> entrySet() { return Collections.unmodifiableSet( (Set>) super.entrySet() .stream() .sorted(Comparator.comparing(entry -> entry.getKey().toString())) .collect(Collectors.toCollection(LinkedHashSet::new))); } }; try { this.loadProperties(properties, localeDirectory, localeFile); this.logger.debug("Successfully loaded locale {} ({})", locale.getDisplayName(), locale); return properties; } catch (final IOException ex) { this.logger.warn("Unable to load locale {} ({}) from source: {}", locale.getDisplayName(), locale, localeFile, ex); return null; } } @Override public String messageOf(final Audience receiver, final String messageKey) { Audience audience = receiver; if (audience instanceof SourcedAudience sourced) { audience = sourced.recipient(); } // Unwrap PlayerCommanders if (audience instanceof PlayerCommander playerCommander) { audience = playerCommander.carbonPlayer(); } if (audience instanceof CarbonPlayer player) { return this.forPlayer(messageKey, player); } else { return this.fromDefaultLocale(messageKey); } } private String forPlayer(final String key, final CarbonPlayer player) { final @Nullable Locale locale = player.locale(); if (locale != null) { final var properties = this.locales.get(locale); if (properties != null) { final var message = properties.getProperty(key); if (message != null) { return fixCrowdin(message); } } } return this.fromDefaultLocale(key); } private String fromDefaultLocale(final String key) { final Properties defaultProperties = this.locales.get(this.defaultLocale); if (defaultProperties != null) { final String value = defaultProperties.getProperty(key); if (value == null) { this.logger.warn("No message mapping for key " + key + " in default locale " + this.defaultLocale.getDisplayName()); return key; } return fixCrowdin(value); } return key; } private void walkPluginJar(final Consumer> user) throws IOException { if (Files.isDirectory(this.pluginJar)) { try (final var stream = Files.walk(this.pluginJar)) { user.accept(stream.map(path -> path.relativize(this.pluginJar))); } return; } try (final FileSystem jar = FileSystems.newFileSystem(this.pluginJar, this.getClass().getClassLoader())) { final Path root = jar.getRootDirectories() .iterator() .next(); try (final var stream = Files.walk(root)) { user.accept(stream); } } } private void loadProperties( final Properties properties, final Path localeDirectory, final Path sourceFile ) throws IOException { final Path userFile = localeDirectory.resolve(sourceFile.getFileName().toString()); final boolean samePath = sourceFile.normalize().toAbsolutePath().equals(userFile.normalize().toAbsolutePath()); if (Files.isRegularFile(userFile)) { // If the file in the localeDirectory exists, read it to the properties final InputStream inputStream = Files.newInputStream(userFile); try (final Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { properties.load(reader); } } else if (samePath && !Files.isRegularFile(userFile)) { throw new IllegalStateException("sourceFile == userFile, and is not a regular file (%s)".formatted(userFile)); } boolean write = false; // Read the file in the jar and add missing entries if (Files.isRegularFile(sourceFile) && !samePath) { try (final Reader reader = new InputStreamReader(Files.newInputStream(sourceFile), StandardCharsets.UTF_8)) { final Properties packaged = new Properties(); packaged.load(reader); for (final Map.Entry entry : packaged.entrySet()) { write |= properties.putIfAbsent(entry.getKey(), entry.getValue()) == null; } } } // todo: copy missing entries from default english locale as well? // Write properties back to file if (write) { try (final Writer outputStream = Files.newBufferedWriter(userFile)) { properties.store(outputStream, null); } } } private static String localeString(final Path localeFile) { return localeFile.getFileName().toString().substring("messages-".length()).replace(".properties", ""); } private static @Nullable Locale parseLocale(String localeString) { // MC uses no_NO when the player selects nb_NO... localeString = localeString.replace("nb_NO", "no_NO"); return Translator.parseLocale(localeString); } // Crowdin exports single quotes as double quotes private static String fixCrowdin(final String s) { return s.replace("''", "'"); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/CarbonMessages.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.UUID; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.moonshine.annotation.Message; import net.kyori.moonshine.annotation.Placeholder; public interface CarbonMessages { /* * ============================================================= * ======================== Basic Chat ========================= * ============================================================= */ @Message("channel.change") void changedChannels(final Audience audience, final String channel); @Message("channel.radius.empty_recipients") void emptyRecipients(final Audience audience); @Message("channel.radius.spy") void radiusSpy( Audience audience, @Placeholder UUID uuid, @Placeholder Key channel, @Placeholder("display_name") Component displayName, @Placeholder String username, @Placeholder Component message ); @Message("channel.not_found") void channelNotFound(final Audience audience); @Message("channel.not_left") void channelNotLeft(final Audience audience); @Message("channel.already_left") void channelAlreadyLeft(final Audience audience); @Message("channel.no_permission") Component channelNoPermission(Audience audience); @Message("channel.left") void channelLeft(final Audience audience); @Message("channel.joined") void channelJoined(final Audience audience); @Message("channel.cooldown") void channelCooldown(final Audience audience, long remaining); /* * ============================================================= * =========================== Mutes =========================== * ============================================================= */ @Message("mute.info.self.muted") void muteInfoSelfMuted(final Audience audience); @Message("mute.info.self.not_muted") void muteInfoSelfNotMuted(final Audience audience); @Message("mute.info.not_muted") void muteInfoNotMuted(final Audience audience, final Component target); @Message("mute.info.muted.duration") void muteInfoMutedDuration(final Audience audience, final Component target, final Component duration); @Message("mute.info.muted") void muteInfoMuted(final Audience audience, final Component target, final boolean muted); @Message("mute.unmute.alert.target") void unmuteAlertRecipient(final Audience audience); @Message("mute.unmute.alert.players") void unmuteAlertPlayers(final Audience audience, final Component target); @Message("mute.unmute.no_target") void unmuteNoTarget(final Audience audience); @Message("mute.exempt") void muteExempt(final Audience audience); @Message("mute.alert.target") void muteAlertRecipient(final Audience audience); @Message("mute.alert.players") void muteAlertPlayers(final Audience audience, final Component target); @Message("mute.cannot_speak") void muteCannotSpeak(final Audience audience); @Message("mute.no_target") void muteNoTarget(final Audience audience); @Message("mute.spy.prefix") Component muteSpyPrefix(final Audience audience); @Message("mute.alert.target.temp") void tempMuteAlertRecipient(final Audience audience, final Component duration); @Message("mute.alert.players.temp") void tempMuteAlertPlayers(final Audience audience, final Component target, final Component duration); @Message("duration.hours") Component durationHours(final int hours, final int minutes, final int seconds); @Message("duration.days") Component durationDays(final long days, final int hours, final int minutes, final int seconds); /* * ============================================================= * ====================== Direct Messages ====================== * ============================================================= */ @Message("whisper.to") Component whisperSender( @NotPlaceholder SourcedAudience audience, String senderUsername, Component senderDisplayName, String recipientUsername, Component recipientDisplayName, UUID recipientUuid, Component message ); @Message("whisper.from") Component whisperRecipient( @NotPlaceholder SourcedAudience audience, String senderUsername, Component senderDisplayName, String recipientUsername, Component recipientDisplayName, UUID recipientUuid, Component message ); @Message("whisper.from.spy") void whisperRecipientSpy( Audience audience, String senderUsername, Component senderDisplayName, String recipientUsername, Component recipientDisplayName, Component message ); @Message("whisper.console") void whisperConsoleLog( Audience audience, String senderUsername, Component senderDisplayName, String recipientUsername, Component recipientDisplayName, Component message ); @Message("whisper.error") void whisperError( final Audience audience, @Placeholder("sender_display_name") final Component senderDisplayName, @Placeholder("recipient_display_name") final Component recipientDisplayName ); @Message("reply.target.missing") void replyTargetNotSet(final Audience audience, @Placeholder("sender_display_name") final Component senderDisplayName); @Message("reply.target.self") void whisperSelfError(final Audience audience, @Placeholder("sender_display_name") final Component senderDisplayName); @Message("whisper.continue.target_missing") void whisperTargetNotSet( final Audience audience, @Placeholder("sender_display_name") final Component senderDisplayName ); @Message("whisper.ignoring_all") void whisperIgnoringAll(final Audience audience); @Message("whisper.ignoring_target") void whisperIgnoringTarget(final Audience audience, final Component target); @Message("whisper.ignored_by_target") void whisperTargetIgnoring(final Audience audience, final Component target); @Message("whisper.ignored_dms") void whisperTargetIgnoringDMs(final Audience audience, final Component target); @Message("whisper.toggled.on") void whispersToggledOn(final Audience audience); @Message("whisper.toggled.off") void whispersToggledOff(final Audience audience); @Message("whisper.no_permission.send") void whisperNoPermissionSend(final Audience audience); @Message("whisper.no_permission.receive") void whisperNoPermissionReceive(final Audience audience); /* * ============================================================= * ========================= Nicknames ========================= * ============================================================= */ @Message("nickname.set") void nicknameSet(final Audience audience, final Component nickname); @Message("nickname.set.others") void nicknameSetOthers(final Audience audience, final String target, final Component nickname); @Message("nickname.error.character_limit") void nicknameErrorCharacterLimit( final Audience audience, final Component nickname, final int minLength, final int maxLength ); @Message("nickname.error.blacklist") void nicknameErrorBlackList(final Audience audience, final Component nickname); @Message("nickname.error.filter") void nicknameErrorFilter(final Audience audience, final Component nickname); @Message("nickname.show.others") void nicknameShowOthers(final Audience audience, final String target, final Component nickname); @Message("nickname.show.others.unset") void nicknameShowOthersUnset(final Audience audience, final String target); @Message("nickname.show") void nicknameShow(final Audience audience, final String target, final Component nickname); @Message("nickname.show.unset") void nicknameShowUnset(final Audience audience, final String target); @Message("nickname.reset") void nicknameReset(final Audience audience); @Message("nickname.reset.others") void nicknameResetOthers(final Audience audience, final String target); @Message("nickname.realname") void realName(final Audience audience, final Component target, final String username); @Message("nickname.realname.target_invalid") void realNameTargetInvalid(final Audience audience, final String input); /* * ============================================================= * ========================== Ignore =========================== * ============================================================= */ @Message("ignore.already_ignored") void alreadyIgnored(final Audience audience, final Component target); @Message("ignore.not_ignored") void notIgnored(Audience audience, Component target); @Message("ignore.exempt") void ignoreExempt(final Audience audience, final Component target); @Message("ignore.now_ignoring") void nowIgnoring(final Audience audience, final Component target); @Message("ignore.no_longer_ignoring") void noLongerIgnoring(final Audience audience, final Component target); @Message("ignore.invalid_target") void ignoreTargetInvalid(final Audience audience); /* * ============================================================= * ========================== Reload =========================== * ============================================================= */ @Message("config.reload.success") void configReloaded(final Audience audience); @Message("config.reload.failed") void configReloadFailed(final Audience audience); /* * ============================================================= * ========================== Spying =========================== * ============================================================= */ @Message("command.spy.enabled") void commandSpyEnabled(final Audience audience); @Message("command.spy.disabled") void commandSpyDisabled(final Audience audience); @Message("command.spy.description") Component commandSpyDescription(); /* * ============================================================= * ========================== Filters ========================== * ============================================================= */ @Message("command.filter.optional.enabled") void commandOptionalFilterEnabled(final Audience audience); @Message("command.filter.optional.disabled") void commandOptionalFilterDisabled(final Audience audience); @Message("command.filter.optional.description") Component commandOptionalFilterDescription(); /* * ============================================================= * ====================== Cloud Messages ======================= * ============================================================= */ @Message("error.command.no_permission") void errorCommandNoPermission(final Audience audience); @Message("error.command.command_execution") void errorCommandCommandExecution( final Audience audience, @Placeholder("throwable_message") final Component throwableMessage, final String stacktrace ); @Message("error.command.argument_parsing") void errorCommandArgumentParsing(final Audience audience, @Placeholder("throwable_message") final Component throwableMessage); @Message("error.command.invalid_player") Component errorCommandInvalidPlayer(String input); @Message("error.command.invalid_sender") void errorCommandInvalidSender(final Audience audience, final String sender_type); @Message("error.command.invalid_syntax") void errorCommandInvalidSyntax(final Audience audience, final Component syntax); @Message("error.command.command_needs_player") Component commandNeedsPlayer(); /* * ============================================================= * =================== Command Documentation =================== * ============================================================= */ @Message("command.clearchat.description") Component commandClearChatDescription(); @Message("command.continue.argument.message") Component commandContinueArgumentMessage(); @Message("command.continue.description") Component commandContinueDescription(); @Message("command.debug.argument.player") Component commandDebugArgumentPlayer(); @Message("command.debug.description") Component commandDebugDescription(); @Message("command.help.argument.query") Component commandHelpArgumentQuery(); @Message("command.help.description") Component commandHelpDescription(); @Message("command.ignore.argument.player") Component commandIgnoreArgumentPlayer(); @Message("command.ignore.argument.uuid") Component commandIgnoreArgumentUUID(); @Message("command.ignore.description") Component commandIgnoreDescription(); @Message("command.ignorelist.description") Component commandIgnoreListDescription(); @Message("command.ignorelist.none_ignored") void commandIgnoreListNoneIgnored(Audience audience); @Message("command.ignorelist.pagination_header") Component commandIgnoreListPaginationHeader(int page, int pages); @Message("command.ignorelist.pagination_element") Component commandIgnoreListPaginationElement(Component displayName, String username); @Message("command.join.description") Component commandJoinDescription(); @Message("command.leave.description") Component commandLeaveDescription(); @Message("command.mute.argument.player") Component commandMuteArgumentPlayer(); @Message("command.mute.argument.duration") Component commandMuteArgumentDuration(); @Message("command.mute.argument.uuid") Component commandMuteArgumentUUID(); @Message("command.mute.description") Component commandMuteDescription(); @Message("command.muteinfo.argument.player") Component commandMuteInfoArgumentPlayer(); @Message("command.muteinfo.argument.uuid") Component commandMuteInfoArgumentUUID(); @Message("command.muteinfo.description") Component commandMuteInfoDescription(); @Message("command.nickname.argument.player") Component commandNicknameArgumentPlayer(); @Message("command.nickname.argument.nickname") Component commandNicknameArgumentNickname(); @Message("command.nickname.reset.description") Component commandNicknameResetDescription(); @Message("command.nickname.set.description") Component commandNicknameSetDescription(); @Message("command.nickname.description") Component commandNicknameDescription(); @Message("command.nickname.others.reset.description") Component commandNicknameOthersResetDescription(); @Message("command.nickname.others.set.description") Component commandNicknameOthersSetDescription(); @Message("command.nickname.others.description") Component commandNicknameOthersDescription(); @Message("command.reload.description") Component commandReloadDescription(); @Message("command.reply.argument.message") Component commandReplyArgumentMessage(); @Message("command.reply.description") Component commandReplyDescription(); @Message("command.togglemsg.description") Component commandToggleMsgDescription(); @Message("command.unignore.argument.player") Component commandUnignoreArgumentPlayer(); @Message("command.unignore.argument.uuid") Component commandUnignoreArgumentUUID(); @Message("command.unignore.description") Component commandUnignoreDescription(); @Message("command.unmute.argument.player") Component commandUnmuteArgumentPlayer(); @Message("command.unmute.argument.uuid") Component commandUnmuteArgumentUUID(); @Message("command.unmute.description") Component commandUnmuteDescription(); @Message("command.whisper.argument.player") Component commandWhisperArgumentPlayer(); @Message("command.whisper.argument.message") Component commandWhisperArgumentMessage(); @Message("command.whisper.description") Component commandWhisperDescription(); @Message("command.realname.description") Component commandRealNameDescription(); @Message("command.realname.argument.player") Component commandRealNameArgumentPlayer(); @Message("command.updateusername.description") Component commandUpdateUsernameDescription(); @Message("command.updateusername.argument.player") Component commandUpdateUsernameArgumentPlayer(); @Message("command.updateusername.argument.uuid") Component commandUpdateUsernameArgumentUUID(); @Message("command.updateusername.notupdated") void usernameNotUpdated(final Audience recipient); @Message("command.updateusername.fetching") void usernameFetching(final Audience audience); @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, String senderUsername, 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("party.cannot_use_channel") Component cannotUsePartyChannel(Audience audience); @Message("party.spy") void partySpy( Audience audience, @Placeholder UUID uuid, @Placeholder("display_name") Component displayName, @Placeholder String username, @Placeholder Component message, @Placeholder("party_name") Component partyName ); @Message("deletemessage.prefix") Component deleteMessagePrefix(); @Message("pagination.page_out_of_range") Component paginationOutOfRange(int page, int pages); @Message("pagination.click_for_next_page") Component paginationClickForNextPage(); @Message("pagination.click_for_previous_page") Component paginationClickForPreviousPage(); @Message("pagination.footer") Component paginationFooter(int page, int pages, Component buttons); /* * ============================================================= * ======================= Integrations ======================== * ============================================================= */ @Message("integrations.towny.cannot_use_alliance_channel") Component cannotUseAllianceChannel(Audience audience); @Message("integrations.towny.cannot_use_nation_channel") Component cannotUseNationChannel(Audience audience); @Message("integrations.towny.cannot_use_town_channel") Component cannotUseTownChannel(Audience audience); @Message("integrations.mcmmo.cannot_use_party_channel") Component cannotUseMcmmoPartyChannel(Audience audience); @Message("integrations.adp_parties.cannot_use_party_channel") Component cannotUseADPPartiesPartyChannel(Audience audience); @Message("integrations.fuuid.cannot_use_faction_channel") Component cannotUseFactionChannel(Audience audience); @Message("integrations.fuuid.cannot_use_alliance_channel") Component cannotUseFactionAllianceChannel(Audience audience); @Message("integrations.fuuid.cannot_use_truce_channel") Component cannotUseTruceChannel(Audience audience); @Message("integrations.fuuid.cannot_use_mod_channel") Component cannotUseFactionModChannel(Audience audience); @Message("integrations.plotsquared.cannot_use_plot_channel") Component cannotUsePlotChannel(Audience audience); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/NotPlaceholder.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Documented @Target({ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface NotPlaceholder { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/Option.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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) { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/OptionTagResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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"); String f = ""; if (arguments.peek() != null) { f = arguments.pop().value(); } return Tag.preProcessParsed(this.state ? t.value() : f); } @Override public boolean has(final String name) { return name.equals(this.name); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/PrefixedDelegateIterator.java ================================================ /* * moonshine - A localisation library for Java. * Copyright (C) Mariell Hoversholm * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package net.draycia.carbon.common.messages; import java.util.Iterator; public final class PrefixedDelegateIterator implements Iterator { private final T prefix; private final Iterator delegate; private boolean seenPrefix = false; public PrefixedDelegateIterator(final T prefix, final Iterator delegate) { this.prefix = prefix; this.delegate = delegate; } @Override public boolean hasNext() { return !this.seenPrefix || this.delegate.hasNext(); } @Override public T next() { if (!this.seenPrefix) { this.seenPrefix = true; return this.prefix; } return this.delegate.next(); } @Override public void remove() { if (!this.seenPrefix) { throw new IllegalStateException("must see prefix before removing from iterator"); } this.delegate.remove(); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/RenderForTagResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import java.util.Map; import java.util.UUID; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.common.users.UserManagerInternal; 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.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public final class RenderForTagResolver implements TagResolver { private static final String TAG_NAME = "render_for"; private final UserManagerInternal users; private final ProfileResolver profileResolver; private final Provider messageRenderer; private final Map resolvedPlaceholders; public interface Factory { RenderForTagResolver create(Map resolvedPlaceholders); } @AssistedInject private RenderForTagResolver( final UserManagerInternal users, final ProfileResolver profileResolver, final Provider messageRenderer, final @Assisted Map resolvedPlaceholders ) { this.users = users; this.profileResolver = profileResolver; this.messageRenderer = messageRenderer; this.resolvedPlaceholders = resolvedPlaceholders; } @SuppressWarnings("DataFlowIssue") @Override public @Nullable Tag resolve(final String name, final ArgumentQueue arguments, final Context ctx) throws ParsingException { if (!this.has(name)) { return null; } final String renderFor = arguments.popOr("Missing username or UUID to render for").value(); CompletableFuture<@Nullable ? extends CarbonPlayer> playerFuture; try { final UUID uuid = UUID.fromString(renderFor); playerFuture = this.users.user(uuid); } catch (final IllegalArgumentException ignore) { playerFuture = this.profileResolver.resolveUUID(renderFor).thenCompose(uuid -> { if (uuid != null) { return this.users.user(uuid); } return CompletableFuture.completedFuture(null); }); } final @Nullable CarbonPlayer player; try { player = playerFuture.join(); if (player == null) { return null; } } catch (final CompletionException | CancellationException ignore) { return null; } final String value = arguments.popOr("Missing message value").value(); if (value.equalsIgnoreCase("inserting")) { return Tag.inserting( this.messageRenderer.get().render(SourcedAudience.of(player, player), arguments.popOr("Missing message value").value(), this.resolvedPlaceholders, null, null) ); } else if (value.equalsIgnoreCase("self_closing_inserting")) { return Tag.selfClosingInserting( this.messageRenderer.get().render(SourcedAudience.of(player, player), arguments.popOr("Missing message value").value(), this.resolvedPlaceholders, null, null) ); } else { return Tag.selfClosingInserting(this.messageRenderer.get().render(SourcedAudience.of(player, player), value, this.resolvedPlaceholders, null, null)); } } @Override public boolean has(final String name) { return name.equalsIgnoreCase(TAG_NAME); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/SourcedAudience.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; /** * An audience, where messages are sent from another Audience. */ @DefaultQualifier(NonNull.class) public interface SourcedAudience extends ForwardingAudience.Single { /** * The source audience. * * @return source */ Audience sender(); /** * The recipient audience. The audience that this sourced audience forwards to. * * @return recipient */ Audience recipient(); @Override default Audience audience() { return this.recipient(); } /** * Create a new {@link SourcedAudience} instance. * * @param sender sender * @param recipient recipient * @return sourced audience */ static SourcedAudience of(final Audience sender, final Audience recipient) { return new SourcedAudienceImpl(sender, recipient); } /** * The empty {@link SourcedAudience}, with an empty sender and recipient. * * @return the empty sourced audience */ static SourcedAudience empty() { return SourcedAudienceImpl.EMPTY; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/SourcedAudienceImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.audience.Audience; record SourcedAudienceImpl(Audience sender, Audience recipient) implements SourcedAudience { static final SourcedAudience EMPTY = new SourcedAudienceImpl(Audience.empty(), Audience.empty()); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/SourcedMessageSender.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.Component; import net.kyori.moonshine.message.IMessageSender; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class SourcedMessageSender implements IMessageSender { @Override public void send(final SourcedAudience receiver, final Component renderedMessage) { receiver.recipient().sendMessage(renderedMessage); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/SourcedReceiverResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Singleton; import java.lang.reflect.Method; import java.lang.reflect.Type; import net.kyori.adventure.audience.Audience; import net.kyori.moonshine.receiver.IReceiverLocator; import net.kyori.moonshine.receiver.IReceiverLocatorResolver; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @Singleton @DefaultQualifier(NonNull.class) public final class SourcedReceiverResolver implements IReceiverLocatorResolver { @Override public IReceiverLocator resolve(final Method method, final Type proxy) { return new Resolver(); } private static final class Resolver implements IReceiverLocator { @Override public SourcedAudience locate(final Method method, final Object proxy, final @Nullable Object[] parameters) { if (parameters.length == 0) { return SourcedAudience.empty(); } final @Nullable Object parameter = parameters[0]; if (parameter instanceof SourcedAudience sourcedAudience) { return sourcedAudience; } else if (parameter instanceof Audience audience) { return SourcedAudience.of(audience, audience); } return SourcedAudience.empty(); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/StandardPlaceholderResolverStrategyButDifferent.java ================================================ /* * moonshine - A localisation library for Java. * Copyright (C) Mariell Hoversholm * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package net.draycia.carbon.common.messages; import io.leangen.geantyref.GenericTypeReflector; import java.lang.reflect.Parameter; import java.lang.reflect.Type; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import net.kyori.adventure.audience.Audience; import net.kyori.moonshine.Moonshine; import net.kyori.moonshine.annotation.Placeholder; import net.kyori.moonshine.annotation.meta.ThreadSafe; import net.kyori.moonshine.exception.PlaceholderResolvingException; import net.kyori.moonshine.exception.UnfinishedPlaceholderException; import net.kyori.moonshine.model.MoonshineMethod; import net.kyori.moonshine.placeholder.ContinuanceValue; import net.kyori.moonshine.placeholder.IPlaceholderResolver; import net.kyori.moonshine.strategy.IPlaceholderResolverStrategy; import net.kyori.moonshine.strategy.supertype.ISupertypeStrategy; import net.kyori.moonshine.strategy.supertype.StandardSupertypeThenInterfaceSupertypeStrategy; import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.util.NamingScheme; import static java.util.Collections.emptyNavigableSet; @ThreadSafe public final class StandardPlaceholderResolverStrategyButDifferent implements IPlaceholderResolverStrategy { private final ISupertypeStrategy supertypeStrategy = new StandardSupertypeThenInterfaceSupertypeStrategy(false); private final NamingScheme namingScheme; /** * Create a new {@link StandardPlaceholderResolverStrategyButDifferent}. * * @param placeholderParamaterDefaultNamingScheme the default naming scheme for placeholders that do not specify a name explicitly, applied to parameter names */ public StandardPlaceholderResolverStrategyButDifferent(final NamingScheme placeholderParamaterDefaultNamingScheme) { this.namingScheme = placeholderParamaterDefaultNamingScheme; } @Override public Map resolvePlaceholders( final Moonshine moonshine, final R receiver, final I intermediateText, final MoonshineMethod moonshineMethod, final @Nullable Object[] parameters ) throws PlaceholderResolvingException { if (parameters.length == 0) { return Collections.emptyMap(); } final Map finalisedPlaceholders = new LinkedHashMap<>(parameters.length); final Map> resolvingPlaceholders = new LinkedHashMap<>(16); final Parameter[] methodParameters = moonshineMethod.reflectMethod().getParameters(); final Type[] exactParameterTypes = GenericTypeReflector.getParameterTypes( moonshineMethod.reflectMethod(), moonshine.proxiedType()); // Don't resolve recipients final int start = moonshineMethod.reflectMethod().getReturnType() != Void.TYPE ? 0 : 1; for (int idx = start; idx < parameters.length; ++idx) { final Parameter parameter = methodParameters[idx]; final @Nullable Object value = parameters[idx]; // Don't resolve Audiences for now if (value == null || parameter.getType() == Audience.class || parameter.getAnnotation(NotPlaceholder.class) != null) { // Nothing to resolve with. continue; } final Type parameterType = GenericTypeReflector.getExactSubType( exactParameterTypes[idx], value.getClass()); final @Nullable Placeholder placeholder = parameter.getAnnotation(Placeholder.class); final String placeholderName = (placeholder != null && !placeholder.value().isEmpty()) ? placeholder.value() : this.namingScheme.coerce(parameter.getName()); resolvingPlaceholders .put(placeholderName, ContinuanceValue.continuanceValue(value, parameterType)); } this.resolvePlaceholder(moonshine, receiver, finalisedPlaceholders, resolvingPlaceholders, moonshineMethod, parameters); return finalisedPlaceholders; } /** * Resolve a single placeholder. * * @param moonshine the moonshine instance * @param finalisedPlaceholders the finalised placeholders * @param resolvingPlaceholders the placeholders to resolve * @param moonshineMethod the method we are resolving a placeholder for */ private void resolvePlaceholder( final Moonshine moonshine, final R receiver, final Map finalisedPlaceholders, final Map> resolvingPlaceholders, final MoonshineMethod moonshineMethod, final @Nullable Object[] parameters ) throws UnfinishedPlaceholderException { final var weightedPlaceholderResolvers = moonshine.weightedPlaceholderResolvers(); // Shamelessly stealing kashike's joke dancing: while (!resolvingPlaceholders.isEmpty()) { final var resolvingPlaceholderIterator = resolvingPlaceholders.entrySet().iterator(); while (resolvingPlaceholderIterator.hasNext()) { final var continuanceEntry = resolvingPlaceholderIterator.next(); final String continuancePlaceholderName = continuanceEntry.getKey(); final Type type = continuanceEntry.getValue().type(); final Object value = continuanceEntry.getValue().value(); final Iterator hierarchyIterator = new PrefixedDelegateIterator<>(type, this.supertypeStrategy.hierarchyIterator(type)); while (hierarchyIterator.hasNext()) { final Type supertype = hierarchyIterator.next(); for (final var weighted : weightedPlaceholderResolvers.getOrDefault(supertype, emptyNavigableSet())) { @SuppressWarnings("unchecked") // This should be equivalent. final var placeholderResolver = (IPlaceholderResolver) weighted.value(); final var resolverResult = placeholderResolver.resolve(continuancePlaceholderName, value, receiver, moonshineMethod.owner().getType(), moonshineMethod.reflectMethod(), parameters); if (resolverResult == null) { // The resolver did not want to resolve this; pass it on. continue; } resolvingPlaceholderIterator.remove(); resolverResult.forEach((resolvedName, resolvedValue) -> resolvedValue.map(conclusionValue -> finalisedPlaceholders .put(resolvedName, conclusionValue.value()), continuanceValue -> resolvingPlaceholders.put(resolvedName, continuanceValue))); continue dancing; } } throw new UnfinishedPlaceholderException(moonshineMethod, continuancePlaceholderName, value); } } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/TagPermissions.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.Map; import java.util.function.Predicate; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.minimessage.tag.standard.StandardTags; 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 TagPermissions { public static final String NICKNAME = "carbon.nickname.tags"; public static final String MESSAGE = "carbon.messagetags"; public static final String PARTY_NAME = "carbon.parties.name.tags"; private static final Map DEFAULT_TAGS = Map.ofEntries( Map.entry("hover", StandardTags.hoverEvent()), Map.entry("click", StandardTags.clickEvent()), Map.entry("color", StandardTags.color()), Map.entry("keybind", StandardTags.keybind()), Map.entry("translatable", StandardTags.translatable()), Map.entry("insertion", StandardTags.insertion()), Map.entry("font", StandardTags.font()), Map.entry("decorations", StandardTags.decorations()), Map.entry("gradient", StandardTags.gradient()), Map.entry("rainbow", StandardTags.rainbow()), Map.entry("reset", StandardTags.reset()), Map.entry("newline", StandardTags.newline()), Map.entry("pride", StandardTags.pride()), Map.entry("shadow_color", StandardTags.shadowColor()), Map.entry("transition", StandardTags.transition()) ); private TagPermissions() { } public static Component parseTags( final @Nullable Audience audience, final String basePermission, final String message, final Predicate permission, final TagResolver.Builder resolver ) { boolean hasAllDecorations = false; for (final Map.Entry entry : DEFAULT_TAGS.entrySet()) { if (permission.test(basePermission + '.' + entry.getKey())) { resolver.resolver(entry.getValue()); if (entry.getKey().equals("decorations")) { hasAllDecorations = true; } } } if (!hasAllDecorations) { for (final TextDecoration decoration : TextDecoration.values()) { if (!permission.test(basePermission + '.' + decoration.name())) { continue; } resolver.resolver(StandardTags.decorations(decoration)); } } final MiniMessage miniMessage = MiniMessage.builder().tags(resolver.build()).build(); if (audience != null) { return miniMessage.deserialize(message, audience); } return miniMessage.deserialize(message); } public static Component parseTags( final @Nullable Audience audience, final String basePermission, final String message, final Predicate permission ) { return parseTags(audience, basePermission, message, permission, TagResolver.builder()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/placeholders/BooleanPlaceholderResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.kyori.adventure.text.minimessage.tag.Tag; 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 class BooleanPlaceholderResolver implements IPlaceholderResolver { @Override public @Nullable Map, ContinuanceValue>> resolve( final String placeholderName, final Boolean value, final R receiver, final Type owner, final Method method, final @Nullable Object[] parameters ) { if (value == null) { return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed("false")))); } return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(value.toString())))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/placeholders/ComponentPlaceholderResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.Tag; 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.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class ComponentPlaceholderResolver implements IPlaceholderResolver { @Override public @Nullable Map, ContinuanceValue>> resolve( final String placeholderName, final Component value, final R receiver, final Type owner, final Method method, final @Nullable Object[] parameters ) { return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.selfClosingInserting(value)))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/placeholders/IntPlaceholderResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.kyori.adventure.text.minimessage.tag.Tag; 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 class IntPlaceholderResolver implements IPlaceholderResolver { @Override public @Nullable Map, ContinuanceValue>> resolve( final String placeholderName, final Integer value, final R receiver, final Type owner, final Method method, final @Nullable Object[] parameters ) { return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(String.valueOf(value))))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/placeholders/KeyPlaceholderResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.kyori.adventure.key.Key; import net.kyori.adventure.text.minimessage.tag.Tag; 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 class KeyPlaceholderResolver implements IPlaceholderResolver { @Override public @Nullable Map, ContinuanceValue>> resolve( final String placeholderName, final Key value, final R receiver, final Type owner, final Method method, final @Nullable Object[] parameters ) { return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(value.toString())))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/placeholders/LongPlaceholderResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.kyori.adventure.text.minimessage.tag.Tag; 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 class LongPlaceholderResolver implements IPlaceholderResolver { @Override public @Nullable Map, ContinuanceValue>> resolve( final String placeholderName, final Long value, final R receiver, final Type owner, final Method method, final @Nullable Object[] parameters ) { return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(String.valueOf(value))))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/placeholders/OptionPlaceholderResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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())))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/placeholders/StringPlaceholderResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.kyori.adventure.text.minimessage.tag.Tag; 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 class StringPlaceholderResolver implements IPlaceholderResolver { @Override public @Nullable Map, ContinuanceValue>> resolve( final String placeholderName, final String value, final R receiver, final Type owner, final Method method, final @Nullable Object[] parameters ) { return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(value)))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messages/placeholders/UUIDPlaceholderResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.UUID; import net.kyori.adventure.text.minimessage.tag.Tag; 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.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class UUIDPlaceholderResolver implements IPlaceholderResolver { @Override public @Nullable Map, ContinuanceValue>> resolve( final String placeholderName, final UUID value, final R receiver, final Type owner, final Method method, final @Nullable Object[] parameters ) { return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(value.toString())))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/CarbonChatPacketHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import java.util.ArrayList; import java.util.List; 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.api.event.CarbonEventHandler; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.KeyedRenderer; 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.DisbandPartyPacket; 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.ConsoleCarbonPlayer; 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.key.Key; import net.kyori.adventure.text.Component; import ninja.egg82.messenger.handler.AbstractMessagingHandler; import ninja.egg82.messenger.packets.Packet; 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 CarbonChatPacketHandler extends AbstractMessagingHandler { private final CarbonEventHandler events; private final CarbonServer server; private final ChannelRegistry channels; 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 PartyInvites partyInvites ) { super(messagingManager.requirePacketService()); this.events = carbonChat.eventHandler(); this.server = carbonChat.server(); this.channels = carbonChat.channelRegistry(); this.userManager = userManager; this.networkUsers = networkUsers; this.whisper = whisper; this.partyInvites = partyInvites; } @Override 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 DisbandPartyPacket pkt) { this.userManager.disbandPartyMessageReceived(pkt); return true; } else if (packet instanceof ChatMessagePacket messagePacket) { this.handleMessagePacket(messagePacket); return true; // Don't log an error when the channel doesn't exist } else if (packet instanceof LocalPlayersPacket playersPacket) { this.networkUsers.handlePacket(playersPacket); return true; } else if (packet instanceof LocalPlayerChangePacket playerChangePacket) { this.networkUsers.handlePacket(playerChangePacket); return true; } else if (packet instanceof WhisperPacket whisperPacket) { this.whisper.handlePacket(whisperPacket); return true; } return false; } private boolean handleMessagePacket(final ChatMessagePacket messagePacket) { final CarbonPlayer sender = this.userManager.user(messagePacket.userId()).join(); final @Nullable ChatChannel channel = this.channels.channel(messagePacket.channelKey()); if (channel == null) { return false; } if (!channel.shouldCrossServer()) { return false; } final List renderers = new ArrayList<>(); final List recipients = channel.recipients(sender); final CarbonChatEventImpl chatEvent = new CarbonChatEventImpl(sender, messagePacket.message(), recipients, renderers, channel, null, false); this.events.emit(chatEvent); renderers.add(KeyedRenderer.keyedRenderer(Key.key("carbon", "console_cs"), ($, recipient, message, original) -> { if (recipient instanceof ConsoleCarbonPlayer) { return Component.textOfChildren(Component.text("[Cross-Server] "), message); } return message; })); for (final Audience recipient : recipients) { if (recipient instanceof CarbonPlayer carbonRecipient && !carbonRecipient.hasPermission("carbon.crossserver")) { continue; } recipient.sendMessage(chatEvent.renderFor(recipient)); } return true; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/MessagingManager.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Supplier; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.CarbonChatInternal; import net.draycia.carbon.common.command.commands.WhisperCommand; 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.DisbandPartyPacket; 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; import net.draycia.carbon.common.util.Exceptions; import ninja.egg82.messenger.MessagingService; import ninja.egg82.messenger.NATSMessagingService; import ninja.egg82.messenger.PacketManager; import ninja.egg82.messenger.RabbitMQMessagingService; import ninja.egg82.messenger.RedisMessagingService; import ninja.egg82.messenger.handler.AbstractServerMessagingHandler; import ninja.egg82.messenger.handler.MessagingHandler; import ninja.egg82.messenger.handler.MessagingHandlerImpl; import ninja.egg82.messenger.packets.AbstractPacket; import ninja.egg82.messenger.packets.MultiPacket; import ninja.egg82.messenger.packets.server.InitializationPacket; import ninja.egg82.messenger.packets.server.KeepAlivePacket; import ninja.egg82.messenger.packets.server.PacketVersionPacket; import ninja.egg82.messenger.packets.server.PacketVersionRequestPacket; import ninja.egg82.messenger.packets.server.ShutdownPacket; import ninja.egg82.messenger.services.PacketService; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @Singleton @DefaultQualifier(NonNull.class) public class MessagingManager { private static final byte protocolVersion = 1; private final Logger logger; private final UUID serverId; private final @MonotonicNonNull ScheduledExecutorService scheduledExecutor; private final @MonotonicNonNull MessagingService messagingService; private volatile @MonotonicNonNull PacketService packetService; @Inject public MessagingManager( final ConfigManager configManager, final CarbonChat carbonChat, final @ServerId UUID serverId, final CarbonServer server, final Logger logger, final UserManagerInternal userManager, final NetworkUsers networkUsers, final WhisperCommand.WhisperHandler whisper, final PacketFactory packetFactory, final PartyInvites partyInvites ) { this.serverId = serverId; this.logger = logger; final boolean proxy = ((CarbonChatInternal) carbonChat).isProxy(); if (proxy || !configManager.primaryConfig().messagingSettings().enabled()) { if (!proxy) { logger.info("Messaging services disabled in config. Cross-server will not work without this!"); } else if (configManager.primaryConfig().messagingSettings().enabled()) { logger.warn("Messaging services enabled in config, but messaging is not supported on proxies. The messaging service is used for the configuration where Carbon is installed on all backends instead of the proxy."); } this.messagingService = null; this.packetService = null; this.scheduledExecutor = null; return; } PacketManager.register(MultiPacket.class, MultiPacket::new); PacketManager.register(KeepAlivePacket.class, KeepAlivePacket::new); PacketManager.register(InitializationPacket.class, InitializationPacket::new); PacketManager.register(PacketVersionPacket.class, PacketVersionPacket::new); PacketManager.register(PacketVersionRequestPacket.class, PacketVersionRequestPacket::new); PacketManager.register(ShutdownPacket.class, ShutdownPacket::new); //PacketManager.register(HeartbeatPacket.class, HeartbeatPacket::new); PacketManager.register(ChatMessagePacket.class, ChatMessagePacket::new); PacketManager.register(SaveCompletedPacket.class, SaveCompletedPacket::new); 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); PacketManager.register(DisbandPartyPacket.class, DisbandPartyPacket::new); this.packetService = new PacketService(4, false, protocolVersion); this.scheduledExecutor = new ExceptionLoggingScheduledThreadPoolExecutor(4, ConcurrentUtil.carbonThreadFactory(logger, "MessagingManager"), logger); 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, partyInvites)); try { this.messagingService = this.initMessagingService( this.packetService, handlerImpl, new File("/"), configManager.primaryConfig().messagingSettings() ); } catch (final IOException | TimeoutException | InterruptedException e) { throw Exceptions.rethrow(e); } this.packetService.addMessenger(this.messagingService); this.packetService.queuePacket(new InitializationPacket(serverId, protocolVersion)); this.packetService.flushQueue(); // Broadcast keepalive packets this.scheduledExecutor.scheduleAtFixedRate(() -> { this.packetService.queuePacket(new KeepAlivePacket(serverId)); this.packetService.flushQueue(); }, 5, 5, TimeUnit.SECONDS); this.scheduledExecutor.scheduleAtFixedRate(() -> { try { this.packetService.flushQueue(); } catch (final IndexOutOfBoundsException ignored) { } }, 0, 250, TimeUnit.MILLISECONDS); } public PacketService requirePacketService() { return Objects.requireNonNull(this.packetService, "packetService"); } private void withPacketService(final Consumer consumer) { if (this.packetService != null) { consumer.accept(this.packetService); } } public void queuePacketAndFlush(final Supplier makePacket) { this.withPacketService(service -> { service.queuePacket(makePacket.get()); service.flushQueue(); }); } public void queuePacket(final Supplier makePacket) { this.withPacketService(service -> service.queuePacket(makePacket.get())); } public void onShutdown() { if (this.scheduledExecutor != null) { ConcurrentUtil.shutdownExecutor(this.scheduledExecutor, TimeUnit.MILLISECONDS, 500); } if (this.packetService != null) { this.packetService.flushQueue(); this.packetService.shutdown(); this.packetService = null; } if (this.messagingService != null) { this.messagingService.close(); } } private MessagingService initMessagingService( final PacketService packetService, final MessagingHandlerImpl handlerImpl, final File packetDir, final MessagingSettings messagingSettings ) throws IOException, TimeoutException, InterruptedException { final String name = "engine1"; final String channelName = "carbon-data"; return switch (messagingSettings.brokerType()) { case RABBITMQ -> { this.logger.info("Initializing RabbitMQ Messaging services..."); final RabbitMQMessagingService.Builder builder = RabbitMQMessagingService.builder(packetService, name, channelName, this.serverId, handlerImpl, 0L, false, packetDir) .url(messagingSettings.url(), messagingSettings.port(), messagingSettings.vhost()) .timeout(5000); if (messagingSettings.username() != null && !messagingSettings.username().isBlank()) { builder.credentials(messagingSettings.username(), messagingSettings.password()); } yield builder.build(); } case NATS -> { this.logger.info("Initializing NATS Messaging services..."); final NATSMessagingService.Builder builder = NATSMessagingService.builder(packetService, name, channelName, this.serverId, handlerImpl, 0L, false, packetDir) .url(messagingSettings.url(), messagingSettings.port()) .life(5000); if (messagingSettings.credentialsFile() != null && !messagingSettings.credentialsFile().isBlank()) { builder.credentials(messagingSettings.credentialsFile()); } yield builder.build(); } case REDIS -> { this.logger.info("Initializing Redis Messaging services..."); final RedisMessagingService.Builder builder = RedisMessagingService.builder(packetService, name, channelName, this.serverId, handlerImpl, 0L, false, packetDir) .url(messagingSettings.url(), messagingSettings.port()); if (messagingSettings.password() != null && !messagingSettings.password().isBlank()) { builder.credentials(messagingSettings.password()); } yield builder.build(); } case NONE -> throw new IllegalStateException("MessagingManager initialized with no messaging broker selected!"); }; } public enum BrokerType { NONE, RABBITMQ, NATS, REDIS, } private static final class CarbonServerHandler extends AbstractServerMessagingHandler { private final CarbonServer server; private final PacketFactory packetFactory; private CarbonServerHandler( final @NonNull CarbonServer server, final @NonNull UUID serverId, final @NonNull PacketService packetService, final @NonNull MessagingHandler messagingHandler, final @NonNull PacketFactory packetFactory ) { super(serverId, packetService, messagingHandler); this.server = server; this.packetFactory = packetFactory; } @Override protected void handleInitialization(final @NonNull InitializationPacket packet) { super.handleInitialization(packet); final List players = this.server.players(); final Map map = new HashMap<>(); for (final CarbonPlayer player : players) { map.put(player.uuid(), player.username()); } this.packetService.queuePacket(this.packetFactory.localPlayersPacket(map)); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/ServerId.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.BindingAnnotation; import com.google.inject.Key; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.UUID; /** * Injection binding annotation for the {@link UUID} identifier of * the currently running Carbon instance for cross-server packet * communications. */ @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) public @interface ServerId { Key KEY = Key.get(UUID.class, ServerId.class); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/CarbonPacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 io.netty.buffer.ByteBuf; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Function; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import ninja.egg82.messenger.packets.AbstractPacket; import org.intellij.lang.annotations.Subst; import org.jetbrains.annotations.NotNull; public abstract class CarbonPacket extends AbstractPacket { private final GsonComponentSerializer componentSerializer = GsonComponentSerializer.gson(); protected CarbonPacket(final @NotNull UUID sender) { super(sender); } protected final void writeComponent(final Component component, final ByteBuf buffer) { this.writeString(this.componentSerializer.serialize(component), buffer); } protected final Component readComponent(final ByteBuf buffer) { return this.componentSerializer.deserialize(this.readString(buffer)); } protected final void writeKey(final Key key, final ByteBuf buffer) { this.writeString(key.asString(), buffer); } protected final Key readKey(final ByteBuf buffer) { final @Subst("carbon:channel") String value = this.readString(buffer); return Key.key(value); } protected final void writeMap( final Map map, final BiConsumer keyWriter, final BiConsumer valueWriter, final ByteBuf buffer ) { this.writeVarInt(map.size(), buffer); for (final Map.Entry entry : map.entrySet()) { keyWriter.accept(entry.getKey(), buffer); valueWriter.accept(entry.getValue(), buffer); } } protected final Map readMap( final ByteBuf buffer, final Function keyReader, final Function valueReader ) { final int size = this.readVarInt(buffer); final Map map = new HashMap<>(); for (int i = 0; i < size; i++) { map.put(keyReader.apply(buffer), valueReader.apply(buffer)); } return map; } protected final > void writeEnum(final E value, final ByteBuf buf) { this.writeVarInt(value.ordinal(), buf); } protected final > E readEnum(final ByteBuf buf, final Class cls) { return cls.getEnumConstants()[this.readVarInt(buf)]; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/ChatMessagePacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 io.netty.buffer.ByteBuf; import java.util.UUID; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import ninja.egg82.messenger.utils.UUIDUtil; import org.jetbrains.annotations.NotNull; public final class ChatMessagePacket extends CarbonPacket { // TODO: store item link placeholder components private UUID userId; private String channelPermission; private Key channelKey; private String username; private Component message; public UUID userId() { return this.userId; } public String channelPermission() { return this.channelPermission; } public Key channelKey() { return this.channelKey; } public String username() { return this.username; } public Component message() { return this.message; } public ChatMessagePacket(final @NotNull UUID sender, final @NotNull ByteBuf data) { super(sender); this.read(data); } public ChatMessagePacket() { super(UUIDUtil.EMPTY_UUID); } public ChatMessagePacket( final @NotNull UUID serverId, final UUID userId, final Key channelKey, final String username, final Component message ) { super(serverId); this.userId = userId; this.channelKey = channelKey; this.username = username; this.message = message; } @Override public void read(final io.netty.buffer.@NotNull ByteBuf buffer) { this.userId = this.readUUID(buffer); this.channelKey = this.readKey(buffer); this.username = this.readString(buffer); this.message = this.readComponent(buffer); } @Override public void write(final io.netty.buffer.@NotNull ByteBuf buffer) { this.writeUUID(this.userId, buffer); this.writeKey(this.channelKey, buffer); this.writeString(this.username, buffer); this.writeComponent(this.message, buffer); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/DisbandPartyPacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 DisbandPartyPacket extends CarbonPacket { private @MonotonicNonNull UUID partyId; @AssistedInject public DisbandPartyPacket( final @ServerId UUID serverId, final @Assisted UUID partyId ) { super(serverId); this.partyId = partyId; } public DisbandPartyPacket(final UUID sender, final ByteBuf data) { super(sender); this.read(data); } public UUID partyId() { return this.partyId; } @Override public void read(final ByteBuf buffer) { this.partyId = this.readUUID(buffer); } @Override public void write(final ByteBuf buffer) { this.writeUUID(this.partyId, buffer); } @Override public String toString() { return "DisbandPartyPacket{" + "partyId=" + this.partyId + ", sender=" + this.sender + '}'; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/InvalidatePartyInvitePacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/LocalPlayerChangePacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public final class LocalPlayerChangePacket extends CarbonPacket { private @MonotonicNonNull UUID playerId; private @MonotonicNonNull String playerName; private @MonotonicNonNull ChangeType changeType; @AssistedInject public LocalPlayerChangePacket( final @ServerId UUID serverId, final @Assisted UUID playerId, final @Assisted @Nullable String playerName, final @Assisted ChangeType changeType ) { super(serverId); if (changeType == ChangeType.ADD && playerName == null) { throw new IllegalArgumentException("playerName cannot be null for ChangeType.ADD"); } this.playerId = playerId; this.playerName = playerName; this.changeType = changeType; } @AssistedInject public LocalPlayerChangePacket(final @ServerId UUID serverId, final @Assisted UUID playerId) { super(serverId); this.playerId = playerId; this.playerName = null; this.changeType = ChangeType.REMOVE; } public LocalPlayerChangePacket(final UUID sender, final ByteBuf data) { super(sender); this.read(data); } public UUID playerId() { return this.playerId; } public String playerName() { return this.playerName; } public ChangeType changeType() { return this.changeType; } @Override public void read(final ByteBuf buffer) { this.playerId = this.readUUID(buffer); final String type = this.readString(buffer); this.changeType = ChangeType.valueOf(type); if (this.changeType == ChangeType.ADD) { this.playerName = this.readString(buffer); } } @Override public void write(final ByteBuf buffer) { this.writeUUID(this.playerId, buffer); this.writeString(this.changeType.name(), buffer); if (this.changeType == ChangeType.ADD) { this.writeString(this.playerName, buffer); } } public enum ChangeType { ADD, REMOVE } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/LocalPlayersPacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 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 LocalPlayersPacket extends CarbonPacket { private @MonotonicNonNull Map players; @AssistedInject public LocalPlayersPacket( final @ServerId UUID serverId, final @Assisted Map players ) { super(serverId); this.players = players; } public LocalPlayersPacket(final UUID sender, final ByteBuf data) { super(sender); this.read(data); } public Map players() { return this.players; } @Override public void read(final ByteBuf buffer) { this.players = this.readMap(buffer, this::readUUID, this::readString); } @Override public void write(final ByteBuf buffer) { this.writeMap(this.players, this::writeUUID, this::writeString, buffer); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/PacketFactory.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 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; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public interface PacketFactory { SaveCompletedPacket saveCompletedPacket(UUID playerId); LocalPlayersPacket localPlayersPacket(Map players); default LocalPlayersPacket clearLocalPlayersPacket() { return this.localPlayersPacket(Map.of()); } LocalPlayerChangePacket localPlayerChangePacket(UUID player, @Nullable String name, LocalPlayerChangePacket.ChangeType type); default LocalPlayerChangePacket addLocalPlayerPacket(final UUID id, final String name) { return this.localPlayerChangePacket(id, name, LocalPlayerChangePacket.ChangeType.ADD); } 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); DisbandPartyPacket disbandParty(UUID party); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyChangePacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 + '}'; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyInvitePacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 + '}'; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/SaveCompletedPacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 SaveCompletedPacket extends CarbonPacket { private @MonotonicNonNull UUID player; @AssistedInject public SaveCompletedPacket(final @ServerId UUID serverId, final @Assisted UUID player) { super(serverId); this.player = player; } public SaveCompletedPacket(final UUID sender, final ByteBuf data) { super(sender); this.read(data); } public UUID playerId() { return this.player; } @Override public void read(final ByteBuf buffer) { this.player = this.readUUID(buffer); } @Override public void write(final ByteBuf buffer) { this.writeUUID(this.player, buffer); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/messaging/packets/WhisperPacket.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.kyori.adventure.text.Component; 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 WhisperPacket extends CarbonPacket { private @MonotonicNonNull UUID from; private @MonotonicNonNull UUID to; private @MonotonicNonNull Component message; @AssistedInject public WhisperPacket( final @ServerId UUID serverId, final @Assisted("from") UUID from, final @Assisted("to") UUID to, final @Assisted Component message ) { super(serverId); this.from = from; this.to = to; this.message = message; } public WhisperPacket(final UUID sender, final ByteBuf data) { super(sender); this.read(data); } public UUID from() { return this.from; } public UUID to() { return this.to; } public Component message() { return this.message; } @Override public void read(final ByteBuf buffer) { this.from = this.readUUID(buffer); this.to = this.readUUID(buffer); this.message = this.readComponent(buffer); } @Override public void write(final ByteBuf buffer) { this.writeUUID(this.from, buffer); this.writeUUID(this.to, buffer); this.writeComponent(this.message, buffer); } @Override public String toString() { return "WhisperPacket{" + "from=" + this.from + ", to=" + this.to + ", message=" + this.message + ", sender=" + this.sender + '}'; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/serialisation/gson/ChatChannelSerializerGson.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.serialisation.gson; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.google.inject.Inject; import java.io.IOException; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.channels.ChatChannel; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.intellij.lang.annotations.Subst; import static net.kyori.adventure.key.Key.key; @DefaultQualifier(NonNull.class) public class ChatChannelSerializerGson extends TypeAdapter { private final ChannelRegistry registry; @Inject public ChatChannelSerializerGson(final ChannelRegistry registry) { this.registry = registry; } @Override public void write(final JsonWriter out, final @Nullable ChatChannel value) throws IOException { if (value == null) { out.value((String) null); } else { out.value(value.key().asString()); } } @Override public @Nullable ChatChannel read(final JsonReader in) throws IOException { @Subst("namespace:value") final @Nullable String channelName = in.nextString(); if (channelName != null) { return this.registry.channel(key(channelName)); } return null; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/serialisation/gson/LocaleSerializerConfigurate.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.serialisation.gson; import com.google.inject.Inject; import java.lang.reflect.Type; import java.util.Locale; import net.kyori.adventure.translation.Translator; 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; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.serialize.SerializationException; import org.spongepowered.configurate.serialize.TypeSerializer; import static java.util.Objects.requireNonNull; @DefaultQualifier(NonNull.class) public class LocaleSerializerConfigurate implements TypeSerializer { private final Logger logger; @Inject public LocaleSerializerConfigurate(final Logger logger) { this.logger = logger; } @Override public Locale deserialize(final Type type, final ConfigurationNode node) { final @Nullable String value = node.getString(); if (value == null) { this.logger.warn("value null for locale! defaulting to en_US"); return Locale.ENGLISH; } return requireNonNull(Translator.parseLocale(value), "value locale cannot be null!"); } @Override public void serialize(final Type type, final @Nullable Locale obj, final ConfigurationNode node) throws SerializationException { if (obj == null) { node.set(null); } else { node.set(obj.toString()); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/serialisation/gson/UUIDSerializerGson.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.serialisation.gson; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.util.UUID; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class UUIDSerializerGson extends TypeAdapter { @Override public void write(final JsonWriter jsonWriter, final @Nullable UUID uuid) throws IOException { if (uuid != null) { jsonWriter.value(uuid.toString()); } else { jsonWriter.value((String) null); } } @Override public UUID read(final JsonReader jsonReader) throws IOException { return UUID.fromString(jsonReader.nextString()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/Backing.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.inject.BindingAnnotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.users.db.DatabaseUserManager; import net.draycia.carbon.common.users.json.JSONUserManager; /** * Injection binding annotation for the backing {@link UserManagerInternal} * (i.e. {@link JSONUserManager} or {@link DatabaseUserManager}), * with the generic type of {@link CarbonPlayerCommon}. * *

Injecting {@link UserManagerInternal} or {@link UserManager} with a generic type of {@literal ?}, without this annotation, * will inject the {@link PlatformUserManager}, which wraps the backing manager (this is generally what you want).

*/ @BindingAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD}) public @interface Backing { } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/CachingUserManager.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.AsyncCache; import com.github.benmanes.caffeine.cache.Cache; 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; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; 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.DisbandPartyPacket; 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; import org.checkerframework.framework.qual.DefaultQualifier; import static net.draycia.carbon.common.users.PlayerUtils.saveExceptionHandler; @DefaultQualifier(NonNull.class) public abstract class CachingUserManager implements UserManagerInternal { private static final int DISBAND_DELAY = 10; protected final Logger logger; protected final ProfileResolver profileResolver; private final ExecutorService executor; 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; private final List queuedDisbands = new CopyOnWriteArrayList<>(); private final Cache recentDisbands = Caffeine.newBuilder() .expireAfterWrite(DISBAND_DELAY + 10, TimeUnit.SECONDS) .build(); protected CachingUserManager( final Logger logger, final ProfileResolver profileResolver, final Injector injector, final Provider messagingManager, 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.injector = injector; this.messagingManager = messagingManager; this.packetFactory = packetFactory; this.server = server; this.cacheLock = new ReentrantLock(); this.cache = new HashMap<>(); } protected abstract CarbonPlayerCommon loadOrCreate(UUID uuid); 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); player.saved(); this.messagingManager.get().queuePacketAndFlush(() -> this.packetFactory.saveCompletedPacket(player.uuid())); }, this.executor); } @Override public Party createParty(final Component name) { throw new UnsupportedOperationException(); } @Override public void saveCompleteMessageReceived(final UUID playerId) { this.cacheLock.lock(); try { this.cache.remove(playerId); } finally { this.cacheLock.unlock(); } } @Override public CompletableFuture saveIfNeeded(final CarbonPlayerCommon player) { if (!player.needsSave()) { return CompletableFuture.completedFuture(null); } return this.save(player); } @Override public CompletableFuture user(final UUID uuid) { this.cacheLock.lock(); try { return this.cache.computeIfAbsent(uuid, $ -> { final CompletableFuture future = CompletableFuture.supplyAsync(() -> { final CarbonPlayerCommon player = this.loadOrCreate(uuid); this.injector.injectMembers(player); if (this instanceof DatabaseUserManager) { player.registerPropertyUpdateListener(() -> this.save(player).exceptionally(saveExceptionHandler(this.logger, player.username, uuid))); } return player; }, this.executor); this.attachPostLoad(uuid, future); return future; }); } finally { this.cacheLock.unlock(); } } @Override public void shutdown() { this.cacheLock.lock(); for (final Runnable task : this.queuedDisbands) { task.run(); } try { final Map> collect = List.copyOf(this.cache.keySet()).stream() .collect(Collectors.toMap(Function.identity(), this::loggedOut)); for (final Map.Entry> entry : collect.entrySet()) { try { entry.getValue().join(); } catch (final Exception ex) { this.logger.warn("Exception saving data for player with uuid '{}'", entry.getKey(), ex); } } ConcurrentUtil.shutdownExecutor(this.executor, TimeUnit.MILLISECONDS, 500); } finally { this.cacheLock.unlock(); } } @Override public CompletableFuture loggedOut(final UUID uuid) { this.messagingManager.get().queuePacket(() -> this.packetFactory.removeLocalPlayerPacket(uuid)); this.cacheLock.lock(); try { final @Nullable CompletableFuture remove = this.cache.remove(uuid); if (remove != null && remove.isDone()) { // don't need to save if it never finished loading final @Nullable CarbonPlayerCommon join = remove.join(); if (join != null) { return this.saveIfNeeded(join); } } return CompletableFuture.completedFuture(null); } finally { this.cacheLock.unlock(); } } @Override public void cleanup() { this.cacheLock.lock(); try { for (final Map.Entry> entry : Map.copyOf(this.cache).entrySet()) { final @Nullable CarbonPlayerCommon getNow = entry.getValue().getNow(null); if (getNow == null || !getNow.transientLoadedNeedsUnload()) { continue; } this.cache.remove(entry.getKey()); this.saveIfNeeded(getNow).exceptionally(saveExceptionHandler(this.logger, getNow.username, getNow.uuid())); } } finally { this.cacheLock.unlock(); } } // Don't keep failed requests, so they can be retried on the next request // The caller is expected to handle the error private void attachPostLoad(final UUID uuid, final CompletableFuture future) { future.whenComplete((result, thr) -> { if (result == null || thr != null) { this.cacheLock.lock(); try { this.cache.remove(uuid); } finally { this.cacheLock.unlock(); } } }); } @Override public CompletableFuture<@Nullable Party> party(final UUID id) { // we delay party deletion for cross-server purposes, so ignore present data when we know it was recently disbanded if (this.recentDisbands.getIfPresent(id) != null) { return CompletableFuture.completedFuture(null); } 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().queuePacketAndFlush(() -> this.packetFactory.partyChange(info.id(), changes)); }, this.executor); } @Override public final void disbandParty(final UUID id) { this.partyCache.synchronous().invalidate(id); final AtomicBoolean ran = new AtomicBoolean(false); final AtomicReference taskRef = new AtomicReference<>(); final Runnable task = () -> { if (ran.compareAndSet(false, true)) { this.disbandSync(id); this.queuedDisbands.remove(taskRef.get()); } }; taskRef.set(task); this.queuedDisbands.add(task); this.recentDisbands.put(id, new Object()); // delay deletion so other servers can post leave events CompletableFuture.delayedExecutor(DISBAND_DELAY, TimeUnit.SECONDS, this.executor).execute(task); this.messagingManager.get().queuePacketAndFlush(() -> this.packetFactory.disbandParty(id)); } @Override public void partyChangeMessageReceived(final PartyChangePacket pkt) { final @Nullable CompletableFuture<@Nullable Party> future = this.partyIfMemberOnline(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); } }); } private @Nullable CompletableFuture<@Nullable Party> partyIfMemberOnline(final UUID partyId) { @Nullable CompletableFuture<@Nullable Party> future = this.partyCache.getIfPresent(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 (partyId.equals(((WrappedCarbonPlayer) player).partyId())) { future = this.party(partyId); } } } return future; } @Override public void disbandPartyMessageReceived(final DisbandPartyPacket pkt) { final @Nullable CompletableFuture<@Nullable Party> future = this.partyIfMemberOnline(pkt.partyId()); this.recentDisbands.put(pkt.partyId(), new Object()); if (future == null) { return; } future.thenAccept(party -> { if (party == null) { return; } ((PartyImpl) party).disbandRaw(); this.partyCache.synchronous().invalidate(pkt.partyId()); }).whenComplete(($, thr) -> { if (thr != null) { this.logger.warn("Exception handling party disband packet {}", pkt, thr); } }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/CarbonPlayerCommon.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.inject.Inject; import java.time.Duration; import java.time.Instant; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; 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; import net.draycia.carbon.common.messages.CarbonMessageRenderer; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; 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 class CarbonPlayerCommon implements CarbonPlayer, ForwardingAudience.Single { private static final long KEEP_TRANSIENT_LOADS_FOR = Duration.ofMinutes(2).toMillis(); private transient @MonotonicNonNull @Inject ChannelRegistry channelRegistry; private transient @MonotonicNonNull @Inject ProfileResolver profileResolver; 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 transient @MonotonicNonNull @Inject CarbonMessages messages; private volatile transient long transientLoadedSince = -1; protected final PersistentUserProperty muted; protected final PersistentUserProperty muteExpiration; protected final PersistentUserProperty deafened; protected final PersistentUserProperty selectedChannel; // All players have these protected transient @MonotonicNonNull String username = null; protected @MonotonicNonNull UUID uuid; // Display information protected final PersistentUserProperty displayName; // Whispers protected final PersistentUserProperty lastWhisperTarget; protected final PersistentUserProperty whisperReplyTarget; protected final PersistentUserProperty ignoringDirectMessages; // Administrative protected final PersistentUserProperty spying; protected final PersistentUserProperty applyOptionalChatFilters; // Punishments protected final PersistentUserProperty> ignoredPlayers; protected final PersistentUserProperty> leftChannels; protected final PersistentUserProperty party; public CarbonPlayerCommon( final boolean muted, final long muteExpiration, final boolean deafened, final @Nullable Key selectedChannel, final @Nullable String username, // will be resolved when requested final UUID uuid, final @Nullable Component displayName, final @Nullable UUID lastWhisperTarget, final @Nullable UUID whisperReplyTarget, final boolean spying, final boolean ignoreDirectMessages, final @Nullable UUID party, final boolean applyOptionalChatFilters ) { this.muted = PersistentUserProperty.of(muted); this.muteExpiration = PersistentUserProperty.of(muteExpiration); this.deafened = PersistentUserProperty.of(deafened); this.selectedChannel = PersistentUserProperty.of(selectedChannel); this.username = username; this.uuid = uuid; this.displayName = PersistentUserProperty.of(displayName); this.lastWhisperTarget = PersistentUserProperty.of(lastWhisperTarget); this.whisperReplyTarget = PersistentUserProperty.of(whisperReplyTarget); this.spying = PersistentUserProperty.of(spying); this.ignoredPlayers = PersistentUserProperty.of(Collections.emptySet()); this.leftChannels = PersistentUserProperty.of(Collections.emptySet()); this.ignoringDirectMessages = PersistentUserProperty.of(ignoreDirectMessages); this.party = PersistentUserProperty.of(party); this.applyOptionalChatFilters = PersistentUserProperty.of(applyOptionalChatFilters); } public CarbonPlayerCommon( final @Nullable String username, // will be resolved when requested final UUID uuid ) { this.muted = PersistentUserProperty.of(false); this.muteExpiration = PersistentUserProperty.of(0L); this.deafened = PersistentUserProperty.of(false); this.selectedChannel = PersistentUserProperty.empty(); this.displayName = PersistentUserProperty.empty(); this.lastWhisperTarget = PersistentUserProperty.empty(); this.whisperReplyTarget = PersistentUserProperty.empty(); this.spying = PersistentUserProperty.of(false); this.ignoredPlayers = PersistentUserProperty.of(Collections.emptySet()); this.leftChannels = PersistentUserProperty.of(Collections.emptySet()); this.username = username; this.uuid = uuid; this.ignoringDirectMessages = PersistentUserProperty.of(false); this.party = PersistentUserProperty.empty(); this.applyOptionalChatFilters = PersistentUserProperty.of(true); } public CarbonPlayerCommon() { this.muted = PersistentUserProperty.of(false); this.muteExpiration = PersistentUserProperty.of(0L); this.deafened = PersistentUserProperty.of(false); this.selectedChannel = PersistentUserProperty.empty(); this.displayName = PersistentUserProperty.empty(); this.lastWhisperTarget = PersistentUserProperty.empty(); this.whisperReplyTarget = PersistentUserProperty.empty(); this.spying = PersistentUserProperty.of(false); this.applyOptionalChatFilters = PersistentUserProperty.of(true); this.ignoredPlayers = PersistentUserProperty.of(Collections.emptySet()); this.leftChannels = PersistentUserProperty.of(Collections.emptySet()); this.ignoringDirectMessages = PersistentUserProperty.of(false); this.party = PersistentUserProperty.empty(); } public boolean needsSave() { return this.properties().anyMatch(PersistentUserProperty::changed); } private Stream> properties() { return Stream.of( this.muted, this.muteExpiration, this.deafened, this.selectedChannel, this.displayName, this.lastWhisperTarget, this.whisperReplyTarget, this.spying, this.applyOptionalChatFilters, this.ignoredPlayers, this.leftChannels, this.ignoringDirectMessages, this.party ); } public void schedule(final Runnable task) { this.scheduler.scheduleForPlayer(this, task); } public void registerPropertyUpdateListener(final Runnable task) { this.properties().forEach(prop -> prop.registerUpdateListener(task)); } @Override public Audience audience() { return Audience.empty(); } @Override public @Nullable Component createItemHoverComponent(final InventorySlot slot) { return null; } @Override public @Nullable Component nickname() { if (!this.config.primaryConfig().nickname().useCarbonNicknames()) { return null; } return this.displayName.orNull(); } public @Nullable Component nicknameRaw() { return this.displayName.orNull(); } @Override public void nickname(final @Nullable Component nickname) { this.displayName.set(nickname); } @Override public boolean hasPermission(final String permission) { throw new UnsupportedOperationException(); } @Override public String primaryGroup() { return "default"; } @Override public List groups() { return List.of("default"); } @Override public boolean muted() { if (this.muted.get()) { if (this.muteExpiration() > 0) { return Instant.now().toEpochMilli() < this.muteExpiration(); } return true; } return false; } @Override public void muted(final boolean muted) { this.muted.set(muted); } @Override public long muteExpiration() { return this.muteExpiration.get(); } @Override public void muteExpiration(final long epochMillis) { this.muteExpiration.set(epochMillis); } @Override public Set ignoring() { return this.ignoredPlayers.get(); } @Override public boolean ignoring(final UUID player) { return this.ignoredPlayers.get().contains(player); } @Override public boolean ignoring(final CarbonPlayer player) { return this.ignoring(player.uuid()); } public void ignoring(final UUID player, final boolean nowIgnoring, final boolean internal) { final Set newIgnored = new HashSet<>(this.ignoredPlayers.get()); if (nowIgnoring) { newIgnored.add(player); } else { newIgnored.remove(player); } if (internal) { this.ignoredPlayers.internalSet(Collections.unmodifiableSet(newIgnored)); } else { this.ignoredPlayers.set(Collections.unmodifiableSet(newIgnored)); } } @Override public void ignoring(final UUID player, final boolean nowIgnoring) { this.ignoring(player, nowIgnoring, false); } @Override public void ignoring(final CarbonPlayer player, final boolean nowIgnoring) { this.ignoring(player.uuid(), nowIgnoring); } @Override public boolean deafened() { return this.deafened.get(); } @Override public void deafened(final boolean deafened) { this.deafened.set(deafened); } @Override public boolean spying() { return this.spying.get(); } @Override public void spying(final boolean spying) { this.spying.set(spying); } @Override public boolean ignoringDirectMessages() { return this.ignoringDirectMessages.get(); } @Override public void ignoringDirectMessages(final boolean ignoring) { this.ignoringDirectMessages.set(ignoring); } @Override public void sendMessageAsPlayer(final String message) { } @Override public boolean online() { return false; } @Override public @Nullable UUID whisperReplyTarget() { return this.whisperReplyTarget.orNull(); } @Override public void whisperReplyTarget(final @Nullable UUID whisperReplyTarget) { this.whisperReplyTarget.set(whisperReplyTarget); } @Override public @Nullable UUID lastWhisperTarget() { return this.lastWhisperTarget.orNull(); } @Override public void lastWhisperTarget(final @Nullable UUID lastWhisperTarget) { this.lastWhisperTarget.set(lastWhisperTarget); } @Override public boolean vanished() { return false; } @Override public boolean awareOf(final CarbonPlayer other) { return true; } @Override public List leftChannels() { return List.copyOf(this.leftChannels.get()); } public void joinChannel(final Key key, final boolean internal) { final Set newKeys = new HashSet<>(this.leftChannels.get()); newKeys.remove(key); if (internal) { this.leftChannels.internalSet(Collections.unmodifiableSet(newKeys)); } else { this.leftChannels.set(Collections.unmodifiableSet(newKeys)); } } @Override public void joinChannel(final ChatChannel channel) { this.joinChannel(channel.key(), false); } public void leaveChannel(final ChatChannel channel, final boolean internal) { final Set newKeys = new HashSet<>(this.leftChannels.get()); newKeys.add(channel.key()); if (internal) { this.leftChannels.internalSet(Collections.unmodifiableSet(newKeys)); } else { this.leftChannels.set(Collections.unmodifiableSet(newKeys)); } } @Override public void leaveChannel(final ChatChannel channel) { this.leaveChannel(channel, false); } @Override public Identity identity() { return Identity.identity(this.uuid); } @Override public @Nullable Locale locale() { return Locale.getDefault(); } @Override public @Nullable ChatChannel selectedChannel() { final @Nullable Key selected = this.selectedChannelKey(); return selected == null ? null : this.channelRegistry.channel(selected); } public ChannelRegistry channelRegistry() { return this.channelRegistry; } public @Nullable Key selectedChannelKey() { return this.selectedChannel.orNull(); } @Override public void selectedChannel(final @Nullable ChatChannel chatChannel) { if (chatChannel == null) { this.selectedChannel.set(null); } else { this.selectedChannel.set(chatChannel.key()); } } @Override public ChannelMessage channelForMessage(final Component message) { throw new UnsupportedOperationException(); } @Override public double distanceSquaredFrom(final CarbonPlayer other) { return -1; } @Override public boolean sameWorldAs(final CarbonPlayer other) { return false; } @Override public String username() { if (this.username == null) { this.username = Objects.requireNonNull( this.profileResolver.resolveName(this.uuid).join(), () -> "Failed to resolve username for player with UUID " + this.uuid + " (null result)" ); } return this.username; } @Override public Component displayName() { throw new UnsupportedOperationException(); } public void username(final String username) { this.username = username; } public void markTransientLoaded(final boolean value) { if (value) { this.transientLoadedSince = System.currentTimeMillis(); } else { this.transientLoadedSince = -1; } } public boolean transientLoadedNeedsUnload() { return this.transientLoadedSince != -1 && System.currentTimeMillis() - this.transientLoadedSince > KEEP_TRANSIENT_LOADS_FOR; } @Override public boolean hasNickname() { if (!this.config.primaryConfig().nickname().useCarbonNicknames()) { return false; } return this.displayName.hasValue(); } public ConfigManager configManager() { return this.config; } public CarbonMessageRenderer messageRenderer() { return this.messageRenderer; } public CarbonMessages carbonMessages() { return this.messages; } @Override public UUID uuid() { return this.uuid; } @Override public boolean equals(final @Nullable Object other) { if (other == null || this.getClass() != other.getClass()) { return false; } return this.uuid.equals(((CarbonPlayerCommon) other).uuid); } @Override public int hashCode() { return this.uuid.hashCode(); } 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()); } @Override public boolean applyOptionalChatFilters() { return this.applyOptionalChatFilters.get(); } @Override public void applyOptionalChatFilters(final boolean applyOptionalChatFilters) { this.applyOptionalChatFilters.set(applyOptionalChatFilters); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/ConsoleCarbonPlayer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.Collections; import java.util.List; 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; import net.kyori.adventure.identity.Identity; 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; import org.jetbrains.annotations.NotNull; @DefaultQualifier(NonNull.class) public class ConsoleCarbonPlayer implements CarbonPlayer, ForwardingAudience.Single { private final Audience audience; public ConsoleCarbonPlayer(final Audience audience) { this.audience = audience; } @Override public @NotNull Audience audience() { return this.audience; } @Override public double distanceSquaredFrom(final CarbonPlayer other) { return 0; } @Override public boolean sameWorldAs(final CarbonPlayer other) { return true; } @Override public String username() { return "Console"; } @Override public Component displayName() { return Component.text(this.username()); } @Override public boolean hasNickname() { return false; } @Override public @Nullable Component nickname() { return null; } @Override public void nickname(final @Nullable Component nickname) { } @Override public UUID uuid() { return new UUID(0, 0); } @Override public @Nullable Component createItemHoverComponent(final InventorySlot slot) { return null; } @Override public @Nullable Locale locale() { return null; } @Override public @Nullable ChatChannel selectedChannel() { return null; } @Override public void selectedChannel(final @Nullable ChatChannel chatChannel) { } @Override public boolean hasPermission(final String permission) { return true; } @Override public String primaryGroup() { return "console_sender"; } @Override public List groups() { return List.of("console_sender"); } @Override public boolean muted() { return false; } @Override public void muted(final boolean muted) { } @Override public long muteExpiration() { return 0; } @Override public void muteExpiration(final long epochMillis) { } @Override public Set ignoring() { return Collections.emptySet(); } @Override public boolean ignoring(final UUID player) { return false; } @Override public boolean ignoring(final CarbonPlayer player) { return false; } @Override public void ignoring(final UUID player, final boolean nowIgnoring) { } @Override public void ignoring(final CarbonPlayer player, final boolean nowIgnoring) { } @Override public boolean deafened() { return false; } @Override public void deafened(final boolean deafened) { } @Override public boolean spying() { return false; } @Override public void spying(final boolean spying) { } @Override public boolean ignoringDirectMessages() { return false; } @Override public void ignoringDirectMessages(final boolean ignoring) { } @Override public void sendMessageAsPlayer(final String message) { } @Override public boolean online() { return true; } @Override public @Nullable UUID whisperReplyTarget() { return null; } @Override public void whisperReplyTarget(final @Nullable UUID uuid) { } @Override public @Nullable UUID lastWhisperTarget() { return null; } @Override public void lastWhisperTarget(final @Nullable UUID uuid) { } @Override public boolean vanished() { return false; } @Override public boolean awareOf(final CarbonPlayer other) { return true; } @Override public List leftChannels() { return List.of(); } @Override public void joinChannel(final ChatChannel channel) { } @Override public void leaveChannel(final ChatChannel channel) { } @Override public CompletableFuture<@Nullable Party> party() { return CompletableFuture.completedFuture(null); } @Override public boolean applyOptionalChatFilters() { return false; } @Override public void applyOptionalChatFilters(final boolean applyOptionalChatFilters) { } @Override public @NotNull Identity identity() { return Identity.nil(); } @Override public ChannelMessage channelForMessage(final Component message) { return new ChannelMessage(message, null); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/MojangProfileResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.TypeAdapter; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.google.inject.Inject; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import net.draycia.carbon.common.util.ConcurrentUtil; import net.draycia.carbon.common.util.FastUuidSansHyphens; 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) public final class MojangProfileResolver implements ProfileResolver { private final HttpClient client; private final Gson gson; private final ExecutorService executorService; private final Map> pendingUuidLookups = new HashMap<>(); private final Map> pendingUsernameLookups = new HashMap<>(); private final ProfileCache cache; private final RateLimiter globalRateLimit; private final RateLimiter uuidToProfileRateLimit; @Inject private MojangProfileResolver(final Logger logger, final ProfileCache cache) { this.client = HttpClient.newHttpClient(); this.gson = new GsonBuilder() .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) .create(); this.executorService = Executors.newFixedThreadPool(2, ConcurrentUtil.carbonThreadFactory(logger, "MojangProfileResolver")); this.cache = cache; this.globalRateLimit = new RateLimiter(600); this.uuidToProfileRateLimit = new RateLimiter(200); } @Override public synchronized CompletableFuture<@Nullable UUID> resolveUUID(final String username, final boolean cacheOnly) { if (username.length() > 25 || username.length() < 1) { // Invalid names return CompletableFuture.completedFuture(null); } if (cacheOnly || this.cache.hasCachedEntry(username)) { return CompletableFuture.completedFuture(this.cache.cachedId(username)); } return this.pendingUuidLookups.computeIfAbsent(username, $ -> { if (!this.globalRateLimit.canSubmit()) { return CompletableFuture.completedFuture(null); } final CompletableFuture<@Nullable BasicLookupResponse> mojangLookup = CompletableFuture.supplyAsync(() -> { try { final HttpRequest request = createRequest( "https://api.mojang.com/users/profiles/minecraft/" + username); return this.sendRequest(request); } catch (final Exception e) { throw new RuntimeException("Exception resolving UUID for name " + username, e); } }, this.executorService); mojangLookup.whenComplete((result, $$$) -> { synchronized (this) { this.cache.cache(result == null ? null : result.id(), username); this.pendingUuidLookups.remove(username); } }); return mojangLookup; }).thenApply(response -> { if (response == null) { return null; } return response.id(); }); } @Override public synchronized CompletableFuture<@Nullable String> resolveName(final UUID uuid, final boolean cacheOnly) { if (cacheOnly || this.cache.hasCachedEntry(uuid)) { return CompletableFuture.completedFuture(this.cache.cachedName(uuid)); } return this.pendingUsernameLookups.computeIfAbsent(uuid, $ -> { final boolean globalLimited = !this.globalRateLimit.canSubmit(); final boolean nameLimited = !this.uuidToProfileRateLimit.canSubmit(); if (globalLimited || nameLimited) { if (nameLimited && !globalLimited) { // Add back to the global limit if we didn't actually make a request due to uuidToProfileRateLimit this.globalRateLimit.available.getAndIncrement(); } return CompletableFuture.completedFuture(null); } final CompletableFuture<@Nullable BasicLookupResponse> mojangLookup = CompletableFuture.supplyAsync(() -> { try { final HttpRequest request = createRequest( "https://api.mojang.com/user/profile/" + uuid.toString().replace("-", "")); return this.sendRequest(request); } catch (final Exception e) { throw new RuntimeException("Exception resolving name for UUID " + uuid, e); } }, this.executorService); mojangLookup.whenComplete((result, $$$) -> { synchronized (this) { this.cache.cache(uuid, result == null ? null : result.name()); this.pendingUsernameLookups.remove(uuid); } }); return mojangLookup; }).thenApply(response -> { if (response == null) { return null; } return response.name(); }); } private static HttpRequest createRequest(final String uri) throws URISyntaxException { return HttpRequest.newBuilder() .uri(new URI(uri)) .GET() .build(); } private @Nullable BasicLookupResponse sendRequest(final HttpRequest request) throws IOException, InterruptedException { final HttpResponse response = this.client.send(request, HttpResponse.BodyHandlers.ofString()); if (response == null) { throw new RuntimeException("Null response for request " + request); } else if (response.statusCode() == 429) { throw new RuntimeException("Got rate-limited by Mojang, could not fulfill request: " + request); } else if (response.statusCode() == 404) { // No such profile return null; } else if (response.statusCode() == 400) { // Invalid name/UUID return null; } else if (response.statusCode() != 200) { throw new RuntimeException("Received non-200 response code (" + response.statusCode() + ") for request " + request + ": " + response.body()); } final BasicLookupResponse basicLookupResponse = this.gson.fromJson(response.body(), new TypeToken() {}.getType()); if (basicLookupResponse == null) { throw new RuntimeException("Malformed response body for request " + request + ": '" + response.body() + "'"); } return basicLookupResponse; } @Override public void shutdown() { ConcurrentUtil.shutdownExecutor(this.executorService, TimeUnit.MILLISECONDS, 500); this.globalRateLimit.shutdown(); this.uuidToProfileRateLimit.shutdown(); } private record BasicLookupResponse(UUID id, String name) { } private static final class UUIDTypeAdapter extends TypeAdapter { private UUIDTypeAdapter() { } @Override public void write(final JsonWriter out, final UUID value) throws IOException { out.value(FastUuidSansHyphens.toString(value)); } @Override public UUID read(final JsonReader in) throws IOException { final String input = in.nextString(); return FastUuidSansHyphens.parseUuid(input); } } private static final class RateLimiter { private final AtomicInteger available; private final Timer timer; private RateLimiter(final int perTenMinutes) { this.timer = new Timer("CarbonChat " + this); this.available = new AtomicInteger(perTenMinutes); this.timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { RateLimiter.this.available.set(perTenMinutes); } }, 0L, Duration.ofMinutes(10).toMillis()); } boolean canSubmit() { return this.available.getAndDecrement() >= 0; } void shutdown() { this.timer.cancel(); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/NetworkUsers.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.inject.Inject; import com.google.inject.Singleton; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Stream; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.command.Commander; import net.draycia.carbon.common.command.PlayerCommander; import net.draycia.carbon.common.command.argument.PlayerSuggestions; import net.draycia.carbon.common.messaging.packets.LocalPlayerChangePacket; 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; import org.incendo.cloud.context.CommandContext; import org.incendo.cloud.context.CommandInput; import org.incendo.cloud.suggestion.Suggestion; /** * Eventually consistent store of who is on each server in the network (besides self). * *

Currently used for username suggestions and whispers.

*/ @DefaultQualifier(NonNull.class) @Singleton public final class NetworkUsers implements PlayerSuggestions { private final CarbonServer server; private final Map> map = new ConcurrentHashMap<>(); private final UserManager userManager; private final ProfileCache profileCache; @Inject private NetworkUsers( final CarbonServer server, final UserManager userManager, final ProfileCache profileCache ) { this.server = server; this.userManager = userManager; this.profileCache = profileCache; } public void handlePacket(final LocalPlayerChangePacket packet) { final Map serverMap = this.map.computeIfAbsent(packet.getSender(), $ -> new ConcurrentHashMap<>()); switch (packet.changeType()) { case ADD -> { serverMap.put(packet.playerId(), packet.playerName()); this.profileCache.cache(packet.playerId(), packet.playerName()); } case REMOVE -> serverMap.remove(packet.playerId()); } this.map.values().removeIf(Map::isEmpty); } public void handlePacket(final LocalPlayersPacket packet) { if (packet.players().isEmpty()) { this.map.remove(packet.getSender()); } else { final Map serverMap = this.map.computeIfAbsent(packet.getSender(), $ -> new ConcurrentHashMap<>()); serverMap.clear(); serverMap.putAll(packet.players()); packet.players().forEach(this.profileCache::cache); } } // PlayerSuggestions impl @Override public CompletableFuture> suggestionsFuture(final CommandContext ctx, final CommandInput input) { final Commander commander = ctx.sender(); final List local = this.server.players(); if (!(commander instanceof PlayerCommander player)) { return CompletableFuture.completedFuture( Stream.concat(local.stream().map(CarbonPlayer::username), this.map.values().stream().flatMap(m -> m.values().stream())) .distinct() .map(Suggestion::suggestion) .toList() ); } final CarbonPlayer carbonPlayer = player.carbonPlayer(); final List> remotePlayerFutures = this.map.values().stream() .flatMap(m -> m.keySet().stream()) .map(this.userManager::user) .toList(); // collect to ensure we request all futures before waiting final CompletableFuture combinedFuture = CompletableFuture.allOf(remotePlayerFutures.toArray(CompletableFuture[]::new)); try { combinedFuture.get(50, TimeUnit.MILLISECONDS); } catch (final TimeoutException ignore) { } catch (final Exception e) { throw Exceptions.rethrow(e); } final Stream remote = remotePlayerFutures.stream() .map(future -> future.getNow(null)) .filter(Objects::nonNull); return CompletableFuture.completedFuture( Stream.concat(local.stream(), remote) .filter(carbonPlayer::awareOf) .map(CarbonPlayer::username) .distinct() .map(Suggestion::suggestion) .toList() ); } public boolean online(final CarbonPlayer player) { if (player.online()) { return true; } 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)); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/PartyImpl.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.disbandRaw(); this.userManager.disbandParty(this.id); } public void disbandRaw() { this.disbanded = true; this.server.players().stream().filter(p -> this.members.contains(p.uuid())).forEach(p -> ((WrappedCarbonPlayer) p).party(null)); for (final UUID member : this.members) { this.emitLeaveEvent(member); } } 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.emitLeaveEvent(id); this.notifyLeave(id); } private void emitLeaveEvent(final UUID id) { this.events.emit(new PartyLeaveEvent() { @Override public UUID playerId() { return id; } @Override public Party party() { return PartyImpl.this; } }); } 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) -> { if (thr != null) { 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 } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/PartyInvites.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.config.ConfigManager; 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; private final ConfigManager config; @Inject private PartyInvites( final Provider messaging, final PacketFactory packetFactory, final UserManagerInternal users, final Logger logger, final CarbonMessages messages, final ConfigManager config ) { this.messaging = messaging; this.packetFactory = packetFactory; this.users = users; this.logger = logger; this.messages = messages; this.config = config; } 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().queuePacket(() -> this.packetFactory.partyInvite(from, to, party)); } public void invalidateInvite(final UUID from, final UUID to) { this.invalidateInvite_(from, to); this.messaging.get().queuePacket(() -> this.packetFactory.invalidatePartyInvite(from, to)); } 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, $ -> this.makeCache()); } private Cache makeCache() { return Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(this.config.primaryConfig().partyChat().expireInvitesAfterSeconds)).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(), from.join().username(), party.join().name()); } }).whenComplete(($, thr) -> { if (thr != null) { this.logger.warn("Exception handling {}", pkt, thr); } }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/PersistentUserProperty.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.jetbrains.annotations.ApiStatus; @DefaultQualifier(NonNull.class) public final class PersistentUserProperty { private final AtomicReference<@Nullable T> valueReference; private final List updateListeners = new CopyOnWriteArrayList<>(); private volatile boolean changed = false; public PersistentUserProperty(final @Nullable T value) { this.valueReference = new AtomicReference<>(value); } /** * Set the value without setting the changed flag. This is a hack. * * @param value value */ @ApiStatus.Internal public void internalSet(final @Nullable T value) { this.valueReference.set(value); } public void set(final @Nullable T value) { final @Nullable T old = this.valueReference.getAndSet(value); if (Objects.equals(value, old)) { return; } this.changed = true; for (final Runnable updateListener : this.updateListeners) { updateListener.run(); } } public void saved() { this.changed = false; } public void registerUpdateListener(final Runnable runnable) { this.updateListeners.add(runnable); } public T get() { return Objects.requireNonNull(this.valueReference.get(), "value required but not present"); } public boolean hasValue() { return this.valueReference.get() != null; } public @Nullable T orNull() { return this.valueReference.get(); } public boolean changed() { return this.changed; } public static PersistentUserProperty of(final @Nullable T value) { return new PersistentUserProperty<>(value); } public static PersistentUserProperty empty() { return new PersistentUserProperty<>(null); } public static final class Serializer implements JsonSerializer>, JsonDeserializer> { @Override public PersistentUserProperty deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { final Type propType = ((ParameterizedType) typeOfT).getActualTypeArguments()[0]; return new PersistentUserProperty<>(context.deserialize(json, propType)); } @Override public JsonElement serialize(final PersistentUserProperty src, final Type typeOfSrc, final JsonSerializationContext context) { final Type propType = ((ParameterizedType) typeOfSrc).getActualTypeArguments()[0]; return context.serialize(src.orNull(), propType); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/PlatformUserManager.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.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.DisbandPartyPacket; 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 @DefaultQualifier(NonNull.class) public final class PlatformUserManager implements UserManagerInternal { private final UserManagerInternal backingManager; private final PlayerFactory playerFactory; private final Injector injector; @Inject private PlatformUserManager( final @Backing UserManagerInternal backingManager, final PlayerFactory playerFactory, final Injector injector ) { this.backingManager = backingManager; this.playerFactory = playerFactory; this.injector = injector; } @Override public CompletableFuture user(final UUID uuid) { return this.backingManager.user(uuid).thenApply(common -> { final WrappedCarbonPlayer wrapped = this.playerFactory.wrap(common); common.markTransientLoaded(!wrapped.online()); return wrapped; }); } @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(); } @Override public void saveCompleteMessageReceived(final UUID playerId) { this.backingManager.saveCompleteMessageReceived(playerId); } @Override public CompletableFuture saveIfNeeded(final WrappedCarbonPlayer player) { return this.backingManager.saveIfNeeded(player.carbonPlayerCommon()); } @Override public CompletableFuture loggedOut(final UUID uuid) { return this.backingManager.loggedOut(uuid); } @Override 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); } @Override public void disbandPartyMessageReceived(final DisbandPartyPacket pkt) { this.backingManager.disbandPartyMessageReceived(pkt); } public interface PlayerFactory { WrappedCarbonPlayer wrap(CarbonPlayerCommon common); static Module moduleFor(final Class carbonPlayerImpl) { return new FactoryModuleBuilder() .implement(WrappedCarbonPlayer.class, carbonPlayerImpl) .build(PlatformUserManager.PlayerFactory.class); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/PlayerUtils.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; 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) public final class PlayerUtils { private PlayerUtils() { } // return value is mostly useful to check if saves are still running; exceptions are already handled on returned futures @SuppressWarnings("unchecked") public static List> saveLoggedInPlayers( final CarbonServer carbonServer, final UserManagerInternal userManager, final Logger logger ) { return carbonServer.players().stream() .map(player -> PlayerUtils.savePlayer(userManager, (C) player, logger)) .toList(); } private static CompletableFuture savePlayer( final UserManagerInternal userManager, final C player, final Logger logger ) { final var saveResult = userManager.saveIfNeeded(player); // avoid fetching the username if it wasn't populated yet; a bit ugly but works (since userManager is always UserManagerInternal) final @Nullable CarbonPlayerCommon common = player instanceof WrappedCarbonPlayer wrapped ? wrapped.carbonPlayerCommon() : null; if (common == null) { throw new IllegalStateException("Failed to unwrap " + CarbonPlayerCommon.class.getSimpleName() + " from " + player.getClass()); } final @Nullable String username = common.username; return saveResult.exceptionally(saveExceptionHandler(logger, username, player.uuid())); } public static Function joinExceptionHandler(final Logger logger, final String username, final UUID uuid) { return thr -> { logger.warn("Exception handling join for player uuid='{}', username='{}'", uuid, username(username), thr); return null; }; } public static Function saveExceptionHandler(final Logger logger, final @Nullable String username, final UUID uuid) { return thr -> { logger.warn("Exception saving data for player uuid='{}', username='{}'", uuid, username(username), thr); return null; }; } private static String username(final @Nullable String username) { return username == null ? "" : username; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/ProfileCache.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.UUID; import net.draycia.carbon.common.DataDirectory; import net.draycia.carbon.common.serialisation.gson.UUIDSerializerGson; import net.draycia.carbon.common.util.FileUtil; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.loader.AtomicFiles; @DefaultQualifier(NonNull.class) @Singleton public final class ProfileCache { private static final long REMOVE_AFTER = Duration.ofDays(7).toMillis(); private static final long REMOVE_NULL_IDS_AFTER = Duration.ofHours(1).toMillis(); private final Gson gson; private final Path cacheFile; private final Map byId; private final Map byName; private final Set entries; private record CacheEntry(@Nullable UUID uuid, @Nullable String name, long updated) { } @Inject private ProfileCache(final @DataDirectory Path dataDirectory) { this.gson = new GsonBuilder() .registerTypeAdapter(UUID.class, new UUIDSerializerGson()) .create(); this.cacheFile = dataDirectory.resolve("users/profile_cache.json"); this.byId = new HashMap<>(); this.byName = new HashMap<>(); this.entries = new HashSet<>(); this.load(); } public synchronized @Nullable String cachedName(final UUID id) { final @Nullable CacheEntry entry = this.byId.get(id); if (entry == null) { return null; } else if (entry.updated() < cutoff()) { return null; } return entry.name(); } public synchronized @Nullable UUID cachedId(final String name) { final @Nullable CacheEntry entry = this.byName.get(name); if (entry == null) { return null; } else if (entry.updated() < cutoff()) { return null; } return entry.uuid(); } public synchronized boolean hasCachedEntry(final String name) { final @Nullable CacheEntry entry = this.byName.get(name); if (entry == null) { return false; } return entry.updated() >= cutoff(); } public synchronized boolean hasCachedEntry(final UUID uuid) { final @Nullable CacheEntry entry = this.byId.get(uuid); if (entry == null) { return false; } return entry.updated() >= cutoff(); } public synchronized void cache(final @Nullable UUID uuid, final @Nullable String name) { final @Nullable CacheEntry r1 = uuid == null ? null : this.byId.remove(uuid); final @Nullable CacheEntry r2 = name == null ? null : this.byName.remove(name); if (r1 != null) { this.entries.remove(r1); } if (r2 != null) { this.entries.remove(r2); } final CacheEntry entry = new CacheEntry(uuid, name, System.currentTimeMillis()); this.entries.add(entry); if (entry.name() != null) { this.byName.put(entry.name(), entry); } if (entry.uuid() != null) { this.byId.put(entry.uuid(), entry); } } private synchronized void cleanup() { final long cutoff = cutoff(); final long nullIdCutoff = nullIdCutoff(); for (final Iterator iterator = this.entries.iterator(); iterator.hasNext();) { final CacheEntry entry = iterator.next(); if (entry.updated() < cutoff || entry.uuid() == null && entry.updated() < nullIdCutoff) { iterator.remove(); if (entry.uuid() != null) { this.byId.remove(entry.uuid()); } if (entry.name() != null) { this.byName.remove(entry.name()); } } } } private static long nullIdCutoff() { return System.currentTimeMillis() - REMOVE_NULL_IDS_AFTER; } private static long cutoff() { return System.currentTimeMillis() - REMOVE_AFTER; } private synchronized void load() { this.entries.clear(); this.byId.clear(); this.byName.clear(); if (!Files.exists(this.cacheFile)) { return; } try { try (final BufferedReader reader = Files.newBufferedReader(this.cacheFile)) { final Set load = this.gson.fromJson(reader, new TypeToken>() {}.getType()); this.entries.addAll(load); for (final CacheEntry entry : this.entries) { if (entry.name() != null) { this.byName.put(entry.name(), entry); } if (entry.uuid() != null) { this.byId.put(entry.uuid(), entry); } } } } catch (final IOException ex) { throw new RuntimeException("Failed to load cache", ex); } } public synchronized void save() { this.cleanup(); try (final BufferedWriter writer = AtomicFiles.atomicBufferedWriter(FileUtil.mkParentDirs(this.cacheFile), StandardCharsets.UTF_8)) { this.gson.toJson(this.entries, writer); } catch (final IOException ex) { throw new RuntimeException("Failed to save cache", ex); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/ProfileResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.UUID; import java.util.concurrent.CompletableFuture; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public interface ProfileResolver { CompletableFuture<@Nullable UUID> resolveUUID(String username, boolean cacheOnly); default CompletableFuture<@Nullable UUID> resolveUUID(final String username) { return this.resolveUUID(username, false); } CompletableFuture<@Nullable String> resolveName(UUID uuid, boolean cacheOnly); default CompletableFuture<@Nullable String> resolveName(final UUID uuid) { return this.resolveName(uuid, false); } void shutdown(); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/UserManagerInternal.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.UUID; 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.DisbandPartyPacket; import net.draycia.carbon.common.messaging.packets.PartyChangePacket; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public interface UserManagerInternal extends UserManager { void shutdown(); CompletableFuture saveIfNeeded(C player); CompletableFuture loggedOut(UUID uuid); void saveCompleteMessageReceived(UUID playerId); void cleanup(); CompletableFuture saveParty(PartyImpl info); void disbandParty(UUID id); void partyChangeMessageReceived(PartyChangePacket pkt); void disbandPartyMessageReceived(DisbandPartyPacket pkt); } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/WrappedCarbonPlayer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 io.github.miniplaceholders.api.MiniPlaceholders; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; 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.draycia.carbon.common.config.PrimaryConfig; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil; import net.draycia.carbon.common.messages.SourcedAudience; import net.draycia.carbon.common.messages.TagPermissions; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.model.user.User; import net.luckperms.api.util.Tristate; 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 static java.util.Objects.requireNonNullElse; @DefaultQualifier(NonNull.class) public abstract class WrappedCarbonPlayer implements CarbonPlayer { protected final CarbonPlayerCommon carbonPlayerCommon; protected WrappedCarbonPlayer(final CarbonPlayerCommon carbonPlayerCommon) { this.carbonPlayerCommon = carbonPlayerCommon; } public CarbonPlayerCommon carbonPlayerCommon() { return this.carbonPlayerCommon; } public @Nullable User user() { return LuckPermsProvider.get().getUserManager().getUser(this.uuid()); } public Component parseMessageTags(final String message) { final TagResolver.Builder resolver = TagResolver.builder(); if (MiniPlaceholdersUtil.miniPlaceholdersLoaded() && this.hasPermission("carbon.chatplaceholders")) { resolver.resolver(MiniPlaceholders.globalPlaceholders()); resolver.resolver(MiniPlaceholders.audiencePlaceholders()); } return TagPermissions.parseTags(this, TagPermissions.MESSAGE, message, this::hasPermission, resolver); } @Override public boolean awareOf(final CarbonPlayer other) { if (other.vanished()) { return this.hasPermission("carbon.whisper.vanished"); } return true; } @Override public Set ignoring() { return this.carbonPlayerCommon.ignoring(); } @Override public boolean ignoring(final UUID player) { return this.carbonPlayerCommon.ignoring(player); } @Override public boolean ignoring(final CarbonPlayer player) { return this.carbonPlayerCommon.ignoring(player); } @Override public void ignoring(final UUID player, final boolean nowIgnoring) { this.carbonPlayerCommon.ignoring(player, nowIgnoring); } @Override public void ignoring(final CarbonPlayer player, final boolean nowIgnoring) { this.carbonPlayerCommon.ignoring(player, nowIgnoring); } @Override public boolean ignoringDirectMessages() { return this.carbonPlayerCommon.ignoringDirectMessages(); } @Override public void ignoringDirectMessages(final boolean ignoring) { this.carbonPlayerCommon.ignoringDirectMessages(ignoring); } @Override public boolean hasPermission(final String permission) { final @Nullable User user = this.user(); if (user == null) { return false; } final var data = user.getCachedData().getPermissionData(user.getQueryOptions()); return data.checkPermission(permission) == Tristate.TRUE; } @Override public String primaryGroup() { final @Nullable User user = this.user(); if (user == null) { return "default"; } return user.getPrimaryGroup(); } @Override public List groups() { final @Nullable User user = this.user(); if (user == null) { return List.of("default"); } final var groups = new ArrayList(); for (final var group : user.getInheritedGroups(user.getQueryOptions())) { groups.add(group.getName()); } return groups; } @Override public String username() { return this.carbonPlayerCommon.username(); } // take care not to call get(Identity.DISPLAY_NAME) on a CarbonPlayer // from this method - it would result in a stack overflow when pointers // are retrieved from EmptyAudienceWithPointers @Override public Component displayName() { final @Nullable Component nick = this.nickname(); if (nick != null) { final PrimaryConfig.NicknameSettings nicknames = this.carbonPlayerCommon.configManager().primaryConfig().nickname(); if (nicknames.skipFormatWhenNameMatches) { final String plainNick = PlainTextComponentSerializer.plainText().serialize(nick); if (plainNick.equals(this.username())) { return nick; } } try { return this.carbonPlayerCommon.messageRenderer().render( SourcedAudience.of(this, this), nicknames.format, Map.of("username", Tag.preProcessParsed(this.username()), "nickname", Tag.selfClosingInserting(nick)), null, null ); } catch (final StackOverflowError overflow) { throw new RuntimeException("Invalid nickname format '%s'. Makes circular reference to CarbonPlayer#displayName().".formatted(nicknames.format), overflow); } } return this.platformDisplayName().orElseGet(() -> Component.text(this.username())); } protected abstract Optional platformDisplayName(); @Override public boolean hasNickname() { return this.carbonPlayerCommon.hasNickname(); } @Override public @Nullable Component nickname() { return this.carbonPlayerCommon.nickname(); } @Override public void nickname(final @Nullable Component nickname) { this.carbonPlayerCommon.nickname(nickname); } @Override public UUID uuid() { return this.carbonPlayerCommon.uuid(); } @Override public @Nullable Component createItemHoverComponent(final InventorySlot slot) { return this.carbonPlayerCommon.createItemHoverComponent(slot); } @Override public @Nullable Locale locale() { return this.carbonPlayerCommon.locale(); } @Override public ChannelMessage channelForMessage(final Component message) { final String text = PlainTextComponentSerializer.plainText().serialize(message); Component formattedMessage = message; ChatChannel channel = requireNonNullElse(this.selectedChannel(), this.carbonPlayerCommon.channelRegistry().defaultChannel()); for (final Key channelKey : this.carbonPlayerCommon.channelRegistry().keys()) { final ChatChannel chatChannel = this.carbonPlayerCommon.channelRegistry().channelOrThrow(channelKey); final @Nullable String prefix = chatChannel.quickPrefix(); if (prefix == null) { continue; } if (text.startsWith(prefix) && chatChannel.permissions().speechPermitted(this).permitted()) { channel = chatChannel; formattedMessage = formattedMessage.replaceText(TextReplacementConfig.builder() .once() .matchLiteral(channel.quickPrefix()) .replacement(Component.empty()) .build()); break; } } return new ChannelMessage(formattedMessage, channel); } @Override public @Nullable ChatChannel selectedChannel() { return this.carbonPlayerCommon.selectedChannel(); } @Override public void selectedChannel(final @Nullable ChatChannel chatChannel) { this.carbonPlayerCommon.selectedChannel(chatChannel); } @Override public boolean muted() { return this.carbonPlayerCommon.muted(); } @Override public void muted(final boolean muted) { this.carbonPlayerCommon.muted(muted); } @Override public long muteExpiration() { return this.carbonPlayerCommon.muteExpiration(); } @Override public void muteExpiration(final long epochMillis) { this.carbonPlayerCommon.muteExpiration(epochMillis); } @Override public boolean deafened() { return this.carbonPlayerCommon.deafened(); } @Override public void deafened(final boolean deafened) { this.carbonPlayerCommon.deafened(deafened); } @Override public boolean spying() { if (this.carbonPlayerCommon.spying() && this.carbonPlayerCommon.configManager().primaryConfig().spyPermissionRequired() && this.online() && !this.hasPermission("carbon.spy")) { this.spying(false); if (this.carbonPlayerCommon.configManager().primaryConfig().spyDisabledMessage()) { this.carbonPlayerCommon.carbonMessages().commandSpyDisabled(this); } return false; } return this.carbonPlayerCommon.spying(); } @Override public void spying(final boolean spying) { this.carbonPlayerCommon.spying(spying); } @Override public void sendMessageAsPlayer(final String message) { this.carbonPlayerCommon.sendMessageAsPlayer(message); } @Override public boolean online() { return this.carbonPlayerCommon.online(); } @Override public @Nullable UUID whisperReplyTarget() { return this.carbonPlayerCommon.whisperReplyTarget(); } @Override public void whisperReplyTarget(final @Nullable UUID uuid) { this.carbonPlayerCommon.whisperReplyTarget(uuid); } @Override public @Nullable UUID lastWhisperTarget() { return this.carbonPlayerCommon.lastWhisperTarget(); } @Override public void lastWhisperTarget(final @Nullable UUID uuid) { this.carbonPlayerCommon.lastWhisperTarget(uuid); } @Override public @NotNull Identity identity() { return this.carbonPlayerCommon.identity(); } @Override public boolean vanished() { return this.carbonPlayerCommon.vanished(); } @Override public List leftChannels() { return this.carbonPlayerCommon.leftChannels(); } @Override public void joinChannel(final ChatChannel channel) { this.carbonPlayerCommon.joinChannel(channel); } @Override public void leaveChannel(final ChatChannel channel) { this.carbonPlayerCommon.leaveChannel(channel); } @Override public boolean equals(final @Nullable Object other) { if (other == null || this.getClass() != other.getClass()) { return false; } final WrappedCarbonPlayer that = (WrappedCarbonPlayer) other; return this.carbonPlayerCommon.equals(that.carbonPlayerCommon); } @Override 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); } @Override public boolean applyOptionalChatFilters() { return this.carbonPlayerCommon.applyOptionalChatFilters(); } @Override public void applyOptionalChatFilters(final boolean applyOptionalChatFilters) { this.carbonPlayerCommon.applyOptionalChatFilters(applyOptionalChatFilters); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/DatabaseUserManager.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.inject.Inject; 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; import net.draycia.carbon.common.config.DatabaseSettings; import net.draycia.carbon.common.messaging.MessagingManager; 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; import net.kyori.adventure.key.Key; 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.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.flywaydb.core.Flyway; 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; import org.jdbi.v3.sqlobject.SqlObjectPlugin; @DefaultQualifier(NonNull.class) public final class DatabaseUserManager extends CachingUserManager { private final Jdbi jdbi; private final QueriesLocator locator; private final ChannelRegistry channelRegistry; private final HikariDataSource dataSource; private DatabaseUserManager( final Jdbi jdbi, final HikariDataSource dataSource, final QueriesLocator locator, final Logger logger, final ProfileResolver profileResolver, final Injector injector, final Provider messagingManager, final PacketFactory packetFactory, final ChannelRegistry channelRegistry, final CarbonServer server ) { super( logger, profileResolver, injector, messagingManager, packetFactory, server ); this.jdbi = jdbi; this.dataSource = dataSource; this.locator = locator; this.channelRegistry = channelRegistry; } @Override public CarbonPlayerCommon loadOrCreate(final UUID uuid) { return this.jdbi.withHandle(handle -> { final @Nullable CarbonPlayerCommon carbonPlayerCommon = handle.createQuery(this.locator.query("select-player")) .bind("id", uuid) .mapTo(CarbonPlayerCommon.class) .findOne() .orElse(null); if (carbonPlayerCommon == null) { return new CarbonPlayerCommon(null, uuid); } handle.createQuery(this.locator.query("select-ignores")) .bind("id", uuid) .mapTo(UUID.class) .forEach(ignoredPlayer -> carbonPlayerCommon.ignoring(ignoredPlayer, true, true)); handle.createQuery(this.locator.query("select-leftchannels")) .bind("id", uuid) .mapTo(Key.class) .forEach(channel -> { final @Nullable ChatChannel chatChannel = this.channelRegistry.channel(channel); if (chatChannel == null) { return; } carbonPlayerCommon.leaveChannel(chatChannel, true); }); return carbonPlayerCommon; }); } @Override public void saveSync(final CarbonPlayerCommon player) { this.jdbi.useTransaction(handle -> { final int inserted = this.bindPlayerArguments(handle.createUpdate(this.locator.query("insert-player")), player).execute(); if (inserted != 1) { this.bindPlayerArguments(handle.createUpdate(this.locator.query("update-player")), player).execute(); } handle.createUpdate(this.locator.query("clear-ignores")) .bind("id", player.uuid()) .execute(); handle.createUpdate(this.locator.query("clear-leftchannels")) .bind("id", player.uuid()) .execute(); final Set ignored = player.ignoring(); if (!ignored.isEmpty()) { final PreparedBatch batch = handle.prepareBatch(this.locator.query("save-ignores")); for (final UUID ignoredPlayer : ignored) { batch.bind("id", player.uuid()).bind("ignoredplayer", ignoredPlayer).add(); } batch.execute(); } final List left = player.leftChannels(); if (!left.isEmpty()) { final PreparedBatch batch = handle.prepareBatch(this.locator.query("save-leftchannels")); for (final Key leftChannel : left) { batch.bind("id", player.uuid()).bind("channel", leftChannel).add(); } batch.execute(); } }); } @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(); this.dataSource.close(); } 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.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; } return update.bind("id", player.uuid()) .bind("muted", player.muted()) .bind("muteexpiration", player.muteExpiration()) .bind("deafened", player.deafened()) .bind("selectedchannel", player.selectedChannelKey()) .bind("displayname", nicknameJson) .bind("lastwhispertarget", player.lastWhisperTarget()) .bind("whisperreplytarget", player.whisperReplyTarget()) .bind("spying", player.spying()) .bind("ignoringdms", player.ignoringDirectMessages()) .bind("party", player.partyId()) .bind("applycustomfilters", player.applyOptionalChatFilters()); } public static final class Factory { private final ChannelRegistry channelRegistry; private final ConfigManager configManager; private final Logger logger; private final ProfileResolver profileResolver; private final Injector injector; private final Provider messagingManager; private final PacketFactory packetFactory; private final CarbonServer server; @Inject private Factory( final ChannelRegistry channelRegistry, final ConfigManager configManager, final Logger logger, final ProfileResolver profileResolver, final Injector injector, final Provider messagingManager, final PacketFactory packetFactory, final CarbonServer server ) { this.channelRegistry = channelRegistry; this.configManager = configManager; this.logger = logger; this.profileResolver = profileResolver; this.injector = injector; this.messagingManager = messagingManager; this.packetFactory = packetFactory; this.server = server; } public DatabaseUserManager create(final String migrationsLocation, final Consumer configureJdbi) { return this.create(migrationsLocation, configureJdbi, this.configManager.primaryConfig().databaseSettings()); } public DatabaseUserManager create(final String migrationsLocation, final Consumer configureJdbi, final DatabaseSettings databaseSettings) { SQLDrivers.loadFrom(this.getClass().getClassLoader()); final HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(databaseSettings.url()); hikariConfig.setUsername(databaseSettings.username()); hikariConfig.setPassword(databaseSettings.password()); hikariConfig.setPoolName("CarbonChat-HikariPool"); hikariConfig.setThreadFactory(ConcurrentUtil.carbonThreadFactory(this.logger, "HikariPool")); final DatabaseSettings.ConnectionPool cfg = Objects.requireNonNull(this.configManager.primaryConfig().databaseSettings().connectionPool()); hikariConfig.setMaximumPoolSize(cfg.maximumPoolSize); hikariConfig.setMinimumIdle(cfg.minimumIdle); hikariConfig.setMaxLifetime(cfg.maximumLifetime); hikariConfig.setKeepaliveTime(cfg.keepaliveTime); hikariConfig.setConnectionTimeout(cfg.connectionTimeout); final HikariDataSource dataSource = new HikariDataSource(hikariConfig); final Flyway flyway = Flyway.configure(CarbonChat.class.getClassLoader()) .baselineVersion("0") .baselineOnMigrate(true) .locations(migrationsLocation) .dataSource(dataSource) .validateMigrationNaming(true) .validateOnMigrate(true) .load(); LogFactory.setLogCreator(new CarbonLogCreator(this.logger)); this.logger.info("Executing Flyway database migrations..."); flyway.repair(); flyway.migrate(); LogFactory.setLogCreator(null); final Jdbi jdbi = Jdbi.create(dataSource) .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()); configureJdbi.accept(jdbi); return new DatabaseUserManager( jdbi, dataSource, new QueriesLocator(this.configManager.primaryConfig().storageType()), this.logger, this.profileResolver, this.injector, this.messagingManager, this.packetFactory, this.channelRegistry, this.server ); } } private record CarbonLogCreator(Logger logger) implements LogCreator { @Override public Log createLogger(final Class clazz) { final Logger l = this.logger; return new Log() { @Override public boolean isDebugEnabled() { return true; } @Override public void debug(final String message) { l.debug(" [{}] {}", clazz.getSimpleName(), message); } @Override public void info(final String message) { l.info(" [{}] {}", clazz.getSimpleName(), message); } @Override public void warn(final String message) { l.warn(" [{}] {}", clazz.getSimpleName(), message); } @Override public void error(final String message) { l.error(" [{}] {}", clazz.getSimpleName(), message); } @Override public void error(final String message, final Exception e) { l.error(" [{}] {}", clazz.getSimpleName(), message, e); } @Override public void notice(final String message) { l.info(" [{}] (Notice) {}", clazz.getSimpleName(), message); } }; } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/QueriesLocator.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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; import com.google.common.base.Splitter; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.common.config.PrimaryConfig; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.jdbi.v3.core.locator.ClasspathSqlLocator; import org.jdbi.v3.core.locator.internal.ClasspathBuilder; @DefaultQualifier(NonNull.class) public final class QueriesLocator { private static final String PREFIX = "queries/"; private static final Splitter SPLITTER = Splitter.on(';'); private final ClasspathSqlLocator locator = ClasspathSqlLocator.create(); private final PrimaryConfig.StorageType storageType; private final Pattern templatePattern = Pattern.compile("\\{([^}]*?)}"); private final Map cache = new ConcurrentHashMap<>(); public QueriesLocator(final PrimaryConfig.StorageType storageType) { this.storageType = storageType; } public List queries(final String name) { return SPLITTER.splitToList(this.query(name)); } public String query(final String name) { return this.locate(PREFIX + name); } private String locate(final String name) { return this.cache.computeIfAbsent(name, $ -> { final String sql = this.locator.getResource( CarbonChat.class.getClassLoader(), new ClasspathBuilder() .appendDotPath(name) .setExtension("sql") .build()); return this.processTemplates(sql); }); } private String processTemplates(final String sql) { return this.templatePattern.matcher(sql).replaceAll(match -> { final String insideBraces = match.group(1); try { final int colonIndex = insideBraces.indexOf(':'); String prefix = insideBraces.substring(0, colonIndex); final String content = insideBraces.substring(colonIndex + 1); boolean not = false; if (prefix.startsWith("!")) { not = true; prefix = prefix.substring(1); } final PrimaryConfig.StorageType storageType = PrimaryConfig.StorageType.valueOf(prefix); if (not) { return storageType != this.storageType ? content : ""; } else { return storageType == this.storageType ? content : ""; } } catch (final Exception ex) { return match.group(0); } }); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/argument/BinaryUUIDArgumentFactory.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.argument; import java.sql.Types; import java.util.UUID; import org.jdbi.v3.core.argument.AbstractArgumentFactory; import org.jdbi.v3.core.argument.Argument; import org.jdbi.v3.core.config.ConfigRegistry; public final class BinaryUUIDArgumentFactory extends AbstractArgumentFactory { public BinaryUUIDArgumentFactory() { super(Types.BINARY); // BINARY(16) } @Override public Argument build(final UUID value, final ConfigRegistry config) { return (position, statement, ctx) -> statement.setBytes(position, unhex(value.toString().replace("-", ""))); } private static byte[] unhex(final String s) { final int len = s.length(); final byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); } return data; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/argument/ComponentArgumentFactory.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.argument; import java.sql.Types; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.jdbi.v3.core.argument.AbstractArgumentFactory; import org.jdbi.v3.core.argument.Argument; import org.jdbi.v3.core.config.ConfigRegistry; public final class ComponentArgumentFactory extends AbstractArgumentFactory { public ComponentArgumentFactory() { super(Types.VARCHAR); } @Override public Argument build(final Component value, final ConfigRegistry config) { return (position, statement, ctx) -> statement.setString(position, GsonComponentSerializer.gson().serialize(value)); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/argument/KeyArgumentFactory.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.argument; import java.sql.Types; import net.kyori.adventure.key.Key; import org.jdbi.v3.core.argument.AbstractArgumentFactory; import org.jdbi.v3.core.argument.Argument; import org.jdbi.v3.core.config.ConfigRegistry; public final class KeyArgumentFactory extends AbstractArgumentFactory { public KeyArgumentFactory() { super(Types.VARCHAR); } @Override public Argument build(final Key value, final ConfigRegistry config) { return (position, statement, ctx) -> statement.setString(position, value.toString()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/mapper/BinaryUUIDColumnMapper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.util.FastUuidSansHyphens; import net.draycia.carbon.common.util.Strings; import org.checkerframework.checker.nullness.qual.Nullable; import org.jdbi.v3.core.mapper.ColumnMapper; import org.jdbi.v3.core.statement.StatementContext; public final class BinaryUUIDColumnMapper implements ColumnMapper { @Override public UUID map(final ResultSet rs, final int columnNumber, final StatementContext ctx) throws SQLException { final byte @Nullable [] bytes = rs.getBytes(columnNumber); if (bytes != null) { return FastUuidSansHyphens.parseUuid(Strings.asHexString(bytes)); } return null; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/mapper/ComponentColumnMapper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.draycia.carbon.common.util.Strings; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.jdbi.v3.core.mapper.ColumnMapper; import org.jdbi.v3.core.statement.StatementContext; public final class ComponentColumnMapper implements ColumnMapper { @Override public Component map(final ResultSet rs, final int columnNumber, final StatementContext ctx) throws SQLException { return GsonComponentSerializer.gson().deserializeOrNull(Strings.trim(rs.getString(columnNumber))); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/mapper/KeyColumnMapper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.draycia.carbon.common.util.Strings; import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.Nullable; import org.intellij.lang.annotations.Subst; import org.jdbi.v3.core.mapper.ColumnMapper; import org.jdbi.v3.core.statement.StatementContext; public final class KeyColumnMapper implements ColumnMapper { @Override public Key map(final ResultSet rs, final int columnNumber, final StatementContext ctx) throws SQLException { final @Nullable @Subst("key:value") String keyValue = Strings.trim(rs.getString(columnNumber)); if (keyValue != null) { return Key.key(keyValue); } return null; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/mapper/NativeUUIDColumnMapper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 org.jdbi.v3.core.mapper.ColumnMapper; import org.jdbi.v3.core.statement.StatementContext; // todo: I'm not entirely sure that this is necessary, but I don't feel like testing PSQL public final class NativeUUIDColumnMapper implements ColumnMapper { @Override public UUID map(final ResultSet rs, final int columnNumber, final StatementContext ctx) throws SQLException { return rs.getObject(columnNumber, UUID.class); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/mapper/PartyRowMapper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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) ); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/db/mapper/PlayerRowMapper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.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 public CarbonPlayerCommon map(final ResultSet rs, final StatementContext ctx) throws SQLException { final ColumnMapper uuid = ctx.findColumnMapperFor(UUID.class).orElseThrow(); return new CarbonPlayerCommon( rs.getBoolean("muted"), rs.getLong("muteexpiration"), rs.getBoolean("deafened"), ctx.findColumnMapperFor(Key.class).orElseThrow().map(rs, "selectedchannel", ctx), null, uuid.map(rs, "id", ctx), ctx.findColumnMapperFor(Component.class).orElseThrow().map(rs, "displayname", ctx), uuid.map(rs, "lastwhispertarget", ctx), uuid.map(rs, "whisperreplytarget", ctx), rs.getBoolean("spying"), rs.getBoolean("ignoringdms"), uuid.map(rs, "party", ctx), rs.getBoolean("applycustomfilters") ); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/users/json/JSONUserManager.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.json; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.inject.Inject; 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; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.messaging.packets.PacketFactory; import net.draycia.carbon.common.serialisation.gson.ChatChannelSerializerGson; 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; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class JSONUserManager extends CachingUserManager { private final Gson serializer; private final Path userDirectory; private final Path partyDirectory; private final ChannelRegistry channelRegistry; @Inject public JSONUserManager( final @DataDirectory Path dataDirectory, final Logger logger, final ProfileResolver profileResolver, final Injector injector, final ChatChannelSerializerGson channelSerializer, final UUIDSerializerGson uuidSerializer, final Provider messagingManager, final PacketFactory packetFactory, final CarbonChannelRegistry channelRegistry, final CarbonServer server ) throws IOException { super( logger, profileResolver, injector, messagingManager, 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()) .registerTypeAdapter(ChatChannel.class, channelSerializer) .registerTypeAdapter(UUID.class, uuidSerializer) .registerTypeAdapter(PersistentUserProperty.class, new PersistentUserProperty.Serializer()) .setPrettyPrinting() .create(); } @Override protected CarbonPlayerCommon loadOrCreate(final UUID uuid) { final Path userFile = this.userFile(uuid); if (Files.exists(userFile)) { try { final @Nullable CarbonPlayerCommon player; try (final Reader reader = Files.newBufferedReader(userFile)) { player = this.serializer.fromJson(reader, CarbonPlayerCommon.class); } if (player == null) { throw new IllegalStateException("Player file found but was empty."); } player.leftChannels().forEach(channel -> { if (this.channelRegistry.channel(channel) == null) { player.joinChannel(channel, true); } }); return player; } catch (final IOException exception) { throw new RuntimeException(exception); } } return new CarbonPlayerCommon(null, uuid); } 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()); try { final String json = this.serializer.toJson(player); if (json == null || json.isBlank()) { throw new IllegalStateException("No data to save - toJson returned null or blank."); } Files.writeString(FileUtil.mkParentDirs(userFile), json); } catch (final IOException exception) { throw new RuntimeException("Exception while saving data for player [%s]".formatted(player.username()), exception); } } @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); } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/CarbonDependencies.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.nio.file.Path; import java.util.Set; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import xyz.jpenilla.gremlin.runtime.DependencyCache; import xyz.jpenilla.gremlin.runtime.DependencyResolver; import xyz.jpenilla.gremlin.runtime.DependencySet; import xyz.jpenilla.gremlin.runtime.logging.Slf4jGremlinLogger; @DefaultQualifier(NonNull.class) public final class CarbonDependencies { private CarbonDependencies() { } public static Set resolve(final Path cacheDir) { final DependencySet deps = DependencySet.readFromClasspathResource( CarbonDependencies.class.getClassLoader(), "carbon-dependencies.txt"); final DependencyCache cache = new DependencyCache(cacheDir); final Logger logger = LoggerFactory.getLogger(CarbonDependencies.class.getSimpleName()); final Set files; try (final DependencyResolver downloader = new DependencyResolver(new Slf4jGremlinLogger(logger))) { files = downloader.resolve(deps, cache).jarFiles(); } cache.cleanup(); return files; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/ChannelUtils.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.draycia.carbon.api.CarbonChatProvider; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.users.CarbonPlayer; import net.kyori.adventure.text.Component; public final class ChannelUtils { private ChannelUtils() { } public static void broadcastMessageToChannel(final Component msg, final ChatChannel channel) { // TODO: Emit events for (final CarbonPlayer recipient : CarbonChatProvider.carbonChat().server().players()) { if (channel.permissions().hearingPermitted(recipient).permitted() && !recipient.leftChannels().contains(channel.key())) { recipient.sendMessage(msg); } } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/CloudUtils.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 com.google.inject.Provider; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import net.draycia.carbon.api.users.CarbonPlayer; 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.command.exception.CommandCompleted; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.util.ComponentMessageThrowable; 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; import org.incendo.cloud.CommandManager; import org.incendo.cloud.exception.ArgumentParseException; import org.incendo.cloud.exception.CommandExecutionException; import org.incendo.cloud.exception.InvalidCommandSenderException; import org.incendo.cloud.exception.InvalidSyntaxException; import org.incendo.cloud.exception.NoPermissionException; import org.incendo.cloud.util.TypeUtils; import static org.incendo.cloud.exception.handling.ExceptionHandler.unwrappingHandler; @DefaultQualifier(NonNull.class) public final class CloudUtils { private static final Component NULL = Component.text("null"); private static final Pattern SPECIAL_CHARACTERS_PATTERN = Pattern.compile("[^\\s\\w\\-]"); @Inject private static Provider> commands; private CloudUtils() { } public static Map defaultCommandSettings() { final Map settings = new HashMap<>(); for (final var command : commands.get()) { settings.put(command.key(), command.defaultCommandSettings()); } return settings; } public static void registerCommands(final Set commands, final Map settings) { for (final var command : commands) { command.commandSettings(settings.get(command.key())); if (command.commandSettings().enabled()) { command.init(); } } } public static Component message(final Throwable throwable) { final @Nullable Component msg = ComponentMessageThrowable.getOrConvertMessage(throwable); return msg == null ? NULL : msg; } public static void decorateCommandManager( final CommandManager commandManager, final CarbonMessages carbonMessages, final Logger logger ) { registerExceptionHandlers(commandManager, carbonMessages, logger); } public static void registerExceptionHandlers( final CommandManager commandManager, final CarbonMessages carbonMessages, final Logger logger ) { commandManager.exceptionController() .registerHandler(ArgumentParseException.class, ctx -> carbonMessages.errorCommandArgumentParsing(ctx.context().sender(), CloudUtils.message(ctx.exception().getCause()))) .registerHandler(InvalidCommandSenderException.class, ctx -> { final Set types = ctx.exception().requiredSenderTypes(); if (types.size() != 1) { throw new IllegalStateException(); } carbonMessages.errorCommandInvalidSender(ctx.context().sender(), TypeUtils.simpleName(types.iterator().next())); }) .registerHandler(InvalidSyntaxException.class, ctx -> carbonMessages.errorCommandInvalidSyntax(ctx.context().sender(), Component.text(ctx.exception().correctSyntax()).replaceText( config -> config.match(SPECIAL_CHARACTERS_PATTERN) .replacement(match -> match.color(NamedTextColor.WHITE))))) .registerHandler(NoPermissionException.class, ctx -> carbonMessages.errorCommandNoPermission(ctx.context().sender())) .registerHandler(CommandExecutionException.class, ctx -> { final Throwable cause = ctx.exception().getCause(); logger.warn("Unexpected exception executing command", cause); final StringWriter writer = new StringWriter(); cause.printStackTrace(new PrintWriter(writer)); final String stackTrace = writer.toString().replaceAll("\t", " "); final @Nullable Component throwableMessage = CloudUtils.message(cause); carbonMessages.errorCommandCommandExecution(ctx.context().sender(), throwableMessage, stackTrace); }) .registerHandler(CommandExecutionException.class, unwrappingHandler(CommandCompleted.class)) .registerHandler(CommandCompleted.class, ctx -> { final @Nullable Component msg = ctx.exception().componentMessage(); if (msg != null) { ctx.context().sender().sendMessage(msg); } }); } public static CarbonPlayer nonPlayerMustProvidePlayer(final CarbonMessages messages, final Commander commander) { if (commander instanceof PlayerCommander playerCommander) { return playerCommander.carbonPlayer(); } throw CommandCompleted.withMessage(messages.commandNeedsPlayer()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/ColorUtils.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.regex.Pattern; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyFormat; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; /** * Basic color related utilities. * * @since 1.0.0 */ @DefaultQualifier(NonNull.class) public final class ColorUtils { private static final Pattern spigotLegacyRGB = Pattern.compile("[§&]x[§&]([0-9a-fA-F])[§&]([0-9a-fA-F])[§&]([0-9a-fA-F])[§&]([0-9a-fA-F])[§&]([0-9a-fA-F])[§&]([0-9a-fA-F])"); private static final Pattern pluginRGB = Pattern.compile("[§&]#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])"); private static final String hexReplacement = "<#$1$2$3$4$5$6>"; private ColorUtils() { } /** * Parses the input into a color.
* Supports named colors, legacy, and hex inputs. * * @param input the color input * @return the color * @since 1.0.0 */ public static @Nullable TextColor parseColor(String input) { if (input.isEmpty()) { return NamedTextColor.WHITE; } for (final NamedTextColor namedColor : NamedTextColor.NAMES.values()) { if (namedColor.toString().equalsIgnoreCase(input)) { return namedColor; } } if (input.contains("&") || input.contains("§")) { input = input.replace("&", "§"); return LegacyComponentSerializer.legacySection().deserialize(input).color(); } return TextColor.fromCSSHexString(input); } /** * Converts the input legacy, legacy rgb, and alternate color formats to MiniMessage color tags. * * @param input the message to convert * @return the converted message * @since 1.0.0 */ public static String legacyToMiniMessage(final String input) { String output = input; // Legacy RGB output = spigotLegacyRGB.matcher(output).replaceAll(hexReplacement); // Alternate RGB, TAB (neznamy) && KiteBoard output = pluginRGB.matcher(output).replaceAll(hexReplacement); // Legacy Colors for (final char c : "0123456789abcdefABCDEF".toCharArray()) { final @Nullable LegacyFormat format = LegacyComponentSerializer.parseChar(Character.toLowerCase(c)); if (format != null) { final @Nullable TextColor color = format.color(); if (color != null) { output = output.replaceAll("[§&]" + c, "<" + color.asHexString() + ">"); } } } // Legacy Formatting for (final char c : "klmnoKLMNO".toCharArray()) { final @Nullable LegacyFormat format = LegacyComponentSerializer.parseChar(Character.toLowerCase(c)); if (format != null) { final @Nullable TextDecoration decoration = format.decoration(); if (decoration != null) { output = output.replaceAll("[§&]" + c, "<" + decoration.name() + ">"); } } } return output; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/ConcurrentUtil.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.common.util.concurrent.ThreadFactoryBuilder; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public final class ConcurrentUtil { private ConcurrentUtil() { } public static void shutdownExecutor(final ExecutorService service, final TimeUnit timeoutUnit, final long timeoutLength) { service.shutdown(); boolean didShutdown; try { didShutdown = service.awaitTermination(timeoutLength, timeoutUnit); } catch (final InterruptedException ignore) { didShutdown = false; } if (!didShutdown) { service.shutdownNow(); } } public static ThreadFactory carbonThreadFactory(final Logger logger, final String name) { return new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("CarbonChat " + name + " Thread #%d") .setUncaughtExceptionHandler((thread, thr) -> logger.warn("Uncaught exception on thread {}", thread.getName(), thr)) .build(); } public static ScheduledExecutorService createPeriodicTasksPool(final Logger logger) { return new ExceptionLoggingScheduledThreadPoolExecutor( 1, carbonThreadFactory(logger, "Periodic Tasks"), logger ); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/DiscordRecipient.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.kyori.adventure.audience.Audience; public final class DiscordRecipient implements Audience { public static final DiscordRecipient INSTANCE = new DiscordRecipient(); private DiscordRecipient() { } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/EmptyAudienceWithPointers.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 net.draycia.carbon.api.users.CarbonPlayer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.pointer.Pointers; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public final class EmptyAudienceWithPointers implements ForwardingAudience.Single { private final Pointers pointers; private EmptyAudienceWithPointers(final Pointers pointers) { this.pointers = pointers; } @Override public Audience audience() { return Audience.empty(); } @Override public Pointers pointers() { return this.pointers; } public static EmptyAudienceWithPointers forCarbonPlayer(final CarbonPlayer player) { return new EmptyAudienceWithPointers(Pointers.builder() .withStatic(Identity.UUID, player.uuid()) .withStatic(Identity.NAME, player.username()) .withDynamic(Identity.DISPLAY_NAME, player::displayName) .build()); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/ExceptionLoggingScheduledThreadPoolExecutor.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.concurrent.Callable; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public final class ExceptionLoggingScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { private final Logger logger; public ExceptionLoggingScheduledThreadPoolExecutor(final int corePoolSize, final ThreadFactory threadFactory, final Logger logger) { super(corePoolSize, threadFactory); this.logger = logger; } @Override public ScheduledFuture schedule(final Runnable command, final long delay, final TimeUnit unit) { return super.schedule(new ExceptionLoggingRunnable(command, this.logger), delay, unit); } @Override public ScheduledFuture schedule(final Callable callable, final long delay, final TimeUnit unit) { throw new UnsupportedOperationException(); } @Override public ScheduledFuture scheduleAtFixedRate(final Runnable command, final long initialDelay, final long period, final TimeUnit unit) { return super.scheduleAtFixedRate(new ExceptionLoggingRunnable(command, this.logger), initialDelay, period, unit); } @Override public ScheduledFuture scheduleWithFixedDelay(final Runnable command, final long initialDelay, final long delay, final TimeUnit unit) { return super.scheduleWithFixedDelay(new ExceptionLoggingRunnable(command, this.logger), initialDelay, delay, unit); } private record ExceptionLoggingRunnable(Runnable wrapped, Logger logger) implements Runnable { @Override public void run() { try { this.wrapped.run(); } catch (final Throwable thr) { this.logger.error("Error executing task '{}'", this.wrapped, thr); Exceptions.rethrow(thr); } } } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/Exceptions.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.function.Consumer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public final class Exceptions { private Exceptions() { } @SuppressWarnings("unchecked") public static RuntimeException rethrow(final Throwable t) throws X { throw (X) t; } public static Consumer sneaky(final CheckedConsumer consumer) { return t -> { try { consumer.accept(t); } catch (final Throwable thr) { rethrow(thr); } }; } @FunctionalInterface public interface CheckedConsumer { void accept(T t) throws X; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/FastUuidSansHyphens.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.Arrays; import java.util.UUID; /* * The MIT License (MIT) * * Copyright (c) 2018 Jon Chambers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /** * This is a modified FastUUID implementation. The primary difference is that it does not dash its * UUIDs. As the native Java 9+ UUID.toString() implementation dashes its UUIDs, we use the FastUUID * methods, which ought to be faster than a String.replace(). */ public final class FastUuidSansHyphens { private static final int MOJANG_BROKEN_UUID_LENGTH = 32; private static final char[] HEX_DIGITS = new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; private static final long[] HEX_VALUES = new long[128]; static { Arrays.fill(HEX_VALUES, -1); HEX_VALUES['0'] = 0x0; HEX_VALUES['1'] = 0x1; HEX_VALUES['2'] = 0x2; HEX_VALUES['3'] = 0x3; HEX_VALUES['4'] = 0x4; HEX_VALUES['5'] = 0x5; HEX_VALUES['6'] = 0x6; HEX_VALUES['7'] = 0x7; HEX_VALUES['8'] = 0x8; HEX_VALUES['9'] = 0x9; HEX_VALUES['a'] = 0xa; HEX_VALUES['b'] = 0xb; HEX_VALUES['c'] = 0xc; HEX_VALUES['d'] = 0xd; HEX_VALUES['e'] = 0xe; HEX_VALUES['f'] = 0xf; HEX_VALUES['A'] = 0xa; HEX_VALUES['B'] = 0xb; HEX_VALUES['C'] = 0xc; HEX_VALUES['D'] = 0xd; HEX_VALUES['E'] = 0xe; HEX_VALUES['F'] = 0xf; } private FastUuidSansHyphens() { // A private constructor prevents callers from accidentally instantiating FastUUID instances } /** * Parses a UUID from the given character sequence. The character sequence must represent a * Mojang UUID. * * @param uuidSequence the character sequence from which to parse a UUID * * @return the UUID represented by the given character sequence * * @throws IllegalArgumentException if the given character sequence does not conform to the string * representation of a Mojang UUID. */ public static UUID parseUuid(final CharSequence uuidSequence) { if (uuidSequence.length() != MOJANG_BROKEN_UUID_LENGTH) { throw new IllegalArgumentException("Illegal UUID string: " + uuidSequence); } long mostSignificantBits = hexValueForChar(uuidSequence.charAt(0)) << 60; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(1)) << 56; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(2)) << 52; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(3)) << 48; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(4)) << 44; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(5)) << 40; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(6)) << 36; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(7)) << 32; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(8)) << 28; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(9)) << 24; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(10)) << 20; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(11)) << 16; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(12)) << 12; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(13)) << 8; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(14)) << 4; mostSignificantBits |= hexValueForChar(uuidSequence.charAt(15)); long leastSignificantBits = hexValueForChar(uuidSequence.charAt(16)) << 60; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(17)) << 56; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(18)) << 52; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(19)) << 48; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(20)) << 44; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(21)) << 40; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(22)) << 36; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(23)) << 32; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(24)) << 28; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(25)) << 24; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(26)) << 20; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(27)) << 16; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(28)) << 12; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(29)) << 8; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(30)) << 4; leastSignificantBits |= hexValueForChar(uuidSequence.charAt(31)); return new UUID(mostSignificantBits, leastSignificantBits); } /** * Returns a string representation of the given UUID. The returned string is formatted as a * Mojang-style UUID. * * @param uuid the UUID to represent as a string * * @return a string representation of the given UUID */ public static String toString(final UUID uuid) { final long mostSignificantBits = uuid.getMostSignificantBits(); final long leastSignificantBits = uuid.getLeastSignificantBits(); final char[] uuidChars = new char[MOJANG_BROKEN_UUID_LENGTH]; uuidChars[0] = HEX_DIGITS[(int) ((mostSignificantBits & 0xf000000000000000L) >>> 60)]; uuidChars[1] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0f00000000000000L) >>> 56)]; uuidChars[2] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00f0000000000000L) >>> 52)]; uuidChars[3] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000f000000000000L) >>> 48)]; uuidChars[4] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000f00000000000L) >>> 44)]; uuidChars[5] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000f0000000000L) >>> 40)]; uuidChars[6] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000f000000000L) >>> 36)]; uuidChars[7] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000f00000000L) >>> 32)]; uuidChars[8] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000f0000000L) >>> 28)]; uuidChars[9] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000f000000L) >>> 24)]; uuidChars[10] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000f00000L) >>> 20)]; uuidChars[11] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000f0000L) >>> 16)]; uuidChars[12] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000000f000L) >>> 12)]; uuidChars[13] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000000f00L) >>> 8)]; uuidChars[14] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000000f0L) >>> 4)]; uuidChars[15] = HEX_DIGITS[(int) (mostSignificantBits & 0x000000000000000fL)]; uuidChars[16] = HEX_DIGITS[(int) ((leastSignificantBits & 0xf000000000000000L) >>> 60)]; uuidChars[17] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0f00000000000000L) >>> 56)]; uuidChars[18] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00f0000000000000L) >>> 52)]; uuidChars[19] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000f000000000000L) >>> 48)]; uuidChars[20] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000f00000000000L) >>> 44)]; uuidChars[21] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000f0000000000L) >>> 40)]; uuidChars[22] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000f000000000L) >>> 36)]; uuidChars[23] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000f00000000L) >>> 32)]; uuidChars[24] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000f0000000L) >>> 28)]; uuidChars[25] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000f000000L) >>> 24)]; uuidChars[26] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000f00000L) >>> 20)]; uuidChars[27] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000f0000L) >>> 16)]; uuidChars[28] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000000f000L) >>> 12)]; uuidChars[29] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000000f00L) >>> 8)]; uuidChars[30] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000000f0L) >>> 4)]; uuidChars[31] = HEX_DIGITS[(int) (leastSignificantBits & 0x000000000000000fL)]; return new String(uuidChars); } private static long hexValueForChar(final char c) { try { if (HEX_VALUES[c] < 0) { throw new IllegalArgumentException("Illegal hexadecimal digit: " + c); } } catch (final ArrayIndexOutOfBoundsException e) { throw new IllegalArgumentException("Illegal hexadecimal digit: " + c); } return HEX_VALUES[c]; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/FileUtil.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.common.hash.Hashing; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; public final class FileUtil { private FileUtil() { } /** * Calculates the SHA256 hash of {@code file} and returns it as a hex string. * * @param file file to hash * @return SHA256 hash string * @throws IOException on I/O error * @throws IllegalArgumentException when {@code file} is not a regular file */ public static String hashString(final Path file) throws IOException { if (!Files.isRegularFile(file)) { throw new IllegalArgumentException("Path '%s' is not a regular file, cannot generate hash string.".formatted(file)); } final byte[] hash = com.google.common.io.Files.asByteSource(file.toFile()).hash(Hashing.sha256()).asBytes(); return Strings.asHexString(hash); } /** * Lists directory entries in {@code path}. * *

If {@code path} does not exist, returns an empty list.

* *

If {@code path} exists, but is not a directory, throws {@link IllegalArgumentException}

* * @param path directory * @return directory entries * @throws IllegalArgumentException when {@code path} exists but is not a directory * @throws UncheckedIOException on I/O error */ public static List listDirectoryEntries(final Path path) { return listDirectoryEntries(path, "*"); } /** * Lists directory entries in {@code path} matching {@code glob}. * *

If {@code path} does not exist, returns an empty list.

* *

If {@code path} exists, but is not a directory, throws {@link IllegalArgumentException}

* * @param path directory * @param glob glob pattern * @return matching directory entries * @throws IllegalArgumentException when {@code path} exists but is not a directory * @throws UncheckedIOException on I/O error */ public static List listDirectoryEntries(final Path path, final String glob) { if (!Files.exists(path)) { return List.of(); } else if (!Files.isDirectory(path)) { throw new IllegalArgumentException("Path '%s' exists but is not a directory!".formatted(path)); } try (final DirectoryStream stream = Files.newDirectoryStream(path, glob)) { final List ret = new ArrayList<>(); stream.forEach(ret::add); return ret; } catch (final IOException exception) { throw new UncheckedIOException("Failed to list directory entries matching '%s' in path '%s'.".formatted(glob, path), exception); } } /** * Attempts to create the parent directories of {@code path} if necessary. * *

Returns {@code path} when successful.

* * @param path path * @return {@code path} * @throws IOException on I/O error */ public static Path mkParentDirs(final Path path) throws IOException { final Path parent = path.getParent(); if (parent != null && !Files.isDirectory(parent)) { try { Files.createDirectories(parent); } catch (final FileAlreadyExistsException ex) { if (!Files.isDirectory(parent)) { throw ex; } } } return path; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/Pagination.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.RandomAccess; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import static java.util.Objects.requireNonNull; import static net.kyori.adventure.text.Component.empty; @DefaultQualifier(NonNull.class) public interface Pagination { ComponentLike header(int page, int pages); ComponentLike footer(int page, int pages); ComponentLike pageOutOfRange(int page, int pages); ComponentLike item(T item, boolean lastOfPage); default List render( final Collection content, final int page, final int itemsPerPage ) { if (content.isEmpty()) { throw new IllegalArgumentException("Cannot paginate an empty collection."); } final int pages = (int) Math.ceil(content.size() / (itemsPerPage * 1.00)); if (page < 1 || page > pages) { return Collections.singletonList(this.pageOutOfRange(page, pages).asComponent()); } final List renderedContent = new ArrayList<>(); final Component header = this.header(page, pages).asComponent(); if (header != empty()) { renderedContent.add(header); } final int start = itemsPerPage * (page - 1); final int maxIndex = start + itemsPerPage; if (content instanceof RandomAccess && content instanceof final List contentList) { for (int i = start; i < maxIndex; i++) { if (i > content.size() - 1) { break; } renderedContent.add(this.item(contentList.get(i), i == maxIndex - 1).asComponent()); } } else { final Iterator iterator = content.iterator(); for (int i = 0; i < start && iterator.hasNext(); i++) { iterator.next(); } for (int i = start; i < maxIndex && iterator.hasNext(); ++i) { renderedContent.add(this.item(iterator.next(), i == maxIndex - 1).asComponent()); } } final Component footer = this.footer(page, pages).asComponent(); if (footer != empty()) { renderedContent.add(footer); } return Collections.unmodifiableList(renderedContent); } static Builder builder() { return new Builder<>(); } final class Builder { private BiIntFunction headerRenderer = ($, $$) -> empty(); private BiIntFunction footerRenderer = ($, $$) -> empty(); private @MonotonicNonNull BiIntFunction pageOutOfRangeRenderer = null; private @MonotonicNonNull ItemRenderer itemRenderer = null; private Builder() { } public Builder header(final BiIntFunction headerRenderer) { this.headerRenderer = headerRenderer; return this; } public Builder footer(final BiIntFunction footerRenderer) { this.footerRenderer = footerRenderer; return this; } public Builder pageOutOfRange(final BiIntFunction pageOutOfRangeRenderer) { this.pageOutOfRangeRenderer = pageOutOfRangeRenderer; return this; } public Builder item(final ItemRenderer itemRenderer) { this.itemRenderer = itemRenderer; return this; } public Pagination build() { return new DelegatingPaginationImpl<>( requireNonNull(this.headerRenderer, "Must provide a header renderer!"), requireNonNull(this.footerRenderer, "Must provide a footer renderer!"), requireNonNull(this.pageOutOfRangeRenderer, "Must provide a page out of range renderer!"), requireNonNull(this.itemRenderer, "Must provide an item renderer!") ); } @FunctionalInterface public interface ItemRenderer { ComponentLike render(T item, boolean lastOfPage); } private record DelegatingPaginationImpl( BiIntFunction headerRenderer, BiIntFunction footerRenderer, BiIntFunction pageOutOfRangeRenderer, ItemRenderer itemRenderer ) implements Pagination { @Override public ComponentLike header(final int page, final int pages) { return this.headerRenderer.apply(page, pages); } @Override public ComponentLike footer(final int page, final int pages) { return this.footerRenderer.apply(page, pages); } @Override public ComponentLike pageOutOfRange(final int page, final int pages) { return this.pageOutOfRangeRenderer.apply(page, pages); } @Override public ComponentLike item(final T item, final boolean lastOfPage) { return this.itemRenderer.render(item, lastOfPage); } } } @FunctionalInterface interface BiIntFunction { T apply(int i, int i1); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/PaginationHelper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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(); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/SQLDrivers.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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 java.sql.Driver; import java.util.ServiceLoader; public final class SQLDrivers { private SQLDrivers() { } public static void loadFrom(final ClassLoader loader) { ServiceLoader.load(Driver.class, loader).stream() .forEach(provider -> forceInit(provider.type())); } private static Class forceInit(final Class klass) { try { Class.forName(klass.getName(), true, klass.getClassLoader()); } catch (final ClassNotFoundException e) { throw new AssertionError(e); } return klass; } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/Strings.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.common.base.Suppliers; import java.net.URI; import java.net.URISyntaxException; import java.util.function.Supplier; import java.util.regex.Pattern; import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.event.ClickEvent; 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 Strings { private static final Pattern DEFAULT_URL_PATTERN = Pattern.compile("(?:(https?)://)?([-\\w_.]+\\.\\w{2,})(/([A-Za-z0-9\\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*)?"); private static final Pattern URL_SCHEME_PATTERN = Pattern.compile("^[a-z][a-z0-9+\\-.]*:"); public static final Supplier URL_REPLACEMENT_CONFIG = Suppliers.memoize( () -> TextReplacementConfig.builder() .match(DEFAULT_URL_PATTERN) .replacement(url -> { String clickUrl = url.content(); if (!URL_SCHEME_PATTERN.matcher(clickUrl).find()) { clickUrl = "http://" + clickUrl; } try { final URI ignored = new URI(clickUrl); // just to validate that the uri is valid return url.clickEvent(ClickEvent.openUrl(clickUrl)); } catch (final URISyntaxException ignored) { return url; } }) .build() ); private Strings() { } public static @Nullable String trim(final @Nullable String s) { return s == null ? null : s.trim(); } public static String asHexString(final byte[] bytes) { final StringBuilder sb = new StringBuilder(bytes.length * 2); for (final byte b : bytes) { sb.append("%02x".formatted(b & 0xFF)); } return sb.toString(); } } ================================================ FILE: common/src/main/java/net/draycia/carbon/common/util/UpdateChecker.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.jar.Manifest; 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) public record UpdateChecker(Logger logger) { private static final String GITHUB_REPO = "Hexaoxide/Carbon"; private static final String UPDATE_CHECKER_FETCHING_VERSION_INFORMATION = "Fetching version information..."; private static final String DEV_BUILD_NOTICE = "This is a development version of CarbonChat ()!"; private static final String UPDATE_CHECKER_BEHIND_RELEASES = "CarbonChat is version(s) out of date ()."; private static final String UPDATE_CHECKER_DOWNLOAD_RELEASE = "Download the latest release () from "; private static final String RELEASE_DOWNLOADS_URL = "https://modrinth.com/plugin/carbon/versions"; private static final Gson GSON = new GsonBuilder().create(); public void checkVersion() { this.logger.info(UPDATE_CHECKER_FETCHING_VERSION_INFORMATION); final @Nullable Manifest manifest = manifest(UpdateChecker.class); // we expect to be shaded into platform jars if (manifest == null) { this.logger.warn("Failed to locate manifest, cannot check for updates."); return; } final String currentVersion = manifest.getMainAttributes().getValue("carbon-version"); final Releases releases; try { releases = this.fetchReleases(); } catch (final IOException e) { this.logger.warn("Failed to list releases, cannot check for updates.", e); return; } final String ver = "v" + currentVersion; if (releases.releaseList().get(0).equals(ver)) { return; } if (currentVersion.contains("-SNAPSHOT")) { this.logger.info(DEV_BUILD_NOTICE.replace("", ver)); } else { final int versionsBehind = releases.releaseList().indexOf(ver); this.logger.info( UPDATE_CHECKER_BEHIND_RELEASES .replace("", String.valueOf(versionsBehind == -1 ? "?" : versionsBehind)) .replace("", ver) ); } this.logger.info( UPDATE_CHECKER_DOWNLOAD_RELEASE .replace("", releases.releaseList().get(0)) .replace("", RELEASE_DOWNLOADS_URL) // , releases.releaseUrls().get(releases.releaseList().get(0))) ); } private Releases fetchReleases() throws IOException { final JsonArray result; try (final BufferedReader reader = new BufferedReader(new InputStreamReader(URI.create("https://api.github.com/repos/%s/releases".formatted(GITHUB_REPO)).toURL().openStream(), StandardCharsets.UTF_8))) { result = GSON.fromJson(reader, JsonArray.class); } final Map versionMap = new LinkedHashMap<>(); for (final JsonElement element : result) { versionMap.put( element.getAsJsonObject().get("tag_name").getAsString(), element.getAsJsonObject().get("html_url").getAsString() ); } return new Releases(new ArrayList<>(versionMap.keySet()), versionMap); } private record Releases(List releaseList, Map releaseUrls) { } public static @Nullable Manifest manifest(final Class clazz) { final String classLocation = "/" + clazz.getName().replace(".", "/") + ".class"; final @Nullable URL resource = clazz.getResource(classLocation); if (resource == null) { return null; } final String classFilePath = resource.toString().replace("\\", "/"); final String archivePath = classFilePath.substring(0, classFilePath.length() - classLocation.length()); try (final InputStream stream = URI.create(archivePath + "/META-INF/MANIFEST.MF").toURL().openStream()) { return new Manifest(stream); } catch (final IOException ex) { return null; } } } ================================================ FILE: common/src/main/resources/carbon-permissions.yml ================================================ carbon.clearchat: description: "Clears the chat for all players except those with carbon.chearchat.exempt." children: carbon.clearchat.clear: true carbon.clearchat.clear: "Clears the chat for all players except those with carbon.chearchat.exempt." carbon.clearchat.exempt: "Exempts the player from having their chat cleared when /clearchat is executed." carbon.debug: "Allows the sender to quickly check what carbon think's the player's primary and non-primary groups are." carbon.help: "Shows Carbon's help menu, detailing each part of Carbon's commands." carbon.hideidentity: "Prevents messages from the player from being blocked clientside." carbon.ignore: "Ignores the player, hiding messages they send in chat and in whispers." carbon.ignore.exempt: "Prevents the player from being ignored." carbon.ignore.unignore: "Removes the player from the sender's ignore list." carbon.itemlink: "Shows the player's held or equipped item in chat." carbon.crossserver: "Allows cross server messages to be received by the player." carbon.parties: "Allows the creation and use of chat parties." carbon.parties.ping_sound: "Allows the ping sound to play when receiving party messages." carbon.ping_sounds: "Allows the ping sound to play when receiving pings." carbon.mute: "Mutes the player, preventing them from sending messages or whispers." carbon.mute.exempt: "Prevents the player from being muted." carbon.mute.info: "Shows if the player is muted or now." carbon.mute.notify: "Notifies the player when someone else has been mute." carbon.mute.unmute: "Unmutes the player, allowing them to use chat and send whispers." carbon.nickname: "Checks your nickname." carbon.nickname.set: "Set or remove your nickname." carbon.nickname.others: "Checks other player's nicknames." carbon.nickname.others.set: "Set or remove other player's nicknames." carbon.reload: "Reloads Carbon's config, channel settings, and translations." carbon.whisper: "Sends private messages to other players." carbon.whisper.continue: "Sends a message to the last player you whispered." carbon.whisper.reply: "Sends a message to the last player who messaged you." carbon.whisper.vanished: "Allows the player to send messages to vanished players." carbon.whisper.ping_sounds: "Allows the ping sound to play when receiving whispers." # Nickname tag permissions carbon.nickname.tags.color: "Allows the use of colors in nicknames." carbon.nickname.tags.gradient: "Allows the use of gradients in nicknames." carbon.nickname.tags.rainbow: "Allows the use of the rainbow tag in nicknames." carbon.nickname.tags.decorations: "Allows the use of obfuscated, bold, strikethrough, underlined, and italic in nicknames. If a player has this permission, the specific decoration permissions won't be checked." carbon.nickname.tags.obfuscated: "Allows the use of obfuscated in nicknames." carbon.nickname.tags.bold: "Allows the use of bold in nicknames." carbon.nickname.tags.strikethrough: "Allows the use of strikethrough in nicknames." carbon.nickname.tags.underlined: "Allows the use of underline in nicknames." carbon.nickname.tags.italic: "Allows the use of italics in nicknames." carbon.nickname.tags.hover: "Allows the use of hover events in nicknames." carbon.nickname.tags.click: "Allows the use of click events in nicknames." carbon.nickname.tags.translatable: "Allows the use of translatable components in nicknames." carbon.nickname.tags.keybind: "Allows the use of keybind components in nicknames." carbon.nickname.tags.insertion: "Allows the use of insertions in nicknames." carbon.nickname.tags.font: "Allows the use of fonts in nicknames." carbon.nickname.tags.reset: "Allows the use of the reset tag in nicknames." carbon.nickname.tags.newline: "Allows the use of the newline tag in nicknames." carbon.nickname.tags.pride: "Allows the use of the pride tag in nicknames." carbon.nickname.tags.shadow_color: "Allows the use of the shadow tag in nicknames." carbon.nickname.tags.transition: "Allows the use of the transition tag in nicknames." # Party name tag permissions carbon.parties.name.tags.color: "Allows the use of colors in party names." carbon.parties.name.tags.gradient: "Allows the use of gradients in party names." carbon.parties.name.tags.rainbow: "Allows the use of the rainbow tag in party names." carbon.parties.name.tags.decorations: "Allows the use of obfuscated, bold, strikethrough, underlined, and italic in party names. If a player has this permission, the specific decoration permissions won't be checked." carbon.parties.name.tags.obfuscated: "Allows the use of obfuscated in party names." carbon.parties.name.tags.bold: "Allows the use of bold in party names." carbon.parties.name.tags.strikethrough: "Allows the use of strikethrough in party names." carbon.parties.name.tags.underlined: "Allows the use of underline in party names." carbon.parties.name.tags.italic: "Allows the use of italics in party names." carbon.parties.name.tags.hover: "Allows the use of hover events in party names." carbon.parties.name.tags.click: "Allows the use of click events in party names." carbon.parties.name.tags.translatable: "Allows the use of translatable components in party names." carbon.parties.name.tags.keybind: "Allows the use of keybind components in party names." carbon.parties.name.tags.insertion: "Allows the use of insertions in party names." carbon.parties.name.tags.font: "Allows the use of fonts in party names." carbon.parties.name.tags.reset: "Allows the use of the reset tag in party names." carbon.parties.name.tags.newline: "Allows the use of the newline tag in party names." carbon.parties.name.tags.pride: "Allows the use of the pride tag in party names." carbon.parties.name.tags.shadow_color: "Allows the use of the shadow tag in party names." carbon.parties.name.tags.transition: "Allows the use of the transition tag in party names." # Message tag permissions carbon.messagetags.color: "Allows the use of colors in messages." carbon.messagetags.gradient: "Allows the use of gradients in messages." carbon.messagetags.rainbow: "Allows the use of the rainbow tag in messages." carbon.messagetags.decorations: "Allows the use of obfuscated, bold, strikethrough, underlined, and italic in messages. If a player has this permission, the specific decoration permissions won't be checked." carbon.messagetags.obfuscated: "Allows the use of obfuscated in messages." carbon.messagetags.bold: "Allows the use of bold in messages." carbon.messagetags.strikethrough: "Allows the use of strikethrough in messages." carbon.messagetags.underlined: "Allows the use of underline in messages." carbon.messagetags.italic: "Allows the use of italics in messages." carbon.messagetags.hover: "Allows the use of hover events in messages." carbon.messagetags.click: "Allows the use of click events in messages." carbon.messagetags.translatable: "Allows the use of translatable components in messages." carbon.messagetags.keybind: "Allows the use of keybind components in messages." carbon.messagetags.insertion: "Allows the use of insertions in messages." carbon.messagetags.font: "Allows the use of fonts in messages." carbon.messagetags.reset: "Allows the use of the reset tag in messages." carbon.messagetags.newline: "Allows the use of the newline tag in messages." carbon.messagetags.pride: "Allows the use of the pride tag in messages." carbon.messagetags.shadow_color: "Allows the use of the shadow tag in messages." carbon.messagetags.transition: "Allows the use of the transition tag in messages." ================================================ FILE: common/src/main/resources/locale/messages-de_AT.properties ================================================ channel.change=Du schreibst nun in command.clearchat.description=Löscht den Chat für alle Spieler. command.continue.argument.message=Die zu sendende Nachricht. command.continue.description=Sendet eine Nachricht an die letzte Person, der du geschrieben hast. command.debug.argument.player=Der Spieler, von dem die Gruppen überprüft werden. command.debug.description=Zeigt die Berechtigungsgruppen der Spieler an. command.filter.optional.enabled=Optionaler Chatfilter aktiviert\! command.filter.optional.disabled=Optionaler Chatfilter deaktiviert\! command.filter.optional.description=Schaltet den optionalen Chatfilter um. command.help.argument.query=Die Suchanfrage. command.help.description=Carbon Befehlsliste. command.help.misc.arguments=Argumente command.help.misc.available_commands=Verfügbare Befehle command.help.misc.click_for_next_page=Klicken für die nächste Seite command.help.misc.click_for_previous_page=Klicken für die vorherige Seite command.help.misc.click_to_show_help=Klicken, um Hilfe für diesen Befehl anzuzeigen command.help.misc.command=Befehl command.help.misc.description=Beschreibung command.help.misc.help=Hilfe command.help.misc.no_description=Keine Beschreibung command.help.misc.no_results_for_query=Keine Ergebnisse für die Abfrage command.help.misc.optional=Optional command.help.misc.page_out_of_range=Error\: Seitn is ned in Reichweitn. Muss in da Reichweitn [1, ] sei command.help.misc.showing_results_for_query=Suchergebnisse für die Abfrage anzeigen command.ignore.argument.player=Der Name des zu ignorierenden Spielers. command.ignore.argument.uuid=Die UUID des zu ignorierenden Spielers. command.ignore.description=Versteckt alle eingehenden Nachrichten von ignorierten Spielern. command.ignorelist.description=Zeigt eine paginierte Liste mit Spielern an, welche du ignorierst. command.ignorelist.none_ignored=Du ignorierst keine Spieler. command.ignorelist.pagination_header=Ignorierte Spieler command.ignorelist.pagination_element= - ''>''>[nicht mehr ignorieren] command.join.description=Trete einem Kanal bei, den du zuvor verlassen hast. command.leave.description=Verlasse einen Kanal, auf den du derzeit Zugriff hast. command.mute.argument.player=Der Name des Spielers, der stummgeschaltet werden soll. command.mute.argument.uuid=Die UUID des Players, der stummgeschaltet werden soll. command.mute.argument.duration=Die Dauer, für die der Spieler stummgeschaltet wird. command.mute.description=Schaltet Spieler stumm, sodass sie weder den Chat nutzen noch anderen Spielern Flüsternachrichten senden können. command.muteinfo.argument.player=Der Name des Spielers. command.muteinfo.argument.uuid=Die UUID des Spielers. command.muteinfo.description=Zeigt an, ob Spieler stumm sind oder nicht. command.nickname.argument.nickname=Der zu setzende Nickname. command.nickname.argument.player=Der Name des Zielspielers. command.nickname.description=Zeigt deinen Nickname an. command.nickname.set.description=Legt deinen Nickname fest. command.nickname.reset.description=Entfernt deinen Nickname. command.nickname.others.description=Zeigt Nicknamen des Spielers. command.nickname.others.set.description=Legt Nicknamen des Spielers fest. command.nickname.others.reset.description=Entfernt jeden eingestellten Nickname vom Ziel. command.reload.description=Lädt Carbons Konfiguration, Kanaleinstellungen und Übersetzungen neu. Lädt und entlädt keine Kanäle. command.reply.argument.message=Die Nachricht, mit welcher geantwortet wird. command.reply.description=Sendet eine Nachricht an die letzte Person, der du geschrieben hast. command.togglemsg.description=Erlaubt und verbietet anderen Spielern, dir Nachrichten zu schreiben. command.unignore.argument.player=Der Name des Spielers, der nicht mehr ignoriert werden soll. command.unignore.argument.uuid=Die UUID des Spielers, der nicht mehr ignoriert werden soll. command.unignore.description=Beendet das Verstecken von Nachrichten des angegebenen Spielers. command.unmute.argument.player=Der Name des Spielers, dessen Stummschaltung aufgehoben werden soll. command.unmute.argument.uuid=Die UUID des Spielers, dessen Stummschaltung aufgehoben werden soll. command.unmute.description=Deaktiviert die Stummschaltung der Spieler, sodass sie den Chat nutzen und anderen Spielern Flüsternachrichten senden können. command.updateusername.argument.player=Der Name des zu aktualisierenden Spielers. command.updateusername.argument.uuid=Die UUID des zu aktualisierenden Spielers. command.updateusername.description=Aktualisiert den Benutzernamen des Spielers auf ihren Mojang-Namen. command.updateusername.fetching=Lade Benutzername... command.updateusername.notupdated=Benutzername konnte nicht abgerufen werden. command.updateusername.updated=Der Benutzername von wurde aktualisiert. command.whisper.argument.message=Die zu sendende Nachricht. command.whisper.argument.player=Der Name des Spielers, dem die Nachricht gesendet werden soll. command.whisper.description=Sendet eine private Nachricht an den angegebenen Spieler. command.party.pagination_header=Partymitglieder\: command.party.pagination_element=''\:''''> - command.party.created=Die Party "" wurde erfolgreich erstellt und du bist ihr beigetreten\! command.party.not_in_party=Du bist nicht in einer Party. Benutze ''/party create'', um eine zu erstellen, oder ''/party accept'' um eine Einladung anzunehmen. command.party.current_party=Du bist in der Party\: command.party.must_leave_current_first=Du musst zuerst deine aktuelle Party verlassen. command.party.name_too_long=Der Name der Party ist zu lang. command.party.received_invite=Zum Akzeptieren anklicken''>Du wurdest von zur Party "" eingeladen. Klicke auf diese Nachricht, um zu akzeptieren. command.party.sent_invite=Einladung zur Party an gesendet. command.party.must_specify_invite=Du musst angeben, wessen Partyeinladung angenommen werden soll. command.party.no_pending_invites=Du hast keine ausstehenden Partyeinladungen. command.party.no_invite_from=Du hast keine ausstehende Partyeinladung von . command.party.joined_party=Erfolgreich der Party "" beigetreten\! command.party.left_party=Erfolgreich die Party "" verlassen. command.party.disbanded=Die Party "" wurde erfolgreich aufgelöst. command.party.cannot_disband_multiple_members=Die Party "" kann nicht aufgelöst werden, da du nicht das letzte Mitglied bist. command.party.must_be_in_party=Du musst in einer Party sein, um diesen Befehl zu verwenden. Benutze "/party create" um eine zu erstellen, oder "/party accept" um eine Einladung anzunehmen. command.party.cannot_invite_self=Du kannst dich nicht selbst einladen. command.party.description=Erhalte Informationen und sehe die Mitglieder deiner aktuellen Party. command.party.create.description=Eine neue Party erstellen. command.party.invite.description=Lade einen Spieler in deine Party ein. command.party.accept.description=Einladungen für Partys akzeptieren. command.party.leave.description=Verlasse deine aktuelle Party. command.party.already_in_party= ist bereits in deiner Party. command.party.disband.description=Löse deine aktuelle Party auf. command.realname.description=Zeigt den echten Namen des Spielers an. command.realname.argument.player=Der Anzeigename des Spielers. command.spy.enabled=Spionage ist jetzt aktiviert. command.spy.disabled=Spionage ist jetzt deaktiviert. command.spy.description=Ermöglicht es einem Spieler, alle privaten und Kanalnachrichten zu sehen, die er sonst nicht sehen würde. duration.days=dhms duration.hours=hms party.player_joined= ist deiner Party beigetreten. party.player_left= hat deine Party verlassen. party.cannot_use_channel=Du musst einer Party beitreten, um diesen Kanal zu nutzen. party.spy=Spy [\: ] config.reload.failed=Konfiguration konnte nicht neu geladen werden config.reload.success=Konfiguration erfolgreich neu geladen error.command.argument_parsing=Ungültiges Befehlsargument\: error.command.command_execution=\n\n Klicken zum Kopieren">''>Bei der Ausführung dieses Befehls ist ein interner Fehler aufgetreten. error.command.invalid_player=Kein Spieler für die Eingabe "" gefunden error.command.invalid_sender=Ungültiger Befehlsabsender. Du musst vom Typ sein error.command.invalid_syntax=Ungültige Befehlssyntax. Der korrekte Befehlssyntax ist\: / error.command.no_permission=Es tut mir leid, aber du hast keine Berechtigung, um diesen Befehl auszuführen. Bitte kontaktiere einen Serveradministrator, wenn du der Meinung bist, dass es sich hier um einen Fehler handelt. error.command.command_needs_player=Nicht-Spieler müssen das Spielerargument angeben, um diesen Befehl auszuführen. ignore.already_ignored=Du ignorierst bereits ignore.not_ignored=Du ignorierst nicht ignore.exempt=Du kannst nicht ignorieren ignore.invalid_target=Kein Ziel gefunden ignore.now_ignoring=Du ignorierst jetzt ignore.no_longer_ignoring=Du ignorierst nicht mehr mute.alert.players= wurde stummgeschaltet mute.alert.players.temp= wurde für stummgeschaltet mute.alert.target=Du wurdest stummgeschaltet mute.alert.target.temp=Du wurdest für stummgeschaltet mute.cannot_speak=Du kannst nicht sprechen, während du stummgeschaltet bist mute.exempt=Dieser Spieler darf nicht stummgeschaltet werden mute.info.muted= ist stummgeschaltet mute.info.muted.duration= ist für stummgeschaltet mute.info.not_muted= ist nicht stummgeschaltet mute.info.self.muted=Du bist stummgeschaltet mute.info.self.not_muted=Du bist nicht stummgeschaltet mute.no_target=Kein Spieler zum Stummschalten angegeben. mute.spy.prefix=Stummgeschaltet''>M mute.unmute.alert.players= ist nicht mehr stummgeschaltet. mute.unmute.alert.target=Du bist nicht mehr stummgeschaltet. mute.unmute.no_target=Kein Spieler zum Entstummen angegeben. nickname.reset.others=Der Nickname von "" wurde zurückgesetzt. nickname.reset=Dein Nickname wurde zurückgesetzt nickname.set.others=Du hast den Nickname von auf gesetzt nickname.set=Dein Nickname wurde auf gesetzt nickname.show.others.unset= hat keinen Nickname gesetzt nickname.show.others=Der Nickname von lautet nickname.show.unset=Du hast keinen Nickname gesetzt nickname.show=Dein Nickname ist nickname.error.character_limit=Der Nickname "" hat das Zeichenlimit überschritten. Er muss auf ~ Zeichen gesetzt werden. nickname.error.blacklist=Nickname "" ist nicht erlaubt. Bitte wähle einen anderen Namen. nickname.error.filter=Nicknamen müssen alphanumerisch sein\! nickname.realname=s echter Name ist reply.target.missing=Du hast niemanden zum Antworten reply.target.self=Du kannst dir nicht selbst zuflüstern whisper.console=[] -> [] whisper.continue.target_missing=Du hast niemanden zum Flüstern whisper.error=Fehler beim Senden der privaten Nachricht whisper.from= ''>[] -> [Du] whisper.from.spy=SPY [] -> [] whisper.ignored_by_target= ignoriert dich whisper.ignoring_target=Du ignorierst whisper.ignoring_all=Du kannst keine Nachrichten senden, solange sie ignoriert werden\! whisper.no_permission.receive=Dieser Spieler hat keine Berechtigung, Nachrichten zu erhalten\! whisper.no_permission.send=Du hast keine Berechtigung, Flüsternachrichten zu senden\! whisper.to= ''> zu senden''>[Du] -> [] whisper.toggled.on=Private Nachrichten werden nun empfangen. whisper.toggled.off=Private Nachrichten werden nun nicht mehr empfangen. channel.cooldown=Du kannst den Chat in Sekunden wieder verwenden\! channel.radius.empty_recipients=Du bist nicht nah genug an jemanden, um eine Nachricht zu senden channel.radius.spy=Spy [\: ] channel.joined=Du bist dem Kanal wieder beigetreten channel.left=Du hast den Kanal verlassen channel.no_permission=Du hast keine Berechtigung, um diesen Kanal zu verwenden channel.already_left=Du hast diesen Kanal bereits verlassen channel.not_left=Du hast diesen Kanal nicht verlassen channel.not_found=Kanal nicht gefunden pagination.page_out_of_range=Seite ist außerhalb des Bereichs\! Es gibt nur Seiten. pagination.click_for_next_page=Klicken für die nächste Seite pagination.click_for_previous_page=Klicken für die vorherige Seite pagination.footer=Seite / integrations.towny.cannot_use_alliance_channel=Du musst einer Allianz beitreten, um diesen Kanal zu nutzen. integrations.towny.cannot_use_nation_channel=Du musst einer Nation beitreten, um diesen Kanal zu nutzen. integrations.towny.cannot_use_town_channel=Du musst einer Stadt beitreten, um diesen Kanal nutzen zu können. integrations.mcmmo.cannot_use_party_channel=Du musst einer mcMMO Party beitreten, um diesen Kanal nutzen zu können. integrations.fuuid.cannot_use_faction_channel=Du musst einer Fraktion beitreten, um diesen Kanal nutzen zu können. integrations.fuuid.cannot_use_alliance_channel=Du musst einer Allianz beitreten, um diesen Kanal zu nutzen. integrations.fuuid.cannot_use_truce_channel=Du musst einen Waffenstillstand mit einer anderen Fraktion haben, um diesen Kanal nutzen zu können. integrations.fuuid.cannot_use_mod_channel=Du musst ein Fraktionsmod/Admin sein, um diesen Kanal nutzen zu können. integrations.plotsquared.cannot_use_plot_channel=Du musst in einem Grundstück sein, um diesen Kanal nutzen zu können. ================================================ FILE: common/src/main/resources/locale/messages-de_CH.properties ================================================ ================================================ FILE: common/src/main/resources/locale/messages-de_DE.properties ================================================ channel.change=Du schreibst nun in command.clearchat.description=Löscht den Chat für alle Spieler. command.continue.argument.message=Die zu sendende Nachricht. command.continue.description=Sendet eine Nachricht an die letzte Person, der du geschrieben hast. command.debug.argument.player=Der Spieler, von dem die Gruppen überprüft werden. command.debug.description=Zeigt die Berechtigungsgruppen der Spieler an. command.filter.optional.enabled=Optionaler Chatfilter aktiviert\! command.filter.optional.disabled=Optionaler Chatfilter deaktiviert\! command.filter.optional.description=Schaltet den optionalen Chatfilter um. command.help.argument.query=Die Suchanfrage. command.help.description=Carbon Befehlsliste. command.help.misc.arguments=Argumente command.help.misc.available_commands=Verfügbare Befehle command.help.misc.click_for_next_page=Klicken für die nächste Seite command.help.misc.click_for_previous_page=Klicken für die vorherige Seite command.help.misc.click_to_show_help=Klicken, um Hilfe für diesen Befehl anzuzeigen command.help.misc.command=Befehl command.help.misc.description=Beschreibung command.help.misc.help=Hilfe command.help.misc.no_description=Keine Beschreibung command.help.misc.no_results_for_query=Keine Ergebnisse für die Abfrage command.help.misc.optional=Optional command.help.misc.page_out_of_range=Fehler\: Seite ist nicht im Bereich. Muss im Bereich [1, ] sein command.help.misc.showing_results_for_query=Suchergebnisse für die Abfrage anzeigen command.ignore.argument.player=Der Name des zu ignorierenden Spielers. command.ignore.argument.uuid=Die UUID des zu ignorierenden Spielers. command.ignore.description=Versteckt alle eingehenden Nachrichten von ignorierten Spielern. command.ignorelist.description=Zeigt eine paginierte Liste mit Spielern an, welche du ignorierst. command.ignorelist.none_ignored=Du ignorierst keine Spieler. command.ignorelist.pagination_header=Ignorierte Spieler command.ignorelist.pagination_element= - ''>''>[nicht mehr ignorieren] command.join.description=Trete einem Kanal bei, den du zuvor verlassen hast. command.leave.description=Verlasse einen Kanal, auf den du derzeit Zugriff hast. command.mute.argument.player=Der Name des Spielers, der stummgeschaltet werden soll. command.mute.argument.uuid=Die UUID des Players, der stummgeschaltet werden soll. command.mute.argument.duration=Die Dauer, für die der Spieler stummgeschaltet wird. command.mute.description=Schaltet Spieler stumm, sodass sie weder den Chat nutzen noch anderen Spielern Flüsternachrichten senden können. command.muteinfo.argument.player=Der Name des Spielers. command.muteinfo.argument.uuid=Die UUID des Spielers. command.muteinfo.description=Zeigt an, ob Spieler stumm sind oder nicht. command.nickname.argument.nickname=Der zu setzende Nickname. command.nickname.argument.player=Der Name des Zielspielers. command.nickname.description=Zeigt deinen Nickname an. command.nickname.set.description=Legt deinen Nickname fest. command.nickname.reset.description=Entfernt deinen Nickname. command.nickname.others.description=Zeigt Nicknamen des Spielers. command.nickname.others.set.description=Legt Nicknamen des Spielers fest. command.nickname.others.reset.description=Entfernt jeden eingestellten Nickname vom Ziel. command.reload.description=Lädt Carbons Konfiguration, Kanaleinstellungen und Übersetzungen neu. Lädt und entlädt keine Kanäle. command.reply.argument.message=Die Nachricht, mit welcher geantwortet wird. command.reply.description=Sendet eine Nachricht an die letzte Person, der du geschrieben hast. command.togglemsg.description=Erlaubt und verbietet anderen Spielern, dir Nachrichten zu schreiben. command.unignore.argument.player=Der Name des Spielers, der nicht mehr ignoriert werden soll. command.unignore.argument.uuid=Die UUID des Spielers, der nicht mehr ignoriert werden soll. command.unignore.description=Beendet das Verstecken von Nachrichten des angegebenen Spielers. command.unmute.argument.player=Der Name des Spielers, dessen Stummschaltung aufgehoben werden soll. command.unmute.argument.uuid=Die UUID des Spielers, dessen Stummschaltung aufgehoben werden soll. command.unmute.description=Deaktiviert die Stummschaltung der Spieler, sodass sie den Chat nutzen und anderen Spielern Flüsternachrichten senden können. command.updateusername.argument.player=Der Name des zu aktualisierenden Spielers. command.updateusername.argument.uuid=Die UUID des zu aktualisierenden Spielers. command.updateusername.description=Aktualisiert den Benutzernamen des Spielers auf ihren Mojang-Namen. command.updateusername.fetching=Lade Benutzername... command.updateusername.notupdated=Benutzername konnte nicht abgerufen werden. command.updateusername.updated=Der Benutzername von wurde aktualisiert. command.whisper.argument.message=Die zu sendende Nachricht. command.whisper.argument.player=Der Name des Spielers, dem die Nachricht gesendet werden soll. command.whisper.description=Sendet eine private Nachricht an den angegebenen Spieler. command.party.pagination_header=Partymitglieder\: command.party.pagination_element=''\:''''> - command.party.created=Die Party "" wurde erfolgreich erstellt und du bist ihr beigetreten\! command.party.not_in_party=Du bist nicht in einer Party. Benutze ''/party create'', um eine zu erstellen, oder ''/party accept'' um eine Einladung anzunehmen. command.party.current_party=Du bist in der Party\: command.party.must_leave_current_first=Du musst zuerst deine aktuelle Party verlassen. command.party.name_too_long=Der Name der Party ist zu lang. command.party.received_invite=Zum Akzeptieren anklicken''>Du wurdest von zur Party "" eingeladen. Klicke auf diese Nachricht, um zu akzeptieren. command.party.sent_invite=Einladung zur Party an gesendet. command.party.must_specify_invite=Du musst angeben, wessen Partyeinladung angenommen werden soll. command.party.no_pending_invites=Du hast keine ausstehenden Partyeinladungen. command.party.no_invite_from=Du hast keine ausstehende Partyeinladung von . command.party.joined_party=Erfolgreich der Party "" beigetreten\! command.party.left_party=Erfolgreich die Party "" verlassen. command.party.disbanded=Die Party "" wurde erfolgreich aufgelöst. command.party.cannot_disband_multiple_members=Die Party "" kann nicht aufgelöst werden, da du nicht das letzte Mitglied bist. command.party.must_be_in_party=Du musst in einer Party sein, um diesen Befehl zu verwenden. Benutze "/party create" um eine zu erstellen, oder "/party accept" um eine Einladung anzunehmen. command.party.cannot_invite_self=Du kannst dich nicht selbst einladen. command.party.description=Erhalte Informationen und sehe die Mitglieder deiner aktuellen Party. command.party.create.description=Eine neue Party erstellen. command.party.invite.description=Lade einen Spieler in deine Party ein. command.party.accept.description=Einladungen für Partys akzeptieren. command.party.leave.description=Verlasse deine aktuelle Party. command.party.already_in_party= ist bereits in deiner Party. command.party.disband.description=Löse deine aktuelle Party auf. command.realname.description=Zeigt den echten Namen des Spielers an. command.realname.argument.player=Der Anzeigename des Spielers. command.spy.enabled=Spionage ist jetzt aktiviert. command.spy.disabled=Spionage ist jetzt deaktiviert. command.spy.description=Ermöglicht es einem Spieler, alle privaten und Kanalnachrichten zu sehen, die er sonst nicht sehen würde. duration.days=dhms duration.hours=hms party.player_joined= ist deiner Party beigetreten. party.player_left= hat deine Party verlassen. party.cannot_use_channel=Du musst einer Party beitreten, um diesen Kanal zu nutzen. party.spy=Spy [\: ] config.reload.failed=Konfiguration konnte nicht neu geladen werden config.reload.success=Konfiguration erfolgreich neu geladen error.command.argument_parsing=Ungültiges Befehlsargument\: error.command.command_execution=\n\n Klicken zum Kopieren">''>Bei der Ausführung dieses Befehls ist ein interner Fehler aufgetreten. error.command.invalid_player=Kein Spieler für die Eingabe "" gefunden error.command.invalid_sender=Ungültiger Befehlsabsender. Du musst vom Typ sein error.command.invalid_syntax=Ungültige Befehlssyntax. Der korrekte Befehlssyntax ist\: / error.command.no_permission=Es tut mir leid, aber du hast keine Berechtigung, um diesen Befehl auszuführen. Bitte kontaktiere einen Serveradministrator, wenn du der Meinung bist, dass es sich hier um einen Fehler handelt. error.command.command_needs_player=Nicht-Spieler müssen das Spielerargument angeben, um diesen Befehl auszuführen. ignore.already_ignored=Du ignorierst bereits ignore.not_ignored=Du ignorierst nicht ignore.exempt=Du kannst nicht ignorieren ignore.invalid_target=Kein Ziel gefunden ignore.now_ignoring=Du ignorierst jetzt ignore.no_longer_ignoring=Du ignorierst nicht mehr mute.alert.players= wurde stummgeschaltet mute.alert.players.temp= wurde für stummgeschaltet mute.alert.target=Du wurdest stummgeschaltet mute.alert.target.temp=Du wurdest für stummgeschaltet mute.cannot_speak=Du kannst nicht sprechen, während du stummgeschaltet bist mute.exempt=Dieser Spieler darf nicht stummgeschaltet werden mute.info.muted= ist stummgeschaltet mute.info.muted.duration= ist für stummgeschaltet mute.info.not_muted= ist nicht stummgeschaltet mute.info.self.muted=Du bist stummgeschaltet mute.info.self.not_muted=Du bist nicht stummgeschaltet mute.no_target=Kein Spieler zum Stummschalten angegeben. mute.spy.prefix=Stummgeschaltet''>M mute.unmute.alert.players= ist nicht mehr stummgeschaltet. mute.unmute.alert.target=Du bist nicht mehr stummgeschaltet. mute.unmute.no_target=Kein Spieler zum Entstummen angegeben. nickname.reset.others=Der Nickname von "" wurde zurückgesetzt. nickname.reset=Dein Nickname wurde zurückgesetzt nickname.set.others=Du hast den Nickname von auf gesetzt nickname.set=Dein Nickname wurde auf gesetzt nickname.show.others.unset= hat keinen Nickname gesetzt nickname.show.others=Der Nickname von lautet nickname.show.unset=Du hast keinen Nickname gesetzt nickname.show=Dein Nickname ist nickname.error.character_limit=Der Nickname "" hat das Zeichenlimit überschritten. Er muss auf ~ Zeichen gesetzt werden. nickname.error.blacklist=Nickname "" ist nicht erlaubt. Bitte wähle einen anderen Namen. nickname.error.filter=Nicknamen müssen alphanumerisch sein\! nickname.realname=s echter Name ist reply.target.missing=Du hast niemanden zum Antworten reply.target.self=Du kannst dir nicht selbst zuflüstern whisper.console=[] -> [] whisper.continue.target_missing=Du hast niemanden zum Flüstern whisper.error=Fehler beim Senden der privaten Nachricht whisper.from= ''>[] -> [Du] whisper.from.spy=SPY [] -> [] whisper.ignored_by_target= ignoriert dich whisper.ignoring_target=Du ignorierst whisper.ignoring_all=Du kannst keine Nachrichten senden, solange sie ignoriert werden\! whisper.no_permission.receive=Dieser Spieler hat keine Berechtigung, Nachrichten zu erhalten\! whisper.no_permission.send=Du hast keine Berechtigung, Flüsternachrichten zu senden\! whisper.to= ''> zu senden''>[Du] -> [] whisper.toggled.on=Private Nachrichten werden nun empfangen. whisper.toggled.off=Private Nachrichten werden nun nicht mehr empfangen. channel.cooldown=Du kannst den Chat in Sekunden wieder verwenden\! channel.radius.empty_recipients=Du bist nicht nah genug an jemanden, um eine Nachricht zu senden channel.radius.spy=Spy [\: ] channel.joined=Du bist dem Kanal wieder beigetreten channel.left=Du hast den Kanal verlassen channel.no_permission=Du hast keine Berechtigung, um diesen Kanal zu verwenden channel.already_left=Du hast diesen Kanal bereits verlassen channel.not_left=Du hast diesen Kanal nicht verlassen channel.not_found=Kanal nicht gefunden pagination.page_out_of_range=Seite ist außerhalb des Bereichs\! Es gibt nur Seiten. pagination.click_for_next_page=Klicken für die nächste Seite pagination.click_for_previous_page=Klicken für die vorherige Seite pagination.footer=Seite / integrations.towny.cannot_use_alliance_channel=Du musst einer Allianz beitreten, um diesen Kanal zu nutzen. integrations.towny.cannot_use_nation_channel=Du musst einer Nation beitreten, um diesen Kanal zu nutzen. integrations.towny.cannot_use_town_channel=Du musst einer Stadt beitreten, um diesen Kanal nutzen zu können. integrations.mcmmo.cannot_use_party_channel=Du musst einer mcMMO Party beitreten, um diesen Kanal nutzen zu können. integrations.fuuid.cannot_use_faction_channel=Du musst einer Fraktion beitreten, um diesen Kanal nutzen zu können. integrations.fuuid.cannot_use_alliance_channel=Du musst einer Allianz beitreten, um diesen Kanal zu nutzen. integrations.fuuid.cannot_use_truce_channel=Du musst einen Waffenstillstand mit einer anderen Fraktion haben, um diesen Kanal nutzen zu können. integrations.fuuid.cannot_use_mod_channel=Du musst ein Fraktionsmod/Admin sein, um diesen Kanal nutzen zu können. integrations.plotsquared.cannot_use_plot_channel=Du musst in einem Grundstück sein, um diesen Kanal nutzen zu können. ================================================ FILE: common/src/main/resources/locale/messages-en_US.properties ================================================ channel.change=You are now messaging command.clearchat.description=Clears the chat window for all players. command.continue.argument.message=The message to send. command.continue.description=Sends a message to the last person you messaged. command.debug.argument.player=The player to check the groups of. command.debug.description=Shows the permission groups of players. command.filter.optional.enabled=Optional chat filter enabled! command.filter.optional.disabled=Optional chat filter disabled! command.filter.optional.description=Toggles the optional chat filter. command.help.argument.query=The search query. command.help.description=Carbon command list. command.help.misc.arguments=Arguments command.help.misc.available_commands=Available Commands command.help.misc.click_for_next_page=Click for next page command.help.misc.click_for_previous_page=Click for previous page command.help.misc.click_to_show_help=Click to show help for this command command.help.misc.command=Command command.help.misc.description=Description command.help.misc.help=Help command.help.misc.no_description=No description command.help.misc.no_results_for_query=No results for query command.help.misc.optional=Optional command.help.misc.page_out_of_range=Error: Page is not in range. Must be in range [1, ] command.help.misc.showing_results_for_query=Showing search results for query command.ignore.argument.player=The name of the player to ignore. command.ignore.argument.uuid=The UUID of the player to ignore. command.ignore.description=Hides all incoming messages from ignored players. command.ignorelist.description=Displays a paginated list of who you are ignoring. command.ignorelist.none_ignored=You are not ignoring any players. command.ignorelist.pagination_header=Ignored players command.ignorelist.pagination_element= - '>'>[unignore] command.join.description=Join a channel you have previously left. command.leave.description=Leave a channel that you currently have access to. command.mute.argument.player=The name of the player to mute. command.mute.argument.uuid=The UUID of the player to mute. command.mute.argument.duration=The duration the player will be muted for. command.mute.description=Mutes players, preventing them from using chat or whispering other players. command.muteinfo.argument.player=The name of the player. command.muteinfo.argument.uuid=The UUID of the player. command.muteinfo.description=Shows if players are muted or not. command.nickname.argument.nickname=The nickname to set. command.nickname.argument.player=The name of the target player. command.nickname.description=Shows your nickname. command.nickname.set.description=Sets your nickname. command.nickname.reset.description=Removes your nickname. command.nickname.others.description=Shows player nicknames. command.nickname.others.set.description=Sets player nicknames. command.nickname.others.reset.description=Removes any set nickname from the target. command.reload.description=Reloads Carbon's config, channel settings, and translations. Will not load or unload any channels. command.reply.argument.message=The message to reply with. command.reply.description=Sends a message to the last player that messaged you. command.togglemsg.description=Allows and disallows other players from mesaging you. command.unignore.argument.player=The name of the player to unignore. command.unignore.argument.uuid=The UUID of the player to unignore. command.unignore.description=Stops hiding messages from the specified player. command.unmute.argument.player=The name of the player to unmute. command.unmute.argument.uuid=The UUID of the player to unmute. command.unmute.description=Unmutes players, allowing them to use chat and whisper other players. command.updateusername.argument.player=The name of the player to update. command.updateusername.argument.uuid=The uuid of the player to update. command.updateusername.description=Updates the player's username to match their mojang name. command.updateusername.fetching=Fetching username... command.updateusername.notupdated=Unable to fetch username. 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. Use '/party create' to create one, or '/party accept' to accept an invite. 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=Click to accept'>'>You were invited to the party '' by . Click this message to accept. 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. Use '/party create' to create one, or '/party accept' to accept an invite. 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. command.realname.description=Shows the player's real name. command.realname.argument.player=The player's display name. command.spy.enabled=Spying is now enabled. command.spy.disabled=Spying is now disabled. command.spy.description=Allows a player to view all private and channel messages they otherwise wouldn't see. duration.days=dhms duration.hours=hms party.player_joined= joined your party. party.player_left= left your party. party.cannot_use_channel=You must join a party to use this channel. party.spy=Spy [: ] config.reload.failed=Config failed to reload config.reload.success=Config reloaded successfully error.command.argument_parsing=Invalid command argument: error.command.command_execution=\n\n Click to copy">'>An internal error occurred while attempting to perform this command. error.command.invalid_player=No player found for input '' error.command.invalid_sender=Invalid command sender. You must be of type error.command.invalid_syntax=Invalid command syntax. Correct command syntax is: / error.command.no_permission=I'm sorry, but you do not have permission to perform this command.\nPlease contact the server administrators if you believe that this is in error. error.command.command_needs_player=Non-players must provide the player argument to execute this command. ignore.already_ignored=You are already ignoring ignore.not_ignored=You are not ignoring ignore.exempt=You cannot ignore ignore.invalid_target=No target found ignore.now_ignoring=You are now ignoring ignore.no_longer_ignoring=You are no longer ignoring mute.alert.players= has been muted mute.alert.players.temp= has been muted for mute.alert.target=You have been muted mute.alert.target.temp=You have been muted for mute.cannot_speak=You cannot speak when muted mute.exempt=That player is exempt from being muted mute.info.muted= is muted mute.info.muted.duration= is muted for mute.info.not_muted= is not muted mute.info.self.muted=You are muted mute.info.self.not_muted=You are not muted mute.no_target=No specified player to mute. mute.spy.prefix=Muted'>M mute.unmute.alert.players= has been unmuted mute.unmute.alert.target=You have been unmuted mute.unmute.no_target=No specified player to unmute. nickname.reset.others='s nickname was reset nickname.reset=Your nickname was reset nickname.set.others=You set 's nickname to nickname.set=Your nickname has been set to nickname.show.others.unset= does not have a nickname set nickname.show.others='s nickname is nickname.show.unset=You do not have a nickname set nickname.show=Your nickname is nickname.error.character_limit=Nickname "" has exceeded the character limit. Must be set to ~ characters. nickname.error.blacklist=Nickname "" is not allowed. Please choose another name. nickname.error.filter=Nicknames must be alphanumeric! nickname.realname='s real name is reply.target.missing=You have no-one to reply to reply.target.self=You cannot whisper to yourself whisper.console=[] -> [] whisper.continue.target_missing=You have no one to whisper whisper.error=Failed to send private message whisper.from= '>[] -> [You] whisper.from.spy=SPY [] -> [] whisper.ignored_by_target= is ignoring you whisper.ignoring_target=You are ignoring whisper.ignoring_all=You cannot send messages while they are ignored! whisper.no_permission.receive=That player doesn't have permission to receive messages! whisper.no_permission.send=You don't have permission to send whispers! whisper.to= '>'>[You] -> [] whisper.toggled.on=Now receiving private messages. whisper.toggled.off=No longer receiving private messages. channel.cooldown=You may use chat again in seconds! channel.radius.empty_recipients=You're not close enough to anyone to send a message channel.radius.spy=Spy [: ] channel.joined=You have rejoined the channel channel.left=You have left the channel channel.no_permission=You do not have permission to use this channel channel.already_left=You have already left this channel channel.not_left=You have not left this channel channel.not_found=Channel not found pagination.page_out_of_range=Page is out of range! There are only pages. pagination.click_for_next_page=Click for next page pagination.click_for_previous_page=Click for previous page pagination.footer=Page / integrations.towny.cannot_use_alliance_channel=You must join an alliance to use this channel. integrations.towny.cannot_use_nation_channel=You must join a nation to use this channel. integrations.towny.cannot_use_town_channel=You must join a town to use this channel. integrations.mcmmo.cannot_use_party_channel=You must join an mcMMO party to use this channel. integrations.adp_parties.cannot_use_party_channel=You must join a party to use this channel. integrations.fuuid.cannot_use_faction_channel=You must join a faction to use this channel. integrations.fuuid.cannot_use_alliance_channel=You must join an alliance to use this channel. integrations.fuuid.cannot_use_truce_channel=You must have a truce with another faction to use this channel. integrations.fuuid.cannot_use_mod_channel=You must be a faction mod/admin to use this channel. integrations.plotsquared.cannot_use_plot_channel=You must be in a plot to use this channel. ================================================ FILE: common/src/main/resources/locale/messages-es_CL.properties ================================================ channel.change=Ahora estás enviando mensajes a command.clearchat.description=Borra la ventana de chat para todos los jugadores. command.continue.argument.message=El mensaje a enviar. command.continue.description=Envía un mensaje a la última persona con la que hablaste. command.debug.argument.player=El jugador para verificar los grupos. command.debug.description=Muestra los grupos de permisos de los jugadores. command.filter.optional.enabled=¡Filtro de chat opcional activado\! command.filter.optional.disabled=¡Filtro de chat opcional desactivado\! command.filter.optional.description=Activa o desactiva el filtro de chat opcional. command.help.argument.query=La consulta de búsqueda. command.help.description=Lista de comandos de Carbon. command.help.misc.arguments=Argumentos command.help.misc.available_commands=Comandos Disponibles command.help.misc.click_for_next_page=Haz clic para la siguiente página command.help.misc.click_for_previous_page=Haz clic para la página anterior command.help.misc.click_to_show_help=Haz clic para mostrar la ayuda de este comando command.help.misc.command=Comando command.help.misc.description=Descripción command.help.misc.help=Ayuda command.help.misc.no_description=Sin descripción command.help.misc.no_results_for_query=No hay resultados para la consulta command.help.misc.optional=Opcional command.help.misc.page_out_of_range=Error\: La página no está en el rango. Debe estar en el rango [1, ] command.help.misc.showing_results_for_query=Mostrando resultados de la búsqueda para la consulta command.ignore.argument.player=El nombre del jugador a ignorar. command.ignore.argument.uuid=El UUID del jugador a ignorar. command.ignore.description=Oculta todos los mensajes entrantes de los jugadores ignorados. command.ignorelist.description=Muestra una lista paginada de los jugadores que estás ignorando. command.ignorelist.none_ignored=No estás ignorando a ningún jugador. command.ignorelist.pagination_header=Jugadores ignorados command.ignorelist.pagination_element= - ''>''>[dejar de ignorar] command.join.description=Únete a un canal que has abandonado previamente. command.leave.description=Abandona un canal al que actualmente tienes acceso. command.mute.argument.player=El nombre del jugador a silenciar. command.mute.argument.uuid=El UUID del jugador a silenciar. command.mute.argument.duration=La duración por la que el jugador será silenciado. command.mute.description=Silencia a los jugadores, impidiéndoles usar el chat o susurrar a otros jugadores. command.muteinfo.argument.player=El nombre del jugador. command.muteinfo.argument.uuid=El UUID del jugador. command.muteinfo.description=Muestra si los jugadores están silenciados o no. command.nickname.argument.nickname=El apodo a establecer. command.nickname.argument.player=El nombre del jugador objetivo. command.nickname.description=Muestra tu apodo. command.nickname.set.description=Establece tu apodo. command.nickname.reset.description=Elimina tu apodo. command.nickname.others.description=Muestra los apodos de los jugadores. command.nickname.others.set.description=Establece los apodos de los jugadores. command.nickname.others.reset.description=Elimina cualquier apodo establecido del objetivo. command.reload.description=Recarga la configuración de Carbon, los ajustes de los canales y las traducciones. No cargará ni descargará ningún canal. command.reply.argument.message=El mensaje para responder. command.reply.description=Envía un mensaje al último jugador que te envió un mensaje. command.togglemsg.description=Permite y no permite que otros jugadores te envíen mensajes. command.unignore.argument.player=El nombre del jugador para dejar de ignorar. command.unignore.argument.uuid=El UUID del jugador para dejar de ignorar. command.unignore.description=Deja de ocultar los mensajes del jugador especificado. command.unmute.argument.player=El nombre del jugador para desilenciar. command.unmute.argument.uuid=El UUID del jugador para desilenciar. command.unmute.description=Desilencia a los jugadores, permitiéndoles usar el chat y susurrar a otros jugadores. command.updateusername.argument.player=El nombre del jugador a actualizar. command.updateusername.argument.uuid=El UUID del jugador a actualizar. command.updateusername.description=Actualiza el nombre de usuario del jugador para que coincida con su nombre de Mojang. command.updateusername.fetching=Obteniendo nombre de usuario... command.updateusername.notupdated=No se pudo obtener el nombre de usuario. command.updateusername.updated=¡Nombre de usuario de actualizado\! command.whisper.argument.message=El mensaje a enviar. command.whisper.argument.player=El nombre del jugador al que enviar el mensaje. command.whisper.description=Envía un mensaje privado al jugador especificado. command.party.pagination_header=Miembros del grupo\: command.party.pagination_element=''\:''''> - command.party.created=¡Grupo creado y unido exitosamente ''''\! command.party.not_in_party=No estás en un grupo. Usa ''/party create'' para crear uno, o ''/party accept'' para aceptar una invitación. command.party.current_party=Estás en el grupo\: command.party.must_leave_current_first=Debes abandonar tu grupo actual primero. command.party.name_too_long=El nombre del grupo es demasiado largo. command.party.received_invite=Haz clic para aceptar''>''>Fuiste invitado al grupo '''' por . Haz clic en este mensaje para aceptar. command.party.sent_invite=Invitación de grupo enviada a . command.party.must_specify_invite=Debes especificar a quién aceptar la invitación. command.party.no_pending_invites=No tienes invitaciones de grupo pendientes. command.party.no_invite_from=No tienes una invitación pendiente de . command.party.joined_party=¡Te has unido exitosamente al grupo ''''\! command.party.left_party=Has abandonado exitosamente el grupo ''''. command.party.disbanded=Grupo disuelto exitosamente ''''. command.party.cannot_disband_multiple_members=No puedes disolver el grupo '''', no eres el último miembro. command.party.must_be_in_party=Debes estar en un grupo para usar este comando. Usa ''/party create'' para crear uno, o ''/party accept'' para aceptar una invitación. command.party.cannot_invite_self=No puedes invitarte a ti mismo. command.party.description=Obtén información y ve los miembros de tu grupo actual. command.party.create.description=Crea un nuevo grupo. command.party.invite.description=Invita a un jugador a tu grupo. command.party.accept.description=Acepta invitaciones de grupo. command.party.leave.description=Abandona tu grupo actual. command.party.already_in_party= ya está en tu grupo. command.party.disband.description=Disuelve tu grupo actual. command.spy.enabled=El espionaje está ahora activado. command.spy.disabled=El espionaje está ahora desactivado. command.spy.description=Permite a un jugador ver todos los mensajes privados y de canal que de otra manera no vería. duration.days=dhms duration.hours=hms party.player_joined= se ha unido a tu grupo. party.player_left= ha abandonado tu grupo. party.cannot_use_channel=Debes unirte a un grupo para usar este canal. party.spy=Espía [\: ] config.reload.failed=Error al recargar la configuración config.reload.success=Configuración recargada exitosamente error.command.argument_parsing=Argumento de comando inválido\: error.command.command_execution=\n\n Haz clic para copiar">''>Ocurrió un error interno al intentar ejecutar este comando. error.command.invalid_player=No se encontró ningún jugador para la entrada '''' error.command.invalid_sender=Remitente de comando inválido. Debes ser de tipo error.command.invalid_syntax=Sintaxis de comando inválida. La sintaxis correcta es\: / error.command.no_permission=Lo siento, pero no tienes permiso para ejecutar este comando.\nPor favor, contacta a los administradores del servidor si crees que esto es un error. error.command.command_needs_player=Los no jugadores deben proporcionar el argumento del jugador para ejecutar este comando. ignore.already_ignored=Ya estás ignorando a ignore.not_ignored=No estás ignorando a ignore.exempt=No puedes ignorar a ignore.invalid_target=No se encontró el objetivo ignore.now_ignoring=Ahora estás ignorando a ignore.no_longer_ignoring=Ya no estás ignorando a mute.alert.players= ha sido silenciado mute.alert.players.temp= ha sido silenciado por mute.alert.target=Has sido silenciado mute.alert.target.temp=Has sido silenciado por mute.cannot_speak=No puedes hablar cuando estás silenciado mute.exempt=Ese jugador está exento de ser silenciado mute.info.muted= está silenciado mute.info.not_muted= no está silenciado mute.info.self.muted=Estás silenciado mute.info.self.not_muted=No estás silenciado mute.no_target=No se especificó ningún jugador para silenciar. mute.spy.prefix=Silenciado''>S mute.unmute.alert.players= ha sido desilenciado mute.unmute.alert.target=Has sido desilenciado mute.unmute.no_target=No se especificó ningún jugador para desilenciar. nickname.reset.others=El apodo de ha sido restablecido nickname.reset=Tu apodo ha sido restablecido nickname.set.others=Has establecido el apodo de a nickname.set=Tu apodo ha sido establecido a nickname.show.others.unset= no tiene un apodo establecido nickname.show.others=El apodo de es nickname.show.unset=No tienes un apodo establecido nickname.show=Tu apodo es nickname.error.character_limit=El apodo "" ha excedido el límite de caracteres. Debe tener entre ~ caracteres. nickname.error.blacklist=El apodo "" no está permitido. Por favor, elige otro nombre. nickname.error.filter=¡Los apodos deben ser alfanuméricos\! reply.target.missing=No tienes a nadie a quien responder reply.target.self=No puedes susurrarte a ti mismo whisper.console=[] -> [] whisper.continue.target_missing=No tienes a nadie a quien susurrar whisper.error=Error al enviar mensaje privado whisper.from= ''>[] -> [] whisper.from.spy=ESPÍA [] -> [] whisper.ignored_by_target= te está ignorando whisper.ignoring_target=Estás ignorando a whisper.ignoring_all=¡No puedes enviar mensajes mientras los ignoras\! whisper.no_permission.receive=¡Ese jugador no tiene permiso para recibir mensajes\! whisper.no_permission.send=¡No tienes permiso para enviar susurros\! whisper.to= ''>''>[] -> [] whisper.toggled.on=Ahora recibes mensajes privados. whisper.toggled.off=Ya no recibes mensajes privados. channel.cooldown=¡Podrás usar el chat nuevamente en segundos\! channel.radius.empty_recipients=No estás lo suficientemente cerca de nadie para enviar un mensaje channel.radius.spy=Espía [\: ] channel.joined=Has vuelto a unirte al canal channel.left=Has abandonado el canal channel.no_permission=No tienes permiso para usar este canal channel.already_left=Ya has abandonado este canal channel.not_left=No has abandonado este canal channel.not_found=Canal no encontrado pagination.page_out_of_range=¡La página está fuera de rango\! Solo hay páginas. pagination.click_for_next_page=Haz clic para la siguiente página pagination.click_for_previous_page=Haz clic para la página anterior pagination.footer=Página / integrations.towny.cannot_use_alliance_channel=Debes unirte a una alianza para usar este canal. integrations.towny.cannot_use_nation_channel=Debes unirte a una nación para usar este canal. integrations.towny.cannot_use_town_channel=Debes unirte a una ciudad para usar este canal. integrations.mcmmo.cannot_use_party_channel=Debes unirte a un grupo de mcMMO para usar este canal. integrations.fuuid.cannot_use_faction_channel=Debes unirte a una facción para usar este canal. integrations.fuuid.cannot_use_alliance_channel=Debes unirte a una alianza para usar este canal. integrations.fuuid.cannot_use_truce_channel=Debes tener una tregua con otra facción para usar este canal. integrations.fuuid.cannot_use_mod_channel=Debes ser un mod/admin de facción para usar este canal. integrations.plotsquared.cannot_use_plot_channel=Debes estar en una parcela para usar este canal. ================================================ FILE: common/src/main/resources/locale/messages-es_ES.properties ================================================ channel.change=Ahora estás enviando mensajes a command.clearchat.description=Borra la ventana de chat para todos los jugadores. command.continue.argument.message=El mensaje a enviar. command.continue.description=Envía un mensaje a la última persona con la que hablaste. command.debug.argument.player=El jugador para verificar los grupos. command.debug.description=Muestra los grupos de permisos de los jugadores. command.filter.optional.enabled=¡Filtro de chat opcional activado\! command.filter.optional.disabled=¡Filtro de chat opcional desactivado\! command.filter.optional.description=Activa o desactiva el filtro de chat opcional. command.help.argument.query=La consulta de búsqueda. command.help.description=Lista de comandos de Carbon. command.help.misc.arguments=Argumentos command.help.misc.available_commands=Comandos Disponibles command.help.misc.click_for_next_page=Haz clic para la siguiente página command.help.misc.click_for_previous_page=Haz clic para la página anterior command.help.misc.click_to_show_help=Haz clic para mostrar la ayuda de este comando command.help.misc.command=Comando command.help.misc.description=Descripción command.help.misc.help=Ayuda command.help.misc.no_description=Sin descripción command.help.misc.no_results_for_query=No hay resultados para la consulta command.help.misc.optional=Opcional command.help.misc.page_out_of_range=Error\: La página no está en el rango. Debe estar en el rango [1, ] command.help.misc.showing_results_for_query=Mostrando resultados de la búsqueda para la consulta command.ignore.argument.player=El nombre del jugador a ignorar. command.ignore.argument.uuid=El UUID del jugador a ignorar. command.ignore.description=Oculta todos los mensajes entrantes de los jugadores ignorados. command.ignorelist.description=Muestra una lista paginada de los jugadores que estás ignorando. command.ignorelist.none_ignored=No estás ignorando a ningún jugador. command.ignorelist.pagination_header=Jugadores ignorados command.ignorelist.pagination_element= - ''>''>[dejar de ignorar] command.join.description=Únete a un canal que has abandonado previamente. command.leave.description=Abandona un canal al que actualmente tienes acceso. command.mute.argument.player=El nombre del jugador a silenciar. command.mute.argument.uuid=El UUID del jugador a silenciar. command.mute.argument.duration=La duración por la que el jugador será silenciado. command.mute.description=Silencia a los jugadores, impidiéndoles usar el chat o susurrar a otros jugadores. command.muteinfo.argument.player=El nombre del jugador. command.muteinfo.argument.uuid=El UUID del jugador. command.muteinfo.description=Muestra si los jugadores están silenciados o no. command.nickname.argument.nickname=El apodo a establecer. command.nickname.argument.player=El nombre del jugador objetivo. command.nickname.description=Muestra tu apodo. command.nickname.set.description=Establece tu apodo. command.nickname.reset.description=Elimina tu apodo. command.nickname.others.description=Muestra los apodos de los jugadores. command.nickname.others.set.description=Establece los apodos de los jugadores. command.nickname.others.reset.description=Elimina cualquier apodo establecido del objetivo. command.reload.description=Recarga la configuración de Carbon, los ajustes de los canales y las traducciones. No cargará ni descargará ningún canal. command.reply.argument.message=El mensaje para responder. command.reply.description=Envía un mensaje al último jugador que te envió un mensaje. command.togglemsg.description=Permite y no permite que otros jugadores te envíen mensajes. command.unignore.argument.player=El nombre del jugador para dejar de ignorar. command.unignore.argument.uuid=El UUID del jugador para dejar de ignorar. command.unignore.description=Deja de ocultar los mensajes del jugador especificado. command.unmute.argument.player=El nombre del jugador para desilenciar. command.unmute.argument.uuid=El UUID del jugador para desilenciar. command.unmute.description=Desilencia a los jugadores, permitiéndoles usar el chat y susurrar a otros jugadores. command.updateusername.argument.player=El nombre del jugador a actualizar. command.updateusername.argument.uuid=El UUID del jugador a actualizar. command.updateusername.description=Actualiza el nombre de usuario del jugador para que coincida con su nombre de Mojang. command.updateusername.fetching=Obteniendo nombre de usuario... command.updateusername.notupdated=No se pudo obtener el nombre de usuario. command.updateusername.updated=¡Nombre de usuario de actualizado\! command.whisper.argument.message=El mensaje a enviar. command.whisper.argument.player=El nombre del jugador al que enviar el mensaje. command.whisper.description=Envía un mensaje privado al jugador especificado. command.party.pagination_header=Miembros del grupo\: command.party.pagination_element=''\:''''> - command.party.created=¡Grupo creado y unido exitosamente ''''\! command.party.not_in_party=No estás en un grupo. Usa ''/party create'' para crear uno, o ''/party accept'' para aceptar una invitación. command.party.current_party=Estás en el grupo\: command.party.must_leave_current_first=Debes abandonar tu grupo actual primero. command.party.name_too_long=El nombre del grupo es demasiado largo. command.party.received_invite=Haz clic para aceptar''>''>Fuiste invitado al grupo '''' por . Haz clic en este mensaje para aceptar. command.party.sent_invite=Invitación de grupo enviada a . command.party.must_specify_invite=Debes especificar a quién aceptar la invitación. command.party.no_pending_invites=No tienes invitaciones de grupo pendientes. command.party.no_invite_from=No tienes una invitación pendiente de . command.party.joined_party=¡Te has unido exitosamente al grupo ''''\! command.party.left_party=Has abandonado exitosamente el grupo ''''. command.party.disbanded=Grupo disuelto exitosamente ''''. command.party.cannot_disband_multiple_members=No puedes disolver el grupo '''', no eres el último miembro. command.party.must_be_in_party=Debes estar en un grupo para usar este comando. Usa ''/party create'' para crear uno, o ''/party accept'' para aceptar una invitación. command.party.cannot_invite_self=No puedes invitarte a ti mismo. command.party.description=Obtén información y ve los miembros de tu grupo actual. command.party.create.description=Crea un nuevo grupo. command.party.invite.description=Invita a un jugador a tu grupo. command.party.accept.description=Acepta invitaciones de grupo. command.party.leave.description=Abandona tu grupo actual. command.party.already_in_party= ya está en tu grupo. command.party.disband.description=Disuelve tu grupo actual. command.realname.description=Muestra el nombre real del jugador. command.spy.enabled=El espionaje está ahora activado. command.spy.disabled=El espionaje está ahora desactivado. command.spy.description=Permite a un jugador ver todos los mensajes privados y de canal que de otra manera no vería. duration.days=dhms duration.hours=hms party.player_joined= se ha unido a tu grupo. party.player_left= ha abandonado tu grupo. party.cannot_use_channel=Debes unirte a un grupo para usar este canal. party.spy=Espía [\: ] config.reload.failed=Error al recargar la configuración config.reload.success=Configuración recargada exitosamente error.command.argument_parsing=Argumento de comando inválido\: error.command.command_execution=\n\n Haz clic para copiar">''>Ocurrió un error interno al intentar ejecutar este comando. error.command.invalid_player=No se encontró ningún jugador para la entrada '''' error.command.invalid_sender=Remitente de comando inválido. Debes ser de tipo error.command.invalid_syntax=Sintaxis de comando inválida. La sintaxis correcta es\: / error.command.no_permission=Lo siento, pero no tienes permiso para ejecutar este comando.\nPor favor, contacta a los administradores del servidor si crees que esto es un error. error.command.command_needs_player=Los no jugadores deben proporcionar el argumento del jugador para ejecutar este comando. ignore.already_ignored=Ya estás ignorando a ignore.not_ignored=No estás ignorando a ignore.exempt=No puedes ignorar a ignore.invalid_target=No se encontró el objetivo ignore.now_ignoring=Ahora estás ignorando a ignore.no_longer_ignoring=Ya no estás ignorando a mute.alert.players= ha sido silenciado mute.alert.players.temp= ha sido silenciado por mute.alert.target=Has sido silenciado mute.alert.target.temp=Has sido silenciado por mute.cannot_speak=No puedes hablar cuando estás silenciado mute.exempt=Ese jugador está exento de ser silenciado mute.info.muted= está silenciado mute.info.muted.duration= está silenciado durante mute.info.not_muted= no está silenciado mute.info.self.muted=Estás silenciado mute.info.self.not_muted=No estás silenciado mute.no_target=No se especificó ningún jugador para silenciar. mute.spy.prefix=Silenciado''>S mute.unmute.alert.players= ha sido desilenciado mute.unmute.alert.target=Has sido desilenciado mute.unmute.no_target=No se especificó ningún jugador para desilenciar. nickname.reset.others=El apodo de ha sido restablecido nickname.reset=Tu apodo ha sido restablecido nickname.set.others=Has establecido el apodo de a nickname.set=Tu apodo ha sido establecido a nickname.show.others.unset= no tiene un apodo establecido nickname.show.others=El apodo de es nickname.show.unset=No tienes un apodo establecido nickname.show=Tu apodo es nickname.error.character_limit=El apodo "" ha excedido el límite de caracteres. Debe tener entre ~ caracteres. nickname.error.blacklist=El apodo "" no está permitido. Por favor, elige otro nombre. nickname.error.filter=¡Los apodos deben ser alfanuméricos\! reply.target.missing=No tienes a nadie a quien responder reply.target.self=No puedes susurrarte a ti mismo whisper.console=[] -> [] whisper.continue.target_missing=No tienes a nadie a quien susurrar whisper.error=Error al enviar mensaje privado whisper.from= ''>[] -> [] whisper.from.spy=ESPÍA [] -> [] whisper.ignored_by_target= te está ignorando whisper.ignoring_target=Estás ignorando a whisper.ignoring_all=¡No puedes enviar mensajes mientras los ignoras\! whisper.no_permission.receive=¡Ese jugador no tiene permiso para recibir mensajes\! whisper.no_permission.send=¡No tienes permiso para enviar susurros\! whisper.to= ''>''>[] -> [] whisper.toggled.on=Ahora recibes mensajes privados. whisper.toggled.off=Ya no recibes mensajes privados. channel.cooldown=¡Podrás usar el chat nuevamente en segundos\! channel.radius.empty_recipients=No estás lo suficientemente cerca de nadie para enviar un mensaje channel.radius.spy=Espía [\: ] channel.joined=Has vuelto a unirte al canal channel.left=Has abandonado el canal channel.no_permission=No tienes permiso para usar este canal channel.already_left=Ya has abandonado este canal channel.not_left=No has abandonado este canal channel.not_found=Canal no encontrado pagination.page_out_of_range=¡La página está fuera de rango\! Solo hay páginas. pagination.click_for_next_page=Haz clic para la siguiente página pagination.click_for_previous_page=Haz clic para la página anterior pagination.footer=Página / integrations.towny.cannot_use_alliance_channel=Debes unirte a una alianza para usar este canal. integrations.towny.cannot_use_nation_channel=Debes unirte a una nación para usar este canal. integrations.towny.cannot_use_town_channel=Debes unirte a una ciudad para usar este canal. integrations.mcmmo.cannot_use_party_channel=Debes unirte a un grupo de mcMMO para usar este canal. integrations.fuuid.cannot_use_faction_channel=Debes unirte a una facción para usar este canal. integrations.fuuid.cannot_use_alliance_channel=Debes unirte a una alianza para usar este canal. integrations.fuuid.cannot_use_truce_channel=Debes tener una tregua con otra facción para usar este canal. integrations.fuuid.cannot_use_mod_channel=Debes ser un mod/admin de facción para usar este canal. integrations.plotsquared.cannot_use_plot_channel=Debes estar en una parcela para usar este canal. ================================================ FILE: common/src/main/resources/locale/messages-fi_FI.properties ================================================ ================================================ FILE: common/src/main/resources/locale/messages-fr_CA.properties ================================================ ================================================ FILE: common/src/main/resources/locale/messages-fr_FR.properties ================================================ integrations.plotsquared.cannot_use_plot_channel=Vous devez être dans un plot pour utiliser ce canal. ================================================ FILE: common/src/main/resources/locale/messages-ja_JP.properties ================================================ channel.change=メッセージを送信中 command.clearchat.description=すべてのプレイヤーのチャットウィンドウをクリアします。 command.continue.argument.message=送信するメッセージです。 command.continue.description=最後にメッセージを送った人にメッセージを送信します。 command.debug.argument.player=プレーヤーのグループをチェックする。 command.debug.description=プレイヤーの権限グループを表示します。 command.help.argument.query=検索クエリ。 command.help.description=Carbonのコマンドリストです。 command.help.misc.arguments=引数 command.help.misc.available_commands=利用可能なコマンド一覧 command.help.misc.click_for_next_page=クリックで次のページへ command.help.misc.click_for_previous_page=クリックで前のページへ command.help.misc.click_to_show_help=クリックしてこのコマンドのヘルプを表示 command.help.misc.command=コマンド command.help.misc.description=説明 command.help.misc.help=ヘルプ command.help.misc.no_description=説明なし command.help.misc.no_results_for_query=クエリの結果はありません command.help.misc.optional=オプション command.help.misc.page_out_of_range=エラー\: ページ は範囲外です。[1, ] の範囲内でなければなりません。 command.help.misc.showing_results_for_query=クエリの検索結果を表示中 command.ignore.argument.player=無視するプレーヤーの名前。 command.ignore.argument.uuid=無視するプレーヤーのUUID。 command.ignore.description=無視したプレイヤーからのすべてのメッセージを非表示にします。 command.ignorelist.description=あなたが無視しているプレイヤーのリストをページ順に表示する。 command.ignorelist.none_ignored=あなたは他のプレイヤーを無視していません。 command.ignorelist.pagination_header=無視したプレイヤー command.ignorelist.pagination_element= - ''>''>[unignore] command.join.description=以前に退出したチャンネルに加入する。 command.leave.description=現在アクセスしているチャンネルから退出する。 command.mute.argument.player=ミュートするプレーヤーの名前。 command.mute.argument.uuid=ミュートするプレーヤーのUUID。 command.mute.description=プレイヤーをミュートし、チャットや他のプレイヤーへのプライベートメッセージを禁止します。 command.muteinfo.argument.player=プレイヤーの名前。 command.muteinfo.argument.uuid=プレイヤーのUUID。 command.muteinfo.description=プレイヤーがミュートかどうかを表示します。 command.nickname.argument.nickname=設定するニックネーム。 command.nickname.argument.player=ターゲットプレイヤーの名前。このフラグがなければ、送信者がターゲットになります。 command.nickname.description=プレイヤーのニックネームを設定および表示します。 command.nickname.set.description=あなたのニックネームを設定する。 command.nickname.reset.description=あなたのニックネームを削除する。 command.nickname.others.description=プレイヤーのニックネームを表示する。 command.nickname.others.set.description=プレイヤーのニックネームを設定する。 command.nickname.others.reset.description=ターゲットから設定されたニックネームを削除する。 command.reload.description=Carbonの設定、チャンネル設定、翻訳をリロードします。チャンネルをロードしたり、アンロードしたりしません。 command.reply.argument.message=返信するメッセージ。 command.reply.description=最後にメッセージを送ったプレイヤーにメッセージを送信します。 command.togglemsg.description=他のプレイヤーからあなたへのメッセージを許可/拒否する。 command.unignore.argument.player=無視を解除するプレーヤーの名前。 command.unignore.argument.uuid=無視を解除するプレイヤーのUUID。 command.unignore.description=指定したプレイヤーからのメッセージの非表示を停止します。 command.unmute.argument.player=ミュートを解除するプレーヤーの名前。 command.unmute.argument.uuid=ミュートを解除するプレーヤーのUUID。 command.unmute.description=プレイヤーのミュートを解除し、チャットや他のプレイヤーへのプライベートメッセージを使用できるようにします。 command.updateusername.argument.player=更新するプレイヤーの名前。 command.updateusername.argument.uuid=更新するプレーヤーのuuid。 command.updateusername.description=プレイヤーのユーザー名をmojangの名前と一致するように更新する。 command.updateusername.fetching=ユーザー名を取得中... command.updateusername.notupdated=ユーザー名を取得できません。 command.updateusername.updated=のユーザー名を更新しました! command.whisper.argument.message=送信するメッセージ。 command.whisper.argument.player=メッセージを送信するプレーヤーの名前。 command.whisper.description=指定したプレーヤーにプライベートメッセージを送信します。 command.party.pagination_header=パーティーメンバー\: command.party.pagination_element=''\:''''> - command.party.created=パーティー「」の作成と参加に成功しました! command.party.not_in_party=あなたはパーティーに参加していません。パーティを作成するには「/party create」を、招待を承認するには「/party accept」を使用して下さい。 command.party.current_party=あなたが参加しているパーティー\: command.party.must_leave_current_first=あなたはまず現在のパーティーから退出しなければなりません。 command.party.name_too_long=パーティー名が長すぎます。 command.party.received_invite=クリックして承認''>''>あなたはからパーティー「」に招待されました。承認するにはこのメッセージをクリックして下さい。 command.party.sent_invite=にパーティーの招待状を送信しました。 command.party.must_specify_invite=あなたは誰の招待を承認するか指定しなければなりません。 command.party.no_pending_invites=あなたには保留中のパーティー招待状はありません。 command.party.no_invite_from=あなたはからの保留中の招待状はありません。 command.party.joined_party=パーティー「」の参加に成功しました! command.party.left_party=パーティー「」の退出に成功しました! command.party.disbanded=パーティー「」の解散に成功しました! command.party.cannot_disband_multiple_members=あなたは最後のメンバーではないため、パーティー「」を解散できません。 command.party.must_be_in_party=このコマンドを使うには、パーティーに入っていなければなりません。パーティを作成するには「/party create」を、招待を承認するには「/party accept」を使用して下さい。 command.party.cannot_invite_self=自分を招待することはできません。 command.party.description=現在参加しているパーティーのメンバー情報を確認する。 command.party.create.description=新しいパーティーを作成する。 command.party.invite.description=プレイヤーをあなたのパーティーに招待する。 command.party.accept.description=パーティーへの招待を承認する。 command.party.leave.description=現在のパーティーから退出する。 command.party.already_in_party=は既にあなたのパーティーに参加しています。 command.party.disband.description=現在のパーティーを解散する。 party.player_joined=があなたのパーティーに参加しました。 party.player_left=があなたのパーティーを退出しました。 party.cannot_use_channel=このチャンネルを使用するには、あなたはパーティーに参加する必要があります。 config.reload.failed=コンフィグのリロードに失敗 config.reload.success=コンフィグのリロードに成功 error.command.argument_parsing=無効なコマンド引数: error.command.command_execution=\n\n クリックしてコピー">>このコマンドの実行中に内部エラーが発生しました。 error.command.invalid_player=入力したプレイヤー''''が見つかりません error.command.invalid_sender=無効なコマンド送信者。 型である必要があります error.command.invalid_syntax=無効なコマンド構文です。正しいコマンド構文\: / error.command.no_permission=申し訳ありませんが、このコマンドを実行する権限がありません。これがエラーだと思われる場合は、サーバー管理者にお問い合わせください。 error.command.command_needs_player=非プレイヤーがこのコマンドを実行するには、プレーヤーの引数を指定しなければなりません。 ignore.already_ignored=あなたは既にを無視しています ignore.not_ignored=あなたはを無視していません ignore.exempt=あなたはを無視する事はできません ignore.invalid_target=ターゲットが見つかりません ignore.now_ignoring=あなたはを無視しています ignore.no_longer_ignoring=あなたはを無視しなくなりました mute.alert.players=がミュートされました mute.alert.target=あなたはミュートされています mute.cannot_speak=ミュート時は話すことができません mute.exempt=そのプレイヤーはミュートされることを免除されています mute.info.muted=はミュートされています。 mute.info.not_muted=はミュートされていません mute.info.self.muted=あなたはミュートされています mute.info.self.not_muted=あなたはミュートされていません mute.no_target=ミュートするプレイヤーが指定されていません。 mute.spy.prefix=ミュートM mute.unmute.alert.players=のミュートが解除されました mute.unmute.alert.target=あなたのミュートが解除されました mute.unmute.no_target=ミュート解除するプレイヤーが指定されていません。 nickname.reset.others=のニックネームがリセットされました nickname.reset=あなたのニックネームがリセットされました nickname.set.others=のニックネームをに設定しました nickname.set=あなたのニックネームがに設定されました nickname.show.others.unset=にはニックネームが設定されていません nickname.show.others=のニックネームはです nickname.show.unset=あなたはニックネームが設定されていません nickname.show=あなたのニックネームはです nickname.error.character_limit=ニックネーム「」は文字数制限を超えています。 文字に設定する必要があります。 nickname.error.blacklist=ニックネーム「」は使用できません。他の名前を入力して下さい。 reply.target.missing=返信できる人がいません reply.target.self=自分自身にプライベートメッセージを送信することはできません whisper.console=[] -> [] whisper.continue.target_missing=プライベートメッセージを送る相手がいません whisper.error=プライベートメッセージの送信に失敗しました。 whisper.from=[] -> [あなた] whisper.ignored_by_target=はあなたを無視しています whisper.ignoring_target=あなたはを無視しています whisper.ignoring_all=無視されている間はメッセージを送信できません! whisper.to=[あなた] -> [] whisper.toggled.on=プライベートメッセージを受信しています。 whisper.toggled.off=プライベートメッセージを受信しなくなりました。 channel.radius.empty_recipients=メッセージを送信できる人が近くには誰もいません。 channel.joined=チャンネルに再加入しました channel.left=あなたはチャンネルから退出しました channel.no_permission=あなたにはこのチャンネルを使用する権限がありません channel.already_left=あなたはすでにこのチャンネルから退出しています channel.not_left=あなたはこのチャンネルから退出していません channel.not_found=チャンネルが見つかりません pagination.page_out_of_range= ページは範囲外です!ページまでです。 pagination.click_for_next_page=クリックして次のページへ pagination.click_for_previous_page=クリックして前のページへ pagination.footer=ページ / integrations.towny.cannot_use_alliance_channel=このチャンネルを利用するには、同盟に参加する必要があります。 integrations.towny.cannot_use_nation_channel=このチャンネルを利用するには、国家に参加する必要があります。 integrations.towny.cannot_use_town_channel=このチャンネルを利用するには、町に参加する必要があります。 integrations.mcmmo.cannot_use_party_channel=このチャンネルを利用するには、mcMMOパーティーに参加する必要があります。 integrations.fuuid.cannot_use_faction_channel=このチャンネルを利用するには、派閥に参加する必要があります。 integrations.fuuid.cannot_use_alliance_channel=このチャンネルを利用するには、同盟に参加する必要があります。 integrations.fuuid.cannot_use_truce_channel=このチャンネルを利用するには、他の派閥と休戦する必要があります。 ================================================ FILE: common/src/main/resources/locale/messages-nl_NL.properties ================================================ channel.change=Je stuurt nu berichten naar command.clearchat.description=Wist het chatvenster voor alle spelers. command.continue.argument.message=Het te verzenden bericht. command.continue.description=Stuurt een bericht naar de laatste persoon die je een bericht hebt gestuurd. command.debug.argument.player=De speler om de groepen van te controleren. command.debug.description=Toont de groepen van de spelers. command.help.argument.query=De zoekopdracht. command.help.description=Carbon command lijst. command.help.misc.arguments=Argumenten command.help.misc.available_commands=Beschikbare command''s command.help.misc.click_for_next_page=Klik voor de volgende pagina command.help.misc.click_for_previous_page=Klik voor de vorige pagina command.help.misc.click_to_show_help=Klik om hulp voor deze command te tonen command.help.misc.description=Beschrijving command.help.misc.help=Hulp command.help.misc.no_description=Geen beschrijving command.help.misc.no_results_for_query=Geen zoekresultaten command.help.misc.optional=Optioneel command.help.misc.page_out_of_range=Fout\: Pagina is niet in bereik. Moet tussen bereik [1, ] zijn command.help.misc.showing_results_for_query=Zoekresultaten voor query weergeven command.ignore.argument.player=De naam van de speler om te negeren. command.ignore.argument.uuid=Het UUID van de speler om te negeren. command.ignore.description=Verbergt alle inkomende berichten van genegeerde spelers. command.mute.argument.player=De naam van de speler om te negeren. command.mute.argument.uuid=Het UUID van de speler om te negeren. command.mute.description=Mute spelers, vermijd dat ze chat gebruiken of andere spelers een prive bericht sturen. command.muteinfo.argument.player=De naam van de speler om te muten. command.muteinfo.argument.uuid=Het UUID van de speler om te negeren. command.muteinfo.description=Laat zien of spelers gemute zijn of niet. command.nickname.argument.nickname=De in te stellen bijnaam. command.nickname.description=Zet en laat speler bijnaam zien. command.reload.description=Herlaadt Carbon''s configuratie, kanaalinstellingen en vertalingen. Dit zal geen kanalen laden of ontladen. command.reply.argument.message=Het bericht waarmee u wilt antwoorden. command.reply.description=Stuurt een bericht naar de laatste persoon waar je een bericht van hebt ontvangen. command.unignore.argument.player=De naam van de speler om te stoppen met negeren. command.unignore.argument.uuid=Het UUID van de speler om te stoppen met negeren. command.unignore.description=Stopt het verbergen van berichten van de opgegeven speler. command.unmute.argument.player=De naam van de speler om te stoppen met negeren. command.unmute.argument.uuid=Het UUID van de speler om te stoppen met negeren. command.unmute.description=Un-mute spelers, geef ze toegang om chat te gebruiken en andere spelers een prive bericht te sturen. command.whisper.argument.message=Het te verzenden bericht. command.whisper.argument.player=De naam van de speler om een bericht naar te sturen. command.whisper.description=Stuurt een privébericht naar de opgegeven speler. config.reload.failed=Configuratie kon niet herladen worden config.reload.success=Configuratie herladen geslaagd error.command.argument_parsing=Ongeldig command argument\: error.command.command_execution=\n\n Klik om te kopiëren ">>Er is een interne fout opgetreden tijdens het proberen van deze command. error.command.invalid_player=Geen speler gevonden voor invoer '''' error.command.invalid_sender=Ongeldige command zender. Je moet van type zijn error.command.invalid_syntax=Ongeldige command syntax. Het correcte command syntax\: / error.command.no_permission=Sorry, maar je hebt geen toestemming om dit commando uit te voeren.\nNeem contact op met de server beheerders als je denkt dat dit een fout is. ignore.exempt=Je kan niet negeren ignore.invalid_target=Geen doel gevonden ignore.now_ignoring=Je bent nu aan het negeren ignore.no_longer_ignoring=Je bent niet langer aan het negeren mute.alert.players= is gemute mute.alert.target=Je bent gemute mute.cannot_speak=Je kan niet praten wanneer je gemute bent mute.exempt=Die speler is vrijgesteld van gemute te worden mute.info.muted= is gemute mute.info.not_muted= is niet gemute mute.info.self.muted=Je bent gemute mute.info.self.not_muted=Je bent niet gemute mute.no_target=Er is geen speler opgegeven om te muten. mute.spy.prefix=GemuteM mute.unmute.alert.players= is gemute mute.unmute.alert.target=Je bent niet langer gemute mute.unmute.no_target=Geen speler gespecifieerd om te umuten. nickname.reset.others=''s bijnaam is opnieuw ingesteld nickname.reset=Jou bijnaam is opnieuw ingesteld nickname.set.others=Je hebt ''s bijnaam ingesteld op nickname.set=Je bijnaam is ingesteld op nickname.show.others.unset= heeft geen bijnaam ingesteld nickname.show.others=''s bijnaam is nickname.show.unset=Je hebt geen bijnaam ingesteld nickname.show=Je bijnaam is reply.target.missing=Je hebt niemand om te beantwoorden reply.target.self=Je kan jezelf geen privebericht sturen whisper.continue.target_missing=Je hebt niemand om een privebericht naar te sturen whisper.ignored_by_target= is je aan het negeren whisper.ignoring_target=Jij negeert ================================================ FILE: common/src/main/resources/locale/messages-nn_NO.properties ================================================ ================================================ FILE: common/src/main/resources/locale/messages-no_NO.properties ================================================ ================================================ FILE: common/src/main/resources/locale/messages-pl_PL.properties ================================================ ================================================ FILE: common/src/main/resources/locale/messages-pt_BR.properties ================================================ ================================================ FILE: common/src/main/resources/locale/messages-ru_RU.properties ================================================ ================================================ FILE: common/src/main/resources/locale/messages-tr_TR.properties ================================================ channel.change=Artık ile iletişim kuruyorsunuz. command.clearchat.description=Tüm oyuncuların sohbet penceresini temizler. command.continue.argument.message=Gönderilecek mesaj. command.continue.description=En son iletişim kurduğunuz kişiye bir mesaj gönderir. command.debug.argument.player=Gruplarını kontrol etmek istediğiniz oyuncu. command.debug.description=Oyuncuların izin gruplarını gösterir. command.help.argument.query=Arama sorgusu. command.help.description=Carbon komut listesi. command.help.misc.arguments=Argümanlar command.help.misc.available_commands=Kullanılabilir Komutlar command.help.misc.click_for_next_page=Sonraki sayfa için tıklayın command.help.misc.click_for_previous_page=Önceki sayfa için tıklayın command.help.misc.click_to_show_help=Bu komut için yardımı göstermek için tıklayın command.help.misc.command=Komut command.help.misc.description=Açıklama command.help.misc.help=Yardım command.help.misc.no_description=Açıklama yok command.help.misc.no_results_for_query=Sorgu için sonuç bulunamadı command.help.misc.optional=İsteğe bağlı command.help.misc.page_out_of_range=Hata\: Sayfa , aralıkta değil. Aralık [1, ] içinde olmalıdır. command.help.misc.showing_results_for_query=Sorgu için sonuçları gösteriliyor command.ignore.argument.player=Ignore edilecek oyuncunun adı. command.ignore.argument.uuid=Ignore edilecek oyuncunun UUID''si. command.ignore.description=Ignore edilen oyunculardan gelen tüm iletileri gizler. command.ignorelist.description=Ignore listesindeki oyuncuların sayfalandırılmış bir listesini gösterir. command.ignorelist.none_ignored=Hiçbir oyuncuyu ignore etmiyorsunuz. command.ignorelist.pagination_header=Ignore Edilen Oyuncular command.ignorelist.pagination_element= - ''> ''> kullanıcısını unignore etmek için tıklayın''>[unignore] command.join.description=Daha önce ayrıldığınız bir kanala katılın. command.leave.description=Şu anda erişiminiz olan bir kanaldan ayrılın. command.mute.argument.player=Susturulacak oyuncunun adı. command.mute.argument.uuid=Susturulacak oyuncunun UUID''si. command.mute.description=Oyuncuları susturur, onların sohbeti kullanmalarını veya diğer oyunculara fısıltı göndermelerini engeller. command.muteinfo.argument.player=Oyuncunun adı. command.muteinfo.argument.uuid=Oyuncunun UUID''si. command.muteinfo.description=Oyuncuların susturulup susturulmadığını gösterir. command.nickname.argument.nickname=Ayarlanacak takma ad. command.nickname.argument.player=Hedef oyuncunun adı. command.nickname.description=Takma adınızı gösterir. command.nickname.set.description=Takma adınızı ayarlar. command.nickname.reset.description=Takma adınızı kaldırır. command.nickname.others.description=Oyuncu takma adlarını gösterir. command.nickname.others.set.description=Oyuncu takma adlarını ayarlar. command.nickname.others.reset.description=Hedeften ayarlanmış herhangi bir takma adı kaldırır. command.reload.description=Carbon''un yapılandırma, kanal ayarları ve çevirilerini yeniden yükler. Kanalları yüklemeye veya boşaltmaya gitmez. command.reply.argument.message=Yanıt olarak gönderilecek ileti. command.reply.description=Size en son mesaj gönderen oyuncuya bir ileti gönderir. command.togglemsg.description=Diğer oyuncuların size mesaj göndermesine izin verir veya engeller. command.unignore.argument.player=Ignore''un kaldırılacağı oyuncunun adı. command.unignore.argument.uuid=Ignore''un kaldırılacağı oyuncunun UUID''si. command.unignore.description=Belirtilen oyuncunun mesajlarını gizlemeyi bırakır. command.unmute.argument.player=Susturmanın kaldırılacağı oyuncunun adı. command.unmute.argument.uuid=Susturmanın kaldırılacağı oyuncunun UUID''si. command.unmute.description=Oyuncuların susturmasını kaldırır, onların sohbeti kullanmalarına ve diğer oyunculara fısıltı göndermelerine izin verir. command.updateusername.argument.player=Güncellenecek oyuncunun adı. command.updateusername.argument.uuid=Oyuncunun UUID''si. command.updateusername.description=Oyuncunun adını Mojang adıyla eşleşecek şekilde günceller. command.updateusername.fetching=Kullanıcı adı alınıyor... command.updateusername.notupdated=Kullanıcı adı alınamıyor. command.updateusername.updated=''nin kullanıcı adı güncellendi\! command.whisper.argument.message=Gönderilecek ileti. command.whisper.argument.player=İletişim kurmak istediğiniz oyuncunun adı. command.whisper.description=Belirtilen oyuncuya özel bir ileti gönderir. config.reload.failed=Yapılandırma yeniden yüklenemedi. config.reload.success=Yapılandırma başarıyla yeniden yüklendi. error.command.argument_parsing=Geçersiz komut argümanı\: error.command.command_execution=\n\n Kopyalamak için tıklayın">>Komutu çalıştırmaya çalışırken içsel bir hata oluştu. error.command.invalid_player='''' için oyuncu bulunamadı. error.command.invalid_sender=Geçersiz komut gönderen. Tür olmalıdır. error.command.invalid_syntax=Geçersiz komut sözdizimi. Doğru komut sözdizimi\: / error.command.no_permission=Üzgünüm, bu komutu gerçekleştirmek için izniniz yok.\nEğer bunun hata olduğuna inanıyorsanız lütfen sunucu yöneticileri ile iletişime geçin. error.command.command_needs_player=Oyuncuların bu komutu çalıştırmak için oyuncu argümanı sağlaması gerekir. ignore.already_ignored=Zaten ''i ignore ediyorsunuz. ignore.not_ignored=''i ignore etmiyorsunuz. ignore.exempt=Bu hedef ignore edilemez. ignore.invalid_target=Hedef bulunamadı. ignore.now_ignoring=Artık ''i ignore ediyorsunuz. ignore.no_longer_ignoring=Artık ''i ignore etmiyorsunuz. mute.alert.players= susturuldu. mute.alert.target=Siz susturuldunuz. mute.cannot_speak=Susturulduğunuzda konuşamazsınız. mute.exempt=Bu oyuncunun susturulmasını engelleyemezsiniz. mute.info.muted= susturulmuş durumda. mute.info.not_muted= susturulmamış durumda. mute.info.self.muted=Siz susturuldunuz. mute.info.self.not_muted=Siz susturulmamış durumdasınız. mute.no_target=Susturulacak belirli bir oyuncu yok. mute.spy.prefix=Susturuldu''>S mute.unmute.alert.players= susturulması kaldırıldı. mute.unmute.alert.target=Siz artık susturulmamış durumdasınız. mute.unmute.no_target=Susturulacak belirli bir oyuncu yok. nickname.reset.others=''in takma adı sıfırlandı. nickname.reset=Takma adınız sıfırlandı. nickname.set.others=Siz ''in takma adını olarak ayarladınız. nickname.set=Takma adınız olarak ayarlandı. nickname.show.others.unset=''in ayarlanmış bir takma adı yok. nickname.show.others=''in takma adı . nickname.show.unset=Sizin ayarlanmış bir takma adınız yok. nickname.show=Takma adınız . reply.target.missing=Cevap verecek kimse bulunmamaktadır. reply.target.self=Kendinize fısıltı gönderemezsiniz. whisper.continue.target_missing=Yanıt verecek kimse bulunmamaktadır. whisper.error=Özel ileti gönderme başarısız oldu. whisper.ignored_by_target= sizin mesajlarınızı görmezden geliyor. whisper.ignoring_target= sizin mesajlarınızı görmezden geliyor. whisper.ignoring_all=Ignore ediliyor durumdayken mesaj gönderemezsiniz\! whisper.toggled.on=Artık özel mesajlar alıyorsunuz. whisper.toggled.off=Artık özel mesajlar almıyorsunuz. channel.radius.empty_recipients=Bir mesaj göndermek için kimseye yeterince yakın değilsiniz. channel.joined=Kanala tekrar katıldınız. channel.left=Kanaldan ayrıldınız. channel.no_permission=Bu kanalı kullanma izniniz yok. channel.already_left=Zaten bu kanaldan ayrıldınız. channel.not_left=Bu kanaldan ayrılmadınız. channel.not_found=Kanal bulunamadı. pagination.page_out_of_range=Sayfa aralık dışında\! Sadece sayfa var.. pagination.click_for_next_page=Sıradaki sayfa için tıklayın. pagination.click_for_previous_page=Önceki sayfa için tıklayın. pagination.footer=Sayfa / . ================================================ FILE: common/src/main/resources/locale/messages-uk_UA.properties ================================================ channel.change=Тепер ви пишете в command.clearchat.description=Очищає вікно чату для всіх гравців. command.continue.argument.message=Повідомлення для відправлення. command.continue.description=Надсилає повідомлення останній особі, якій ви писали. command.debug.argument.player=The player to check the groups of. command.debug.description=Показує групи дозволів гравців. command.filter.optional.enabled=>Додатковий фільтр чату увімкнено\! command.filter.optional.disabled=Додатковий фільтр чату вимкнено\! command.filter.optional.description=Увімкнути або вимкнути додатковий фільтр чату. command.help.argument.query=Пошуковий запит. command.help.description=Список команд Carbon command.help.misc.arguments=Аргументи command.help.misc.available_commands=Доступні команди command.help.misc.click_for_next_page=Натисніть, щоб перейти на наступну сторінку command.help.misc.click_for_previous_page=Натисніть, щоб перейти на попередню сторінку command.help.misc.click_to_show_help=Натисніть, щоб показати довідку для цієї команди command.help.misc.command=Команда command.help.misc.description=Опис command.help.misc.help=Довідка command.help.misc.no_description=Немає опису command.help.misc.no_results_for_query=Немає результатів за запитом command.help.misc.optional=Додаткове command.help.misc.page_out_of_range=Помилка\: Сторінка не входить в діапазон. Вона повинна бути в діапазоні [1, ] command.help.misc.showing_results_for_query=Показ результатів за запитом command.ignore.argument.player=Нікнейм гравця, якого буде проігноровано. command.ignore.argument.uuid=UUID гравця, якого буде проігноровано. command.ignore.description=Приховує всі вхідні повідомлення від ігнорованих гравців. command.ignorelist.description=Показує перелік гравців, яких ви ігноруєте. command.ignorelist.none_ignored=Ви не ігноруєте жодних гравців. command.ignorelist.pagination_header=Ігноровані гравці command.ignorelist.pagination_element= - ''>''>[unignore] command.join.description=Приєднатися до каналу, який ви раніше покинули. command.leave.description=Покинути канал, до якого ви маєте доступ. command.mute.argument.player=Нікнейм гравця, якого буде замучено. command.mute.argument.uuid=UUID гравця, якого буде замучено. command.mute.argument.duration=Тривалість муту. command.mute.description=Мутить гравців, забороняючи їм писати в чат або надсилати приватні повідомлення. command.muteinfo.argument.player=Нікнейм гравця. command.muteinfo.argument.uuid=UUID гравця. command.muteinfo.description=Показує, чи гравець в муті чи ні. command.nickname.argument.nickname=Нікнейм для встановлення. command.nickname.argument.player=Нікнейм цільового гравця. command.nickname.description=Показує ваш нікнейм. command.nickname.set.description=Встановлює ваш нікнейм. command.nickname.reset.description=Видаляє ваш нікнейм. command.nickname.others.description=Показує нікнейм гравця. command.nickname.others.set.description=Встановлює нікнейм гравцю. command.nickname.others.reset.description=Видаляє встановлений нікнейм цілі. command.reload.description=Перезавантажує конфігурацію Carbon, налаштування каналів і переклади. Канали не будуть завантажені чи виватажені command.reply.argument.message=Повідомлення для відповіді. command.reply.description=Надсилає повідомлення останньому гравцю, який вам писав. command.togglemsg.description=Дозволяє або забороняє іншим гравцям писати вам. command.unignore.argument.player=Нікнейм гравця для зняття ігнорування. command.unignore.argument.uuid=UUID гравця для зняття ігнорування. command.unignore.description=Припиняє ігнорування зазначеного гравця. command.unmute.argument.player=Нікнейм гравця з якого буде знято мут. command.unmute.argument.uuid=UUID гравця з якого буде знято мут. command.unmute.description=Розблоковує гравців, дозволяючи їм писати. command.updateusername.argument.player=Нікнейм гравця, якому буде оновлено нікнейм. command.updateusername.argument.uuid=UUID гравця, якому буде оновлено нікнейм. command.updateusername.description=Оновлює нікнейм гравця відповідно до нікнейму Mojang. command.updateusername.fetching=Отримання нікнейму користувача... command.updateusername.notupdated=Не вдалося отримати нікнейм користувача. command.updateusername.updated=Оновлено нікнейм \! command.whisper.argument.message=Повідомлення для відправлення. command.whisper.argument.player=Нікнейм гравця, якому надіслати повідомлення. command.whisper.description=Надсилає приватне повідомлення вказаному гравцю. command.party.pagination_header=Учасники групи\: command.party.pagination_element=''\:''''> - command.party.created=Успішно створено й приєднано до групи ''''\! command.party.not_in_party=Ви не в групі. Використайте ''/party create'' або ''/party accept''. command.party.current_party=Ви в групі\: command.party.must_leave_current_first=Спочатку покиньте поточну групу. command.party.name_too_long=Назва групи надто довга. command.party.received_invite=Натисніть, щоб прийняти''>''>Вас запросив до групи '''' гравець . Натисніть, щоб прийняти. command.party.sent_invite=Запрошення надіслано гравцю . command.party.must_specify_invite=Вкажіть, чиє запрошення прийняти. command.party.no_pending_invites=Немає активних запрошень. command.party.no_invite_from=Немає запрошення від . command.party.joined_party=Ви приєдналися до групи ''''\! command.party.left_party=Ви покинули групу ''''. command.party.disbanded=Групу '''' розпущено. command.party.cannot_disband_multiple_members=Не можна розпустити групу '''', ви не останній учасник. command.party.must_be_in_party=Ви повинні бути в групі, щоб використовувати цю команду. Використайте ''/party create'' або ''/party accept''. command.party.cannot_invite_self=Не можна запросити самого себе. command.party.description=Інформація про поточну групу. command.party.create.description=Створити нову групу. command.party.invite.description=Запросити гравця до групи. command.party.accept.description=рийняти запрошення до групи. command.party.leave.description=Покинути поточну групу. command.party.already_in_party= вже у вашій групі. command.party.disband.description=Розпустити поточну групу. command.spy.enabled=Режим шпигування увімкнено. command.spy.disabled=Режим шпигування вимкнено. command.spy.description=Дозволяє гравцеві переглядати всі приватні повідомлення та повідомлення каналів, які він інакше не бачив би. duration.days=дгхвс duration.hours=гхвс party.player_joined= приєднався до вашої групи. party.player_left= покинув вашу групу. party.cannot_use_channel=Ви повинні бути в групі, щоб використовувати цей канал. party.spy=Шпигун [\: ] config.reload.failed=Не вдалося перезавантажити конфігурацію config.reload.success=Конфігурацію успішно перезавантажено error.command.argument_parsing=Невірний аргумент команди\: error.command.command_execution=\n\n Натисніть, щоб скопіювати">''>Виникла внутрішня помилка під час виконання цієї команди. error.command.invalid_player=Не знайдено гравця за запитом '''' error.command.invalid_sender=Невірний відправник команди. Ви повинні бути типу error.command.invalid_syntax=Невірний синтаксис команди. Правильний синтаксис\: / error.command.no_permission=На жаль, у вас немає дозволу на виконання цієї команди.\nЗверніться до адміністратора сервера, якщо вважаєте це помилкою. error.command.command_needs_player=Консоль повинна вказати аргумент player для виконання цієї команди. ignore.already_ignored=Ви вже ігноруєте ignore.not_ignored=Ви не ігноруєте ignore.exempt=Ви не можете ігнорувати ignore.invalid_target=Ціль не знайдено ignore.now_ignoring=Ви тепер ігноруєте ignore.no_longer_ignoring=Ви більше не ігноруєте mute.alert.players= був замучений mute.alert.players.temp= був замучений на mute.alert.target=Вас було замучено mute.alert.target.temp=Вас було замучено на mute.cannot_speak=Ви не можете писати, поки замучені mute.exempt=Цього гравця не можна заглушити mute.info.muted= замучений mute.info.not_muted= не замучений mute.info.self.muted=Ви замучений mute.info.self.not_muted=Ви не замучені mute.no_target=Не вказано гравця для муту. mute.spy.prefix=red>Замучений''>M mute.unmute.alert.players=\= розмучено mute.unmute.alert.target=Вас було розмучено mute.unmute.no_target=Не вказано гравця для розмуту. nickname.reset.others= — нікнейм скинуто nickname.reset=Ваш нікнейм скинуто nickname.set.others=Ви встановили нікнейм на nickname.set=Ваш нікнейм встановлено на nickname.show.others.unset= не має встановленого нікнейму nickname.show.others= має нікнейм nickname.show.unset=У вас не встановлено нікнейм nickname.show=Ваш нікнейм\: nickname.error.character_limit=Нікнейм "" перевищує допустиму довжину. Має бути від до символів. nickname.error.blacklist=Нікнейм "" заборонений. Будь ласка, виберіть інший. nickname.error.filter=Нікнейм повинен складатися лише з літер та цифр\! reply.target.missing=Немає адресата для відповіді reply.target.self=Ви не можете надсилати повідомлення самому собі whisper.console=[] -> [] whisper.continue.target_missing=<Немає адресата для приватного повідомлення whisper.error=Не вдалося надіслати приватне повідомлення whisper.from= ''>[] -> [Ви] whisper.from.spy=ШПИГУН [] -> [] whisper.ignored_by_target= ігнорує вас whisper.ignoring_target=Ви ігноруєте whisper.ignoring_all=Ви не можете надсилати повідомлення, поки ви всіх ігноруєте\! whisper.no_permission.receive=Цей гравець не має дозволу отримувати повідомлення\! whisper.no_permission.send=У вас немає дозволу на відправлення приватних повідомлень\! whisper.to= ''>''>[Ви] -> [] whisper.toggled.on=Прийом приватних повідомлень увімкнено. whisper.toggled.off=Прийом приватних повідомлень вимкнено. channel.cooldown=Ви зможете знову писати через секунд\! channel.radius.empty_recipients=Вас ніхто не почув channel.radius.spy=Шпигун [\: ] channel.joined=Ви знову приєдналися до каналу channel.left=Ви покинули канал channel.no_permission=У вас немає дозволу на використання цього каналу channel.already_left=Ви вже покинули цей канал channel.not_left=Ви не покидали цей канал channel.not_found=Канал не знайдено pagination.page_out_of_range=Сторінка поза діапазоном\! Всього сторінок\: . pagination.click_for_next_page=Натисніть, щоб перейти на наступну сторінку pagination.click_for_previous_page=Натисніть, щоб перейти на попередню сторінку pagination.footer=Сторінка / integrations.towny.cannot_use_alliance_channel=Ви повинні приєднатися до альянсу, щоб використовувати цей канал. integrations.towny.cannot_use_nation_channel=Ви повинні приєднатися до нації, щоб використовувати цей канал. integrations.towny.cannot_use_town_channel=Ви повинні приєднатися до міста, щоб використовувати цей канал. integrations.mcmmo.cannot_use_party_channel=Ви повинні приєднатися до групи mcMMO, щоб використовувати цей канал. integrations.fuuid.cannot_use_faction_channel=Ви повинні приєднатися до фракції, щоб використовувати цей канал. integrations.fuuid.cannot_use_alliance_channel=Ви повинні приєднатися до альянсу, щоб використовувати цей канал. integrations.fuuid.cannot_use_truce_channel=Ви повинні мати перемир’я з іншою фракцією, щоб використовувати цей канал. integrations.fuuid.cannot_use_mod_channel=Ви повинні бути модератором або адміністратором фракції, щоб використовувати цей канал. integrations.plotsquared.cannot_use_plot_channel=Ви повинні бути на ділянці, щоб використовувати цей канал. ================================================ FILE: common/src/main/resources/locale/messages-zh_CN.properties ================================================ channel.change=你正在 频道上聊天 command.clearchat.description=清空所有玩家的聊天框. command.continue.argument.message=要发送的消息. command.continue.description=向你上次私信你的人发送消息. command.debug.argument.player=检查玩家的权限组. command.debug.description=显示玩家的权限组. command.help.argument.query=搜索查询. command.help.description=Carbon 命令列表. command.help.misc.arguments=参数 command.help.misc.available_commands=可用命令 command.help.misc.click_for_next_page=下一页 command.help.misc.click_for_previous_page=上一页 command.help.misc.click_to_show_help=点击显示此命令的帮助 command.help.misc.command=命令 command.help.misc.description=描述 command.help.misc.help=帮助 command.help.misc.no_description=无描述 command.help.misc.no_results_for_query=没有结果 command.help.misc.optional=可选 command.help.misc.page_out_of_range=错误\: 页面 不在范围内.必须在范围 [1, ] 内 command.help.misc.showing_results_for_query=显示搜索结果 command.ignore.argument.player=要屏蔽的玩家名字. command.ignore.argument.uuid=要屏蔽的UUID. command.ignore.description=屏蔽指定玩家的消息. command.ignorelist.description=显示你屏蔽的玩家列表. command.ignorelist.none_ignored=你没有屏蔽任何玩家. command.ignorelist.pagination_header=屏蔽列表 command.ignorelist.pagination_element= - ''>''>[取消屏蔽] command.join.description=加入你上次离开的频道. command.leave.description=离开你当前的频道. command.mute.argument.player=要禁言的玩家. command.mute.argument.uuid=要禁言的UUID. command.mute.description=禁言指定玩家, 阻止指定玩家发送消息. command.muteinfo.argument.player=玩家. command.muteinfo.argument.uuid=UUID. command.muteinfo.description=显示玩家是否被禁言. command.nickname.argument.nickname=昵称. command.nickname.argument.player=玩家. command.nickname.description=显示你的昵称. command.nickname.set.description=设置你的昵称. command.nickname.reset.description=删除你的昵称. command.nickname.others.description=显示玩家的昵称. command.nickname.others.set.description=设置玩家的昵称. command.nickname.others.reset.description=重置玩家的昵称. command.reload.description=重载配置. command.reply.argument.message=要回复的消息. command.reply.description=向最近私信你的玩家发送消息. command.togglemsg.description=允许或禁止其他玩家向你发送消息. command.unignore.argument.player=要取消屏蔽的玩家. command.unignore.argument.uuid=要取消屏蔽的UUID. command.unignore.description=取消屏蔽指定玩家的消息. command.unmute.argument.player=要取消禁言的玩家. command.unmute.argument.uuid=要取消禁言的UUID. command.unmute.description=取消禁言指定玩家. command.updateusername.argument.player=要更新的玩家名称. command.updateusername.argument.uuid=要更新的玩家的 UUID. command.updateusername.description=更新玩家的用户名以匹配其正版名称. command.updateusername.fetching=获取用户名中... command.updateusername.notupdated=无法获取用户名. command.updateusername.updated=已更新 的用户名\! command.whisper.argument.message=要发送的私聊消息. command.whisper.argument.player=要发送消息的玩家名称. command.whisper.description=向指定玩家发送私聊消息. command.party.pagination_header=队伍成员\: command.party.pagination_element=''\:''''> - command.party.created=成功创建并加入队伍''''\! command.party.not_in_party=你不在任何队伍中.使用''/party create''创建一个队伍, 或使用''/party accept''接受邀请. command.party.current_party=你在队伍\: command.party.must_leave_current_first=你必须先离开当前的队伍. command.party.name_too_long=队伍名称太长. command.party.received_invite=单击接受''>''>你收到来自队伍''''的邀请, 由发送.点击此消息以接受. command.party.sent_invite=已向 发送队伍邀请. command.party.must_specify_invite=你必须指定邀请的人. command.party.no_pending_invites=你没有任何待处理的组队邀请. command.party.no_invite_from=你没有来自 的待处理邀请. command.party.joined_party=成功加入了队伍 '''' \! command.party.left_party=成功离开了队伍 '''' . command.party.disbanded=成功解散了队伍 '''' . command.party.cannot_disband_multiple_members=无法解散队伍 '''' , 因为你不是最后一个成员. command.party.must_be_in_party=你必须加入一个队伍才能使用该指令.使用 ''/party create'' 创建一个队伍, 或使用 ''/party accept'' 接受邀请. command.party.cannot_invite_self=你不能邀请自己. command.party.description=查看当前队伍的信息. command.party.create.description=创建一个新的队伍. command.party.invite.description=邀请玩家加入你的队伍. command.party.accept.description=接受队伍邀请. command.party.leave.description=离开当前队伍. command.party.already_in_party= 已经在你的队伍中. command.party.disband.description=解散当前队伍. party.player_joined=加入了你的队伍. party.player_left=离开了你的队伍. party.cannot_use_channel=你必须加入一个队伍才能使用该频道. config.reload.failed=配置加载失败 config.reload.success=配置加载成功 error.command.argument_parsing=无效的命令参数\: error.command.command_execution=\n\n 点击复制">''>执行此命令时发生内部错误. error.command.invalid_player=未找到名为 '''' 的玩家 error.command.invalid_sender=无效的命令发送者.你必须是类型为 的用户 error.command.invalid_syntax=无效命令, 正确用法\: / error.command.no_permission=你没有执行此命令的权限. error.command.command_needs_player=需要提供玩家参数才能执行此命令. ignore.already_ignored=你已经在屏蔽 ignore.not_ignored=你没有屏蔽 ignore.exempt=你不能屏蔽 ignore.invalid_target=未找到玩家 ignore.now_ignoring=你现在正在屏蔽 ignore.no_longer_ignoring=你不再屏蔽 mute.alert.players= 已被禁言 mute.alert.target=你已被禁言 mute.cannot_speak=你被禁言时不能发言 mute.exempt=该玩家不受禁言限制 mute.info.muted= 被禁言了 mute.info.not_muted= 没有被禁言 mute.info.self.muted=你被禁言了 mute.info.self.not_muted=你没有被禁言 mute.no_target=没有指定要禁言的玩家. mute.spy.prefix=禁言''>M mute.unmute.alert.players= 已被取消禁言 mute.unmute.alert.target=你已被取消禁言 mute.unmute.no_target=没有指定要解除禁言的玩家. nickname.reset.others=你已重置 的昵称 nickname.reset=你已重置自己的昵称 nickname.set.others=你将 的昵称设置为 nickname.set=你的昵称已设置为 nickname.show.others.unset= 未设置昵称 nickname.show.others= 的昵称是 nickname.show.unset=你未设置昵称 nickname.show=你的昵称是 nickname.error.character_limit=昵称 '''' 超过了字符限制, 必须设置为 ~ 个字符. nickname.error.blacklist=昵称"" 不允许使用, 请选择其他昵称. nickname.error.filter=昵称必须由字母或数字组成\! reply.target.missing=没有回复 reply.target.self=你不能给自己发送私信 whisper.console=[] -> [] whisper.continue.target_missing=无法继续发送私信 whisper.error=发送私信失败 whisper.from= ''>hover\:show_text\:''单击开始回复''[] -> [] whisper.ignored_by_target= 已经屏蔽你的私信 whisper.ignoring_target=你正在屏蔽 whisper.ignoring_all=无法在他们被你屏蔽时发送消息\! whisper.to= ''>发送消息''>[] -> [] whisper.toggled.on=你开启了私信的接收. whisper.toggled.off=你关闭了私信的接收. channel.cooldown=请等待 秒后再聊天\! channel.radius.empty_recipients=你附近没有人, 无法发送消息. channel.joined=你已重新加入该频道 channel.left=你已离开该频道 channel.no_permission=你没有权限进入该频道 channel.already_left=你已经离开了该频道 channel.not_left=你还未离开该频道 channel.not_found=找不到该频道 pagination.page_out_of_range= 页超出范围, 总共只有 页. pagination.click_for_next_page=下一页 pagination.click_for_previous_page=上一页 pagination.footer=/ integrations.towny.cannot_use_alliance_channel=你必须加入一个联盟才能使用该频道. integrations.towny.cannot_use_nation_channel=你必须加入一个国家才能使用该频道. integrations.towny.cannot_use_town_channel=你必须加入一个城镇才能使用该频道. integrations.mcmmo.cannot_use_party_channel=你必须加入一个 mcMMO 队伍才能使用该频道. integrations.fuuid.cannot_use_faction_channel=你必须加入一个派系才能使用该频道. integrations.fuuid.cannot_use_alliance_channel=你必须加入一个联盟才能使用该频道. integrations.fuuid.cannot_use_truce_channel=你必须与另一个派系结成停战协议才能使用该频道. ================================================ FILE: common/src/main/resources/locale/messages-zh_TW.properties ================================================ channel.change=你現在正在屏蔽 command.clearchat.description=清空所有玩家的聊天框. command.continue.argument.message=要發送的消息. command.continue.description=嚮你上次私信你的人發送消息. command.debug.argument.player=檢查玩家的權限組. command.debug.description=顯示玩家的權限組. command.help.argument.query=搜索查詢. command.help.description=Carbon 命令列錶. command.help.misc.arguments=參數 command.help.misc.available_commands=可用命令 command.help.misc.click_for_next_page=下一頁 command.help.misc.click_for_previous_page=上一頁 command.help.misc.click_to_show_help=點選顯示此命令的幫助 command.help.misc.command=命令 command.help.misc.description=描述 command.help.misc.help=幫助 command.help.misc.no_description=無描述 command.help.misc.no_results_for_query=冇有結果 command.help.misc.optional=可選 command.help.misc.page_out_of_range=錯誤\: 頁麵 不在範圍內.必須在範圍 [1, ] 內 command.help.misc.showing_results_for_query=顯示搜索結果\: command.ignore.argument.player=要屏蔽的玩家名字. command.ignore.argument.uuid=要屏蔽的UUID. command.ignore.description=屏蔽指定玩家的消息. command.ignorelist.description=顯示你屏蔽的玩家列錶. command.ignorelist.none_ignored=你冇有屏蔽任何玩家. command.ignorelist.pagination_header=屏蔽列錶 command.ignorelist.pagination_element= - ''>''>[取消屏蔽] command.join.description=加入你上次離開的頻道. command.leave.description=離開你當前的頻道. command.mute.argument.player=要禁言的玩家. command.mute.argument.uuid=要禁言的UUID. command.mute.description=禁言指定玩家, 阻止指定玩家發送消息. command.muteinfo.argument.player=玩家. command.muteinfo.argument.uuid=UUID. command.muteinfo.description=顯示玩家是否被禁言. command.nickname.argument.nickname=昵稱. command.nickname.argument.player=玩家. command.nickname.description=顯示你的昵稱. command.nickname.set.description=設定你的昵稱. command.nickname.reset.description=刪除你的昵稱. command.nickname.others.description=顯示玩家的昵稱. command.nickname.others.set.description=設定玩家的昵稱. command.nickname.others.reset.description=重置玩家的昵稱. command.reload.description=重載配置. command.reply.argument.message=要回複的消息. command.reply.description=嚮最近私信你的玩家發送消息. command.togglemsg.description=允許或禁止其他玩家嚮你發送消息. command.unignore.argument.player=要取消屏蔽的玩家. command.unignore.argument.uuid=要取消屏蔽的UUID. command.unignore.description=取消屏蔽指定玩家的消息. command.unmute.argument.player=要取消禁言的玩家. command.unmute.argument.uuid=要取消禁言的UUID. command.unmute.description=取消禁言指定玩家. command.updateusername.argument.player=要更新的玩家名稱. command.updateusername.argument.uuid=要更新的玩家的 UUID. command.updateusername.description=更新玩家的用戶名以匹配其正版名稱. command.updateusername.fetching=獲取用戶名中... command.updateusername.notupdated=無法獲取用戶名. command.updateusername.updated=已更新 的用戶名\! command.whisper.argument.message=要發送的私聊消息. command.whisper.argument.player=要發送消息的玩家名稱. command.whisper.description=嚮指定玩家發送私聊消息. command.party.pagination_header=隊伍成員\: command.party.pagination_element=''\:''''> - command.party.created=成功創建並加入隊伍''''\! command.party.not_in_party=你不在任何隊伍中.使用''/party create''創建一個隊伍, 或使用''/party accept''接受邀請. command.party.current_party=你在隊伍\: command.party.must_leave_current_first=你必須先離開當前的隊伍. command.party.name_too_long=隊伍名稱太長. command.party.received_invite=單擊接受''>''>你收到來自隊伍''''的邀請, 由發送.點選此消息以接受. command.party.sent_invite=已嚮 發送隊伍邀請. command.party.must_specify_invite=你必須指定邀請的人. command.party.no_pending_invites=你冇有任何待處理的組隊邀請. command.party.no_invite_from=你冇有來自 的待處理邀請. command.party.joined_party=成功加入了隊伍 '''' \! command.party.left_party=成功離開了隊伍 '''' . command.party.disbanded=成功解散了隊伍 '''' . command.party.cannot_disband_multiple_members=無法解散隊伍 '''' , 因為你不是最後一個成員. command.party.must_be_in_party=你必須加入一個隊伍才能使用該指令.使用 ''/party create'' 創建一個隊伍, 或使用 ''/party accept'' 接受邀請. command.party.cannot_invite_self=你不能邀請自己. command.party.description=檢視當前隊伍的信息. command.party.create.description=創建一個新的隊伍. command.party.invite.description=邀請玩家加入你的隊伍. command.party.accept.description=接受隊伍邀請. command.party.leave.description=離開當前隊伍. command.party.already_in_party= 已經在你的隊伍中. command.party.disband.description=解散當前隊伍. party.player_joined=加入了你的隊伍. party.player_left=離開了你的隊伍. party.cannot_use_channel=你必須加入一個隊伍才能使用該頻道. config.reload.failed=配置加載失敗 config.reload.success=配置加載成功 error.command.argument_parsing=無效的命令參數\: error.command.command_execution=\n\n 點選複製">''>執行此命令時發生內部錯誤. error.command.invalid_player=未找到名為 '''' 的玩家 error.command.invalid_sender=無效的命令發送者.你必須是類型為 的用戶 error.command.invalid_syntax=無效命令, 正確用法\: / error.command.no_permission=你冇有執行此命令的權限. error.command.command_needs_player=需要提供玩家參數才能執行此命令. ignore.already_ignored=你已經在屏蔽 ignore.not_ignored=你冇有屏蔽 ignore.exempt=你不能屏蔽 ignore.invalid_target=未找到玩家 ignore.now_ignoring=你現在正在屏蔽 ignore.no_longer_ignoring=你不再屏蔽 mute.alert.players= 已被禁言 mute.alert.target=你已被禁言 mute.cannot_speak=你被禁言時不能發言 mute.exempt=該玩家不受禁言限製 mute.info.muted= 被禁言了 mute.info.not_muted= 冇有被禁言 mute.info.self.muted=你被禁言了 mute.info.self.not_muted=你冇有被禁言 mute.no_target=冇有指定要禁言的玩家. mute.spy.prefix=禁言''>M mute.unmute.alert.players= 已被取消禁言 mute.unmute.alert.target=你已被取消禁言 mute.unmute.no_target=冇有指定要解除禁言的玩家. nickname.reset.others=你已重置 的昵稱 nickname.reset=你已重置自己的昵稱 nickname.set.others=你將 的昵稱設定為 nickname.set=你的昵稱已設定為 nickname.show.others.unset= 未設定昵稱 nickname.show.others= 的昵稱是 nickname.show.unset=你未設定昵稱 nickname.show=你的昵稱是 nickname.error.character_limit=昵稱 '''' 超過了字符限製, 必須設定為 ~ 個字符. nickname.error.blacklist=昵稱"" 不允許使用, 請選擇其他昵稱. reply.target.missing=冇有回複 reply.target.self=你不能給自己發送私信 whisper.console=[] -> [] whisper.continue.target_missing=無法繼續發送私信 whisper.error=發送私信失敗 whisper.from= ''>hover\:show_text\:''單擊開始回複''[] -> [] whisper.ignored_by_target= 已經屏蔽你的私信 whisper.ignoring_target=你正在屏蔽 whisper.ignoring_all=無法在他們被你屏蔽時發送消息\! whisper.to= ''>發送消息''>[] -> [] whisper.toggled.on=你開啓了私信的接收. whisper.toggled.off=你關閉了私信的接收. channel.radius.empty_recipients=你附近冇有人, 無法發送消息. channel.joined=你已重新加入該頻道 channel.left=你已離開該頻道 channel.no_permission=你冇有權限進入該頻道 channel.already_left=你已經離開了該頻道 channel.not_left=你還未離開該頻道 channel.not_found=找不到該頻道 pagination.page_out_of_range= 頁超出範圍, 總共隻有 頁. pagination.click_for_next_page=下一頁 pagination.click_for_previous_page=上一頁 pagination.footer=/ integrations.towny.cannot_use_alliance_channel=你必須加入一個聯盟才能使用該頻道. integrations.towny.cannot_use_nation_channel=你必須加入一個國家才能使用該頻道. integrations.towny.cannot_use_town_channel=你必須加入一個城鎮才能使用該頻道. integrations.mcmmo.cannot_use_party_channel=你必須加入一個 mcMMO 隊伍才能使用該頻道. integrations.fuuid.cannot_use_faction_channel=你必須加入一個派係才能使用該頻道. integrations.fuuid.cannot_use_alliance_channel=你必須加入一個聯盟才能使用該頻道. integrations.fuuid.cannot_use_truce_channel=你必須與另一個派係結成停戰協議才能使用該頻道. ================================================ FILE: common/src/main/resources/queries/clear-ignores.sql ================================================ DELETE FROM carbon_ignores WHERE (id = :id); ================================================ FILE: common/src/main/resources/queries/clear-leftchannels.sql ================================================ DELETE FROM carbon_leftchannels WHERE (id = :id); ================================================ FILE: common/src/main/resources/queries/clear-party-members.sql ================================================ DELETE FROM carbon_party_members WHERE (partyid = :partyid); ================================================ FILE: common/src/main/resources/queries/drop-party-member.sql ================================================ DELETE FROM carbon_party_members WHERE (playerid = :playerid); ================================================ FILE: common/src/main/resources/queries/drop-party.sql ================================================ DELETE FROM carbon_parties WHERE (partyid = :partyid); ================================================ FILE: common/src/main/resources/queries/insert-party-member.sql ================================================ INSERT{!PSQL: IGNORE} INTO carbon_party_members (partyid, playerid) VALUES(:partyid, :playerid){PSQL: ON CONFLICT DO NOTHING}; ================================================ FILE: common/src/main/resources/queries/insert-party.sql ================================================ INSERT INTO carbon_parties( partyid, name ) VALUES ( :partyid, :name ); ================================================ FILE: common/src/main/resources/queries/insert-player.sql ================================================ INSERT{!PSQL: IGNORE} INTO carbon_users( id, muted, muteexpiration, deafened, selectedchannel, displayname, lastwhispertarget, whisperreplytarget, spying, ignoringdms, party, applycustomfilters ) VALUES ( :id, :muted, :muteexpiration, :deafened, :selectedchannel, :displayname, :lastwhispertarget, :whisperreplytarget, :spying, :ignoringdms, :party, :applycustomfilters ){PSQL: ON CONFLICT DO NOTHING}; ================================================ FILE: common/src/main/resources/queries/migrations/h2/V1__create_tables.sql ================================================ CREATE TABLE carbon_users ( `id` UUID NOT NULL PRIMARY KEY, `muted` BOOLEAN, `deafened` BOOLEAN, `selectedchannel` VARCHAR(256), `displayname` VARCHAR(1024), `lastwhispertarget` UUID, `whisperreplytarget` UUID, `spying` BOOLEAN, `ignoringdms` BOOLEAN ); CREATE TABLE carbon_ignores ( `id` UUID NOT NULL, `ignoredplayer` UUID NOT NULL, PRIMARY KEY (id, ignoredplayer) ); CREATE TABLE carbon_leftchannels ( `id` UUID NOT NULL, `channel` VARCHAR(256) NOT NULL, PRIMARY KEY (id, channel) ); ================================================ FILE: common/src/main/resources/queries/migrations/h2/V2__increase_nickname_size.sql ================================================ ALTER TABLE carbon_users MODIFY displayname VARCHAR(8192); ================================================ FILE: common/src/main/resources/queries/migrations/h2/V3__parties.sql ================================================ 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; ================================================ FILE: common/src/main/resources/queries/migrations/h2/V4__filters.sql ================================================ ALTER TABLE carbon_users ADD COLUMN applycustomfilters BOOLEAN; ================================================ FILE: common/src/main/resources/queries/migrations/h2/V5__tempmute.sql ================================================ ALTER TABLE carbon_users ADD COLUMN muteexpiration BOOLEAN AFTER muted; ================================================ FILE: common/src/main/resources/queries/migrations/h2/V6__tempmute.sql ================================================ ALTER TABLE carbon_users ALTER COLUMN muteexpiration BIGINT; ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V10__tempmute.sql ================================================ ALTER TABLE carbon_users MODIFY COLUMN muteexpiration BIGINT; ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V1__create_tables.sql ================================================ CREATE TABLE carbon_users ( `id` BINARY(16) NOT NULL PRIMARY KEY, `muted` BOOLEAN, `deafened` BOOLEAN, `selectedchannel` VARCHAR(256), `username` VARCHAR(20), `displayname` VARCHAR(1024), `lastwhispertarget` BINARY(16), `whisperreplytarget` BINARY(16), `spying` BOOLEAN ); CREATE TABLE carbon_ignores ( `id` BINARY(16) NOT NULL, `ignoredplayer` BINARY(16) NOT NULL, PRIMARY KEY (id, ignoredplayer) ); ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V2__create_tables.sql ================================================ CREATE TABLE carbon_leftchannels ( `id` BINARY(16) NOT NULL, `channel` BINARY(16) NOT NULL, PRIMARY KEY (id, channel) ); ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V3__fix_leftchannels.sql ================================================ DROP TABLE carbon_leftchannels; CREATE TABLE carbon_leftchannels ( `id` BINARY(16) NOT NULL, `channel` VARCHAR(256) NOT NULL, PRIMARY KEY (id, channel) ); ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V4__drop_usernames.sql ================================================ ALTER TABLE carbon_users DROP COLUMN username; ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V5__add_dmtoggle.sql ================================================ ALTER TABLE carbon_users ADD COLUMN ignoringdms BOOLEAN; ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V6__increase_nickname_size.sql ================================================ ALTER TABLE carbon_users MODIFY displayname VARCHAR(8192); ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V7__parties.sql ================================================ 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); ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V8__filters.sql ================================================ ALTER TABLE carbon_users ADD COLUMN applycustomfilters BOOLEAN; ================================================ FILE: common/src/main/resources/queries/migrations/mysql/V9__tempmute.sql ================================================ ALTER TABLE carbon_users ADD COLUMN muteexpiration BOOLEAN AFTER muted; ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V10__tempmute.sql ================================================ ALTER TABLE carbon_users ALTER COLUMN muteexpiration TYPE BIGINT USING (CASE WHEN muteexpiration THEN 1 ELSE 0 END); ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V1__create_tables.sql ================================================ CREATE TABLE carbon_users ( id UUID NOT NULL PRIMARY KEY, muted BOOLEAN, deafened BOOLEAN, selectedchannel VARCHAR(256), username VARCHAR(20), displayname VARCHAR(1024), lastwhispertarget UUID, whisperreplytarget UUID, spying BOOLEAN ); CREATE TABLE carbon_ignores ( id UUID NOT NULL, ignoredplayer UUID NOT NULL, PRIMARY KEY (id, ignoredplayer) ); ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V2__create_tables.sql ================================================ CREATE TABLE carbon_leftchannels ( id UUID NOT NULL, channel VARCHAR(100) NOT NULL, PRIMARY KEY (id, channel) ); ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V3__fix_leftchannels.sql ================================================ ALTER TABLE carbon_leftchannels ALTER COLUMN channel TYPE VARCHAR(256), ALTER COLUMN channel SET NOT NULL; ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V4__drop_usernames.sql ================================================ ALTER TABLE carbon_users DROP COLUMN username; ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V5__add_dmtoggle.sql ================================================ ALTER TABLE carbon_users ADD COLUMN ignoringdms BOOLEAN; ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V6__increase_nickname_size.sql ================================================ ALTER TABLE carbon_users ALTER COLUMN displayname TYPE VARCHAR(8192); ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V7__parties.sql ================================================ 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; ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V8__filters.sql ================================================ ALTER TABLE carbon_users ADD COLUMN applycustomfilters BOOLEAN; ================================================ FILE: common/src/main/resources/queries/migrations/postgresql/V9__tempmute.sql ================================================ ALTER TABLE carbon_users ADD COLUMN muteexpiration BOOLEAN; ================================================ FILE: common/src/main/resources/queries/save-ignores.sql ================================================ INSERT{!PSQL: IGNORE} INTO carbon_ignores (id, ignoredplayer) VALUES(:id, :ignoredplayer){PSQL: ON CONFLICT DO NOTHING}; ================================================ FILE: common/src/main/resources/queries/save-leftchannels.sql ================================================ INSERT{!PSQL: IGNORE} INTO carbon_leftchannels (id, channel) VALUES(:id, :channel){PSQL: ON CONFLICT DO NOTHING}; ================================================ FILE: common/src/main/resources/queries/select-ignores.sql ================================================ SELECT ignoredplayer FROM carbon_ignores WHERE (id = :id); ================================================ FILE: common/src/main/resources/queries/select-leftchannels.sql ================================================ SELECT channel FROM carbon_leftchannels WHERE (id = :id); ================================================ FILE: common/src/main/resources/queries/select-party-members.sql ================================================ SELECT playerid FROM carbon_party_members WHERE (partyid = :partyid); ================================================ FILE: common/src/main/resources/queries/select-party.sql ================================================ SELECT partyid, name FROM carbon_parties WHERE (partyid = :partyid); ================================================ FILE: common/src/main/resources/queries/select-player.sql ================================================ SELECT id, muted, muteexpiration, deafened, selectedchannel, displayname, lastwhispertarget, whisperreplytarget, spying, ignoringdms, party, applycustomfilters FROM carbon_users WHERE (id = :id); ================================================ FILE: common/src/main/resources/queries/update-player.sql ================================================ UPDATE carbon_users SET muted = :muted, muteexpiration = :muteexpiration, deafened = :deafened, selectedchannel = :selectedchannel, displayname = :displayname, lastwhispertarget = :lastwhispertarget, whisperreplytarget = :whisperreplytarget, spying = :spying, ignoringdms = :ignoringdms, party = :party, applycustomfilters = :applycustomfilters WHERE (id = :id); ================================================ FILE: crowdin.yml ================================================ files: - source: common/src/main/resources/locale/messages-en_US.properties translation: /common/src/main/resources/locale/messages-%locale_with_underscore%.properties bundles: - 1 ================================================ FILE: fabric/build.gradle.kts ================================================ import xyz.jpenilla.resourcefactory.fabric.Environment import java.util.function.Predicate import kotlin.io.path.invariantSeparatorsPathString plugins { id("carbon.shadow-platform") id("quiet-fabric-loom") alias(libs.plugins.resource.factory.fabric.convention) } val shade: Configuration by configurations.creating configurations.implementation { extendsFrom(shade) } dependencies { minecraft(libs.fabricMinecraft) mappings(loom.officialMojangMappings()) modImplementation(libs.fabricLoader) modImplementation(libs.fabricApi) modRuntimeOnly(libs.fabricApiDeprecated) // LuckPerms needs to work at dev time shade(projects.carbonchatCommon) { exclude("net.kyori", "adventure-api") exclude("net.kyori", "adventure-text-serializer-gson") exclude("net.kyori", "adventure-text-serializer-plain") exclude("org.incendo", "cloud-core") exclude("org.incendo", "cloud-services") exclude("org.incendo", "cloud-brigadier") exclude("org.incendo", "cloud-minecraft-signed-arguments") exclude("io.leangen.geantyref") } modImplementation(libs.cloudFabric) { exclude("net.fabricmc.fabric-api") } include(libs.cloudFabric) implementation(libs.cloudSigned) include(libs.cloudSigned) modImplementation(libs.fabricPermissionsApi) include(libs.fabricPermissionsApi) modImplementation(libs.adventurePlatformFabric) modImplementation(libs.miniplaceholders) runtimeDownload(libs.mysql) include(libs.jarRelocator) runtimeOnly(libs.jarRelocator) { isTransitive = false } runtimeDownload(libs.checkerQual) } fabricModJson { id = rootProject.name.lowercase() name = rootProject.name version = project.version.toString() description = project.description author("Draycia") author("jmp") contact { homepage = GITHUB_REPO_URL sources = GITHUB_REPO_URL issues = "$GITHUB_REPO_URL/issues" } license("GPLv3") environment = Environment.ANY mainEntrypoint("net.draycia.carbon.fabric.CarbonFabricBootstrap") mixin("carbonchat.mixins.json") depends("fabricloader", ">=" + libs.versions.fabricLoader.get()) depends("fabric-api", "*") depends("cloud", "*") depends("adventure-platform-fabric", "*") depends("minecraft", ">=${libs.versions.minecraft.get()}") depends("luckperms", ">=5.0.0") suggests("miniplaceholders", "*") } carbonPlatform { productionJar = tasks.remapJar.flatMap { it.archiveFile } } tasks { shadowJar { configurations = listOf(shade) relocateDependency("org.incendo.cloud.minecraft.extras") standardRuntimeRelocations() relocateGuice() relocateDependency("org.checkerframework") } writeDependencies { standardRuntimeRelocations() relocateGuice() relocateDependency("org.checkerframework") } runServer { dependsOn(shadowJar) classpathFilter = Predicate { val s = it.toPath().toAbsolutePath().invariantSeparatorsPathString !s.contains("build/libs") && !s.contains("build/classes") && !s.contains("build/resources") } doFirst { val jar = shadowJar.get().archiveFile.get().asFile val mods = file("run/mods") mods.mkdirs() jar.copyTo(mods.resolve("carbonchat-dev.jar"), overwrite = true) } } } publishMods.modrinth { minecraftVersions.set(listOf(libs.versions.minecraft.get())) modLoaders.addAll("fabric") requires("fabric-api") requires("adventure-platform-mod") } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/CarbonChatFabric.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provider; import com.google.inject.Singleton; import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.common.CarbonChatInternal; import net.draycia.carbon.common.PeriodicTasks; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.command.ExecutionCoordinatorHolder; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.users.PlatformUserManager; import net.draycia.carbon.common.users.ProfileCache; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.fabric.listeners.FabricChatHandler; import net.draycia.carbon.fabric.listeners.FabricJoinQuitListener; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) @Singleton public final class CarbonChatFabric extends CarbonChatInternal { @Inject private CarbonChatFabric( final Injector injector, final Logger logger, final @PeriodicTasks ScheduledExecutorService periodicTasks, final ProfileCache profileCache, final ProfileResolver profileResolver, final CarbonMessages carbonMessages, final PlatformUserManager userManager, final ExecutionCoordinatorHolder commandExecutor, final CarbonServer carbonServer, final CarbonEventHandler eventHandler, final CarbonChannelRegistry channelRegistry, final Provider messagingManagerProvider, @SuppressWarnings("unused") // Make sure it initializes now final MinecraftServerHolder minecraftServerHolder ) { super( injector, logger, periodicTasks, profileCache, profileResolver, userManager, commandExecutor, carbonServer, carbonMessages, eventHandler, channelRegistry, messagingManagerProvider ); } public void onInitialize() { this.init(); // Platform Listeners this.registerChatListener(); this.registerServerLifecycleListeners(); this.registerPlayerStatusListeners(); this.loadAddonEntrypoints(); } @SuppressWarnings({"unchecked", "rawtypes"}) private void loadAddonEntrypoints() { final List> containers = FabricLoader.getInstance().getEntrypointContainers("carbonchat", Consumer.class); for (final EntrypointContainer container : containers) { try { final Consumer entrypoint = container.getEntrypoint(); entrypoint.accept(this); } catch (final Throwable t) { this.logger().error("Failed to invoke 'carbonchat' entrypoint for addon mod '{}'", container.getProvider().getMetadata().getId(), t); } } } private void registerChatListener() { ServerMessageEvents.ALLOW_CHAT_MESSAGE.register(this.injector().getInstance(FabricChatHandler.class)); } private void registerServerLifecycleListeners() { ServerLifecycleEvents.SERVER_STARTED.register(server -> this.checkVersion()); ServerLifecycleEvents.SERVER_STOPPED.register(server -> this.shutdown()); } private void registerPlayerStatusListeners() { final FabricJoinQuitListener listener = this.injector().getInstance(FabricJoinQuitListener.class); ServerPlayConnectionEvents.DISCONNECT.register(listener); ServerPlayConnectionEvents.JOIN.register(listener); } public boolean luckPermsLoaded() { return FabricLoader.getInstance().isModLoaded("luckperms"); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/CarbonChatFabricModule.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric; import com.google.inject.Provider; import com.google.inject.Provides; import com.google.inject.Singleton; import com.mojang.brigadier.tree.CommandNode; import java.nio.file.Path; import java.util.Arrays; import java.util.Iterator; import java.util.Map; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.common.CarbonCommonModule; import net.draycia.carbon.common.CarbonPlatformModule; import net.draycia.carbon.common.DataDirectory; import net.draycia.carbon.common.PlatformScheduler; import net.draycia.carbon.common.RawChat; import net.draycia.carbon.common.command.CommandSettings; import net.draycia.carbon.common.command.Commander; import net.draycia.carbon.common.command.ExecutionCoordinatorHolder; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.messages.CarbonMessageRenderer; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.users.PlatformUserManager; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.common.util.CloudUtils; import net.draycia.carbon.fabric.command.FabricCommander; import net.draycia.carbon.fabric.command.FabricPlayerCommander; import net.draycia.carbon.fabric.listeners.FabricChatHandler; import net.draycia.carbon.fabric.users.CarbonPlayerFabric; import net.draycia.carbon.fabric.users.FabricProfileResolver; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; import net.kyori.adventure.key.Key; import net.minecraft.commands.CommandSourceStack; import net.minecraft.server.level.ServerPlayer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.SenderMapper; import org.incendo.cloud.fabric.FabricServerCommandManager; @DefaultQualifier(NonNull.class) public final class CarbonChatFabricModule extends CarbonPlatformModule { private final Logger logger; private final ModContainer modContainer; CarbonChatFabricModule() { final ModContainer modContainer = FabricLoader.getInstance().getModContainer("carbonchat") .orElseThrow(() -> new IllegalStateException("Could not find ModContainer for carbonchat.")); this.modContainer = modContainer; this.logger = LogManager.getLogger(modContainer.getMetadata().getName()); } @Provides @Singleton public CommandManager commandManager( final ExecutionCoordinatorHolder executionCoordinatorHolder, final Provider carbonChat, final CarbonMessages carbonMessages ) { // Remove existing commands matching our commands or aliases CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { final Map settings = carbonChat.get().injector().getInstance(ConfigManager.class).loadCommandSettings(); final Iterator> it = dispatcher.getRoot().getChildren().iterator(); while (it.hasNext()) { final CommandNode next = it.next(); final String name = next.getName(); if (settings.values().stream().anyMatch(s -> s.name().equals(name) || Arrays.asList(s.aliases()).contains(name))) { it.remove(); } } }); final FabricServerCommandManager commandManager = new FabricServerCommandManager<>( executionCoordinatorHolder.executionCoordinator(), SenderMapper.create( commandSourceStack -> { if (commandSourceStack.getEntity() instanceof ServerPlayer) { return new FabricPlayerCommander(carbonChat.get(), commandSourceStack); } return FabricCommander.from(commandSourceStack); }, commander -> ((FabricCommander) commander).commandSourceStack() ) ); CloudUtils.decorateCommandManager(commandManager, carbonMessages, this.logger); return commandManager; } @Override protected void configurePlatform() { this.install(new CarbonCommonModule()); this.bind(ModContainer.class).toInstance(this.modContainer); this.bind(CarbonChat.class).to(CarbonChatFabric.class); this.bind(Logger.class).toInstance(this.logger); this.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(FabricLoader.getInstance().getConfigDir().resolve(this.modContainer.getMetadata().getId())); this.bind(CarbonServer.class).to(CarbonServerFabric.class); this.bind(ProfileResolver.class).to(FabricProfileResolver.class); this.bind(PlatformScheduler.class).to(FabricScheduler.class); this.install(PlatformUserManager.PlayerFactory.moduleFor(CarbonPlayerFabric.class)); this.bind(CarbonMessageRenderer.class).to(FabricMessageRenderer.class); this.bind(Key.class).annotatedWith(RawChat.class).toProvider(() -> FabricChatHandler.CHAT_TYPE_KEY); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/CarbonFabricBootstrap.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric; import com.google.inject.Guice; import net.draycia.carbon.api.CarbonChatProvider; import net.draycia.carbon.common.util.CarbonDependencies; import net.fabricmc.api.ModInitializer; import net.fabricmc.loader.api.FabricLoader; import xyz.jpenilla.gremlin.runtime.platformsupport.FabricClasspathAppender; public class CarbonFabricBootstrap implements ModInitializer { @Override public void onInitialize() { new FabricClasspathAppender().append( CarbonDependencies.resolve( FabricLoader.getInstance().getConfigDir().resolve("carbonchat").resolve("libraries") ) ); final CarbonChatFabric carbonChat = Guice.createInjector(new CarbonChatFabricModule()) .getInstance(CarbonChatFabric.class); CarbonChatProvider.register(carbonChat); carbonChat.onInitialize(); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/CarbonServerFabric.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.List; import java.util.Objects; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.jetbrains.annotations.NotNull; @Singleton @DefaultQualifier(NonNull.class) public final class CarbonServerFabric implements CarbonServer, ForwardingAudience.Single { private final MinecraftServerHolder serverHolder; private final UserManager userManager; @Inject private CarbonServerFabric(final MinecraftServerHolder serverHolder, final UserManager userManager) { this.serverHolder = serverHolder; this.userManager = userManager; } @Override public @NotNull Audience audience() { return MinecraftServerAudiences.of(this.serverHolder.requireServer()).all(); } @Override public Audience console() { return new ConsoleCarbonPlayer(this.serverHolder.requireServer().createCommandSourceStack()); } @Override public List players() { return this.serverHolder.requireServer().getPlayerList().getPlayers().stream() .map(serverPlayer -> this.userManager.user(serverPlayer.getUUID()).getNow(null)) .filter(Objects::nonNull) .toList(); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/FabricMessageRenderer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric; import com.google.inject.Inject; import com.google.inject.Singleton; import io.github.miniplaceholders.api.MiniPlaceholders; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil; import net.draycia.carbon.common.messages.CarbonMessageRenderer; import net.draycia.carbon.common.messages.RenderForTagResolver; import net.draycia.carbon.common.messages.SourcedAudience; 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.resolver.TagResolver; 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 class FabricMessageRenderer extends CarbonMessageRenderer { private final ConfigManager configManager; @Inject public FabricMessageRenderer(final ConfigManager configManager, final RenderForTagResolver.Factory renderForTagResolver) { super(renderForTagResolver); this.configManager = configManager; } @Override public Component render( final Audience receiver, final String intermediateMessage, final TagResolver.Builder tagResolver ) { final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage); final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig = MiniPlaceholdersUtil.miniPlaceholdersLoaded() ? this.configManager.primaryConfig().integrations().config(MiniPlaceholdersIntegration.configMeta()) : null; if (miniplaceholdersConfig != null) { tagResolver.resolver(MiniPlaceholders.globalPlaceholders()); if (receiver instanceof SourcedAudience) { tagResolver.resolver(MiniPlaceholders.audiencePlaceholders()); if (miniplaceholdersConfig.relationalPlaceholders) { tagResolver.resolver(MiniPlaceholders.relationalPlaceholders()); } } } final Audience parseAudience = receiver instanceof SourcedAudience sourced ? MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.recipient(), sourced.sender()) : receiver; return MiniMessage.miniMessage().deserialize(placeholderResolvedMessage, parseAudience, tagResolver.build()); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/FabricScheduler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric; import com.google.inject.Inject; import com.google.inject.Singleton; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.PlatformScheduler; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) @Singleton public final class FabricScheduler implements PlatformScheduler { private final MinecraftServerHolder serverHolder; @Inject private FabricScheduler(final MinecraftServerHolder serverHolder) { this.serverHolder = serverHolder; } @Override public void scheduleForPlayer(final CarbonPlayer carbonPlayer, final Runnable runnable) { this.serverHolder.requireServer().execute(runnable); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/MinecraftServerHolder.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.Objects; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.minecraft.server.MinecraftServer; 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 MinecraftServerHolder { private volatile @Nullable MinecraftServer server; @Inject private MinecraftServerHolder() { ServerLifecycleEvents.SERVER_STARTING.register(server -> this.server = server); ServerLifecycleEvents.SERVER_STOPPED.register(server -> this.server = null); } public MinecraftServer requireServer() { return Objects.requireNonNull(this.server, "server requested when not active"); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/command/FabricCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric.command; import me.lucko.fabric.api.permissions.v0.Permissions; import net.draycia.carbon.common.command.Commander; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.minecraft.commands.CommandSourceStack; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public interface FabricCommander extends Commander, ForwardingAudience.Single { static FabricCommander from(final CommandSourceStack commandSourceStack) { return new FabricCommanderImpl(commandSourceStack); } CommandSourceStack commandSourceStack(); @Override default Audience audience() { return this.commandSourceStack(); } record FabricCommanderImpl(CommandSourceStack commandSourceStack) implements FabricCommander { @Override public boolean hasPermission(final String permission) { return Permissions.check(this.commandSourceStack, permission, this.commandSourceStack.getServer().operatorUserPermissions().level()); } } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/command/FabricPlayerCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric.command; import com.mojang.brigadier.exceptions.CommandSyntaxException; import me.lucko.fabric.api.permissions.v0.Permissions; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.command.PlayerCommander; import net.minecraft.commands.CommandSourceStack; import net.minecraft.server.level.ServerPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import static java.util.Objects.requireNonNull; @DefaultQualifier(NonNull.class) public record FabricPlayerCommander( CarbonChat carbon, CommandSourceStack commandSourceStack ) implements PlayerCommander, FabricCommander { public ServerPlayer player() { try { return this.commandSourceStack.getPlayerOrException(); } catch (final CommandSyntaxException e) { throw new IllegalStateException("FabricPlayerCommander was created for non-player CommandSourceStack!", e); } } @Override public CarbonPlayer carbonPlayer() { return requireNonNull( this.carbon.userManager().user(this.player().getUUID()).join(), "No CarbonPlayer for logged in Player!" ); } @Override public boolean hasPermission(final String permission) { return Permissions.check(this.commandSourceStack, permission, this.commandSourceStack.getServer().operatorUserPermissions().level()); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/listeners/FabricChatHandler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric.listeners; import com.google.inject.Inject; import java.util.Objects; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.event.events.CarbonChatEventImpl; import net.draycia.carbon.common.event.events.CarbonEarlyChatEvent; import net.draycia.carbon.common.listeners.ChatListenerInternal; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.fabric.CarbonChatFabric; import net.draycia.carbon.fabric.users.CarbonPlayerFabric; import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; import net.kyori.adventure.chat.SignedMessage; import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences; import net.kyori.adventure.text.Component; import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.ChatType; import net.minecraft.network.chat.FilterMask; import net.minecraft.network.chat.OutgoingChatMessage; import net.minecraft.network.chat.PlayerChatMessage; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerPlayer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.jetbrains.annotations.Nullable; public class FabricChatHandler extends ChatListenerInternal implements ServerMessageEvents.AllowChatMessage { public static final Identifier CHAT_TYPE_KEY = Identifier.fromNamespaceAndPath("carbonchat", "chat"); private final CarbonChatFabric carbonChat; private @MonotonicNonNull ResourceKey chatTypeResourceKey; @Inject public FabricChatHandler( final ConfigManager configManager, final CarbonChatFabric carbonChat, final CarbonMessages carbonMessages ) { super(carbonChat.eventHandler(), carbonMessages, configManager); this.carbonChat = carbonChat; } @Override public boolean allowChatMessage(final PlayerChatMessage chatMessage, final ServerPlayer serverPlayer, final ChatType.Bound bound) { if (serverPlayer == null) { return false; } final @Nullable CarbonPlayer sender = this.carbonChat.userManager().user(serverPlayer.getUUID()).join(); final MinecraftServerAudiences audiences = MinecraftServerAudiences.of(serverPlayer.level().getServer()); final SignedMessage signedMessage = audiences.asAdventure(chatMessage); final Component originalMessage = Objects.requireNonNullElse(signedMessage.unsignedContent(), Component.text(signedMessage.message())); final @Nullable CarbonEarlyChatEvent earlyChatEvent = this.prepareAndEmitPreChatEvent(sender, originalMessage); if (earlyChatEvent == null || earlyChatEvent.cancelled()) { return false; } final @Nullable Component message = this.parseTags(sender, earlyChatEvent.message()); final @Nullable CarbonChatEventImpl chatEvent = this.prepareAndEmitChatEvent(sender, message, audiences.asAdventure(chatMessage)); if (chatEvent == null || chatEvent.cancelled()) { return false; } for (final var recipient : chatEvent.recipients()) { final Component finishedMessage = chatEvent.renderFor(recipient); final net.minecraft.network.chat.Component nativeMessage = audiences.nonWrappingSerializer().serialize(finishedMessage); final PlayerChatMessage customChatMessage = new PlayerChatMessage(chatMessage.link(), chatMessage.signature(), chatMessage.signedBody(), nativeMessage, FilterMask.FULLY_FILTERED); final RegistryAccess registryAccess = serverPlayer.level().registryAccess(); if (this.chatTypeResourceKey == null) { this.chatTypeResourceKey = registryAccess.lookupOrThrow(Registries.CHAT_TYPE) .get(CHAT_TYPE_KEY) .flatMap(Holder.Reference::unwrapKey) .orElseThrow(); } final ChatType.Bound customBound = ChatType.bind(this.chatTypeResourceKey, registryAccess, nativeMessage); if (recipient instanceof CommandSourceStack recipientSource) { recipientSource.sendChatMessage(new OutgoingChatMessage.Player(customChatMessage), false, customBound); } else if (recipient instanceof CarbonPlayerFabric carbonPlayerFabric) { carbonPlayerFabric.player().ifPresent(fabricPlayer -> { fabricPlayer.sendChatMessage(new OutgoingChatMessage.Player(customChatMessage), false, customBound); }); } } return false; } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/listeners/FabricJoinQuitListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric.listeners; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.List; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.messaging.packets.PacketFactory; import net.draycia.carbon.common.users.ProfileCache; import net.draycia.carbon.common.users.UserManagerInternal; import net.fabricmc.fabric.api.networking.v1.PacketSender; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.minecraft.network.protocol.game.ClientboundCustomChatCompletionsPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerGamePacketListenerImpl; 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; import static net.draycia.carbon.common.users.PlayerUtils.saveExceptionHandler; @DefaultQualifier(NonNull.class) public class FabricJoinQuitListener implements ServerPlayConnectionEvents.Join, ServerPlayConnectionEvents.Disconnect { private final ProfileCache profileCache; private final Logger logger; private final ConfigManager configManager; private final UserManagerInternal userManager; private final Provider messaging; private final PacketFactory packetFactory; @Inject public FabricJoinQuitListener( final Logger logger, final ConfigManager configManager, final ProfileCache profileCache, final UserManagerInternal userManager, final Provider messaging, final PacketFactory packetFactory ) { this.logger = logger; this.configManager = configManager; this.profileCache = profileCache; this.userManager = userManager; this.messaging = messaging; this.packetFactory = packetFactory; } @Override public void onPlayReady(final ServerGamePacketListenerImpl handler, final PacketSender sender, final MinecraftServer server) { this.profileCache.cache(handler.getPlayer().getUUID(), handler.getPlayer().getGameProfile().name()); this.messaging.get().queuePacket(() -> this.packetFactory.addLocalPlayerPacket(handler.getPlayer().getUUID(), handler.getPlayer().getGameProfile().name())); final @Nullable List suggestions = this.configManager.primaryConfig().customChatSuggestions(); if (suggestions == null || suggestions.isEmpty()) { return; } sender.sendPacket(new ClientboundCustomChatCompletionsPacket(ClientboundCustomChatCompletionsPacket.Action.SET, suggestions)); } @Override public void onPlayDisconnect(final ServerGamePacketListenerImpl handler, final MinecraftServer server) { this.userManager.loggedOut(handler.getPlayer().getGameProfile().id()) .exceptionally(saveExceptionHandler(this.logger, handler.getPlayer().getGameProfile().name(), handler.getPlayer().getGameProfile().id())); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/users/CarbonPlayerFabric.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric.users; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import java.util.List; import java.util.Locale; import java.util.Optional; import me.lucko.fabric.api.permissions.v0.Permissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.InventorySlot; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.users.WrappedCarbonPlayer; import net.draycia.carbon.common.util.EmptyAudienceWithPointers; import net.draycia.carbon.fabric.CarbonChatFabric; import net.draycia.carbon.fabric.MinecraftServerHolder; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.item.ItemStack; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class CarbonPlayerFabric extends WrappedCarbonPlayer implements ForwardingAudience.Single { private final MinecraftServerHolder serverHolder; private final Provider carbonChatFabric; @AssistedInject public CarbonPlayerFabric(final @Assisted CarbonPlayerCommon carbonPlayerCommon, final MinecraftServerHolder serverHolder, final Provider carbonChatFabric) { super(carbonPlayerCommon); this.serverHolder = serverHolder; this.carbonChatFabric = carbonChatFabric; } @Override public @NonNull Audience audience() { return this.player().map(p -> (Audience) p).orElseGet(() -> EmptyAudienceWithPointers.forCarbonPlayer(this)); } public Optional player() { return Optional.ofNullable( this.serverHolder.requireServer().getPlayerList() .getPlayer(this.carbonPlayerCommon.uuid()) ); } @Override public boolean vanished() { return false; } @Override public @Nullable Locale locale() { return this.player() .flatMap(player -> player.get(Identity.LOCALE)) .orElseGet(Locale::getDefault); } @Override public boolean online() { return this.player().isPresent(); } @Override public double distanceSquaredFrom(final CarbonPlayer other) { if (this.player().isEmpty()) { return -1; } final @Nullable ServerPlayer player = this.player().orElse(null); final @Nullable ServerPlayer otherPlayer = this.serverHolder.requireServer() .getPlayerList().getPlayer(other.uuid()); if (player == null || otherPlayer == null) { return -1; } final double deltaX = player.position().x() - otherPlayer.position().x(); final double deltaY = player.position().y() - otherPlayer.position().y(); final double deltaZ = player.position().z() - otherPlayer.position().z(); return (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ); } @Override public boolean sameWorldAs(final CarbonPlayer other) { if (this.player().isEmpty()) { return false; } final Optional player = this.player(); final @Nullable ServerPlayer otherPlayer = this.serverHolder.requireServer() .getPlayerList().getPlayer(other.uuid()); if (player.isEmpty() || otherPlayer == null) { return false; } return player.get().level().equals(otherPlayer.level()); } @Override public @Nullable Component createItemHoverComponent(final InventorySlot slot) { final Optional playerOptional = this.player(); if (playerOptional.isEmpty()) { return null; } final ServerPlayer player = playerOptional.get(); final EquipmentSlot equipmentSlot; if (slot.equals(InventorySlot.MAIN_HAND)) { equipmentSlot = EquipmentSlot.MAINHAND; } else if (slot.equals(InventorySlot.OFF_HAND)) { equipmentSlot = EquipmentSlot.OFFHAND; } else if (slot.equals(InventorySlot.HELMET)) { equipmentSlot = EquipmentSlot.HEAD; } else if (slot.equals(InventorySlot.CHEST)) { equipmentSlot = EquipmentSlot.CHEST; } else if (slot.equals(InventorySlot.LEGS)) { equipmentSlot = EquipmentSlot.LEGS; } else if (slot.equals(InventorySlot.BOOTS)) { equipmentSlot = EquipmentSlot.FEET; } else { return null; } final @Nullable ItemStack item = player.getItemBySlot(equipmentSlot); if (item == null || item.isEmpty()) { return null; } final int amount = Math.min(item.getCount(), 99); final Component quantity = amount <= 1 ? Component.empty() : Component.text(" x" + amount); final Component interim = MinecraftServerAudiences.of(player.level().getServer()).asAdventure(item.getDisplayName()); return Component.empty().append( Component.text("["), interim, quantity, Component.text("]") ) .hoverEvent(item) .colorIfAbsent(TextColor.color(item.getRarity().color().getColor())); } @Override public boolean hasPermission(final String permission) { return this.player() .map(player -> Permissions.check(player, permission, player.level().getServer().operatorUserPermissions().level())) .orElse(false); } @Override public String primaryGroup() { if (!this.carbonChatFabric.get().luckPermsLoaded()) { return "default"; } return super.primaryGroup(); } @Override public List groups() { if (!this.carbonChatFabric.get().luckPermsLoaded()) { return List.of("default"); } return super.groups(); } @Override protected Optional platformDisplayName() { return this.player().flatMap(p -> p.get(Identity.DISPLAY_NAME)); } } ================================================ FILE: fabric/src/main/java/net/draycia/carbon/fabric/users/FabricProfileResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.fabric.users; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.UUID; import java.util.concurrent.CompletableFuture; import net.draycia.carbon.common.users.MojangProfileResolver; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.fabric.MinecraftServerHolder; import net.minecraft.server.level.ServerPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @Singleton @DefaultQualifier(NonNull.class) public final class FabricProfileResolver implements ProfileResolver { private final MinecraftServerHolder serverHolder; private final ProfileResolver mojang; @Inject private FabricProfileResolver(final MinecraftServerHolder serverHolder, final MojangProfileResolver mojang) { this.serverHolder = serverHolder; this.mojang = mojang; } @Override public CompletableFuture<@Nullable UUID> resolveUUID(final String username, final boolean cacheOnly) { final @Nullable ServerPlayer online = this.serverHolder.requireServer().getPlayerList().getPlayerByName(username); if (online != null) { return CompletableFuture.completedFuture(online.getUUID()); } return this.mojang.resolveUUID(username, cacheOnly); } @Override public CompletableFuture<@Nullable String> resolveName(final UUID uuid, final boolean cacheOnly) { final @Nullable ServerPlayer online = this.serverHolder.requireServer().getPlayerList().getPlayer(uuid); if (online != null) { return CompletableFuture.completedFuture(online.getGameProfile().name()); } return this.mojang.resolveName(uuid, cacheOnly); } @Override public void shutdown() { this.mojang.shutdown(); } } ================================================ FILE: fabric/src/main/resources/carbonchat.mixins.json ================================================ { "required": true, "minVersion": "0.8.4", "package": "net.draycia.carbon.fabric.mixin", "compatibilityLevel": "JAVA_21", "mixins": [], "client": [ ], "injectors": { "defaultRequire": 1 } } ================================================ FILE: fabric/src/main/resources/data/carbonchat/chat_type/chat.json ================================================ { "chat": { "parameters": [ "content" ], "translation_key": "%s" }, "narration": { "parameters": [ "content" ], "translation_key": "%s" } } ================================================ FILE: fabric/src/main/resources/pack.mcmeta ================================================ { "pack": { "pack_format": 18, "description": "CarbonChat Data" } } ================================================ FILE: gradle/libs.versions.toml ================================================ [plugins] sponge-gradle = { id = "org.spongepowered.gradle.plugin", version = "2.3.0" } hangar-publish = { id = "io.papermc.hangar-publish-plugin", version = "0.1.4" } indra-publishing-sonatype = { id = "net.kyori.indra.publishing.sonatype", version.ref = "indra" } javadoc-links = { id = "org.incendo.cloud-build-logic.javadoc-links" } cloud-buildLogic-rootProject-publishing = { id = "org.incendo.cloud-build-logic.publishing.root-project", version.ref = "cloud-build-logic" } resource-factory-paper-convention = { id = "xyz.jpenilla.resource-factory-paper-convention", version.ref = "resource-factory" } resource-factory-bukkit-convention = { id = "xyz.jpenilla.resource-factory-bukkit-convention", version.ref = "resource-factory" } resource-factory-velocity-convention = { id = "xyz.jpenilla.resource-factory-velocity-convention", version.ref = "resource-factory" } resource-factory-fabric-convention = { id = "xyz.jpenilla.resource-factory-fabric-convention", version.ref = "resource-factory" } [versions] indra = "4.0.0" cloud-build-logic = "0.0.17" shadow = "9.4.1" mod-publish-plugin = "1.1.0" gremlin = "0.0.9" runTask = "3.0.2" resource-factory = "1.3.1" adventure = "4.18.0" cloud = "2.0.0" cloudMinecraft = "2.0.0-beta.15" cloudModded = "2.0.0-beta.15" cloudSponge = "2.0.0-SNAPSHOT" configurate = "4.2.0" checkerQual = "3.49.2" stylecheck = "0.2.1" bstats = "3.1.0" paperApi = "1.21.4-R0.1-SNAPSHOT" paperTrail = "1.0.1" foliaApi = "1.21.4-R0.1-SNAPSHOT" event = "1.0.0" registry = "1.0.0-SNAPSHOT" kyoriMoonshine = "2.0.4" guice = "7.0.0" velocityApi = "3.5.0-SNAPSHOT" minecraft = "1.21.11" fabricLoader = "0.18.3" fabricApi = "0.139.5+1.21.11" fabricPermissionsApi = "0.6.1" adventurePlatformFabric = "6.8.0" luckPermsApi = "5.5" essentialsx = "2.20.1" discordsrv = "1.30.1" placeholderapi = "2.11.6" miniplaceholders = "3.1.0" jdbi = "3.49.6" hikari = "7.0.2" mysql = "9.7.0" flyway = "11.14.1" caffeine = "3.2.3" mariadb = "3.5.7" messenger = "1.1.0-SNAPSHOT" zstdjni = "1.5.7-6" jedis = "7.0.0" postgresql = "42.7.8" rabbitmq = "5.27.0" nats = "2.23.0" h2 = "2.4.240" towny = "0.101.2.5" plotsquared_bom = "1.55" plotsquared_core = "7.5.8" mcmmo = "2.2.043" adpParties = "3.2.9" fuuid = "1.6.9.5-U0.6.35" # synced with version used by lowest supported mc (currently 1.21.4 on paper) gson = "2.11.0" guava = "33.3.1-jre" log4j = "2.24.1" netty = "4.1.115.Final" [libraries] indraCommon = { group = "net.kyori", name = "indra-common", version.ref = "indra" } cloud-build-logic = { module = "org.incendo:cloud-build-logic", version.ref = "cloud-build-logic" } indraLicenseHeader = { group = "net.kyori", name = "indra-licenser-spotless", version.ref = "indra" } shadow = { group = "com.gradleup.shadow", name = "shadow-gradle-plugin", version.ref = "shadow" } mod-publish-plugin = { module = "me.modmuss50:mod-publish-plugin", version.ref = "mod-publish-plugin" } gremlin-gradle = { group = "xyz.jpenilla", name = "gremlin-gradle", version.ref = "gremlin" } run-task = { module = "xyz.jpenilla:run-task", version.ref = "runTask" } adventureBom = { group = "net.kyori", name = "adventure-bom", version.ref = "adventure" } adventureApi = { group = "net.kyori", name = "adventure-api" } adventureTextSerializerGson = { group = "net.kyori", name = "adventure-text-serializer-gson" } adventureTextSerializerPlain = { group = "net.kyori", name = "adventure-text-serializer-plain" } adventureTextSerializerLegacy = { group = "net.kyori", name = "adventure-text-serializer-legacy" } adventureSerializerConfigurate4 = { group = "net.kyori", name = "adventure-serializer-configurate4", version.ref = "adventure" } minimessage = { group = "net.kyori", name = "adventure-text-minimessage", version.ref = "adventure" } adventurePlatformFabric = { group = "net.kyori", name = "adventure-platform-fabric", version.ref = "adventurePlatformFabric" } log4jBom = { group = "org.apache.logging.log4j", name = "log4j-bom", version.ref = "log4j" } log4jApi = { group = "org.apache.logging.log4j", name = "log4j-api" } event = { group = "com.sasorio", name = "event-api", version.ref = "event" } registry = { group = "com.seiama", name = "registry", version.ref = "registry" } kyoriMoonshine = { group = "net.kyori.moonshine", name = "moonshine", version.ref = "kyoriMoonshine" } kyoriMoonshineCore = { group = "net.kyori.moonshine", name = "moonshine-core", version.ref = "kyoriMoonshine" } kyoriMoonshineStandard = { group = "net.kyori.moonshine", name = "moonshine-standard", version.ref = "kyoriMoonshine" } cloudBom = { module = "org.incendo:cloud-bom", version.ref = "cloud" } cloudCore = { group = "org.incendo", name = "cloud-core", version.ref = "cloud" } brigadier = "com.mojang:brigadier:1.0.18" cloudMinecraftBom = { module = "org.incendo:cloud-minecraft-bom", version.ref = "cloudMinecraft" } cloudMinecraftExtras = { group = "org.incendo", name = "cloud-minecraft-extras", version.ref = "cloudMinecraft" } cloudPaper = { group = "org.incendo", name = "cloud-paper", version.ref = "cloudMinecraft" } cloudSigned = { group = "org.incendo", name = "cloud-minecraft-signed-arguments", version.ref = "cloudMinecraft" } cloudPaperSigned = { group = "org.incendo", name = "cloud-paper-signed-arguments", version.ref = "cloudMinecraft" } cloudSponge = { group = "org.incendo", name = "cloud-sponge", version.ref = "cloudSponge" } cloudVelocity = { group = "org.incendo", name = "cloud-velocity", version.ref = "cloudMinecraft" } cloudFabric = { group = "org.incendo", name = "cloud-fabric", version.ref = "cloudModded" } configurateCore = { group = "org.spongepowered", name = "configurate-core", version.ref = "configurate" } configurateHocon = { group = "org.spongepowered", name = "configurate-hocon", version.ref = "configurate" } configurateYaml = { group = "org.spongepowered", name = "configurate-yaml", version.ref = "configurate" } checkerQual = { group = "org.checkerframework", name = "checker-qual", version.ref = "checkerQual" } stylecheck = { group = "ca.stellardrift", name = "stylecheck", version.ref = "stylecheck" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } bstatsBukkit = { group = "org.bstats", name = "bstats-bukkit", version.ref = "bstats" } bstatsVelocity = { group = "org.bstats", name = "bstats-velocity", version.ref = "bstats" } bstatsSponge = { group = "org.bstats", name = "bstats-sponge", version.ref = "bstats" } guice = { group = "com.google.inject", name = "guice", version.ref = "guice" } assistedInject = { group = "com.google.inject.extensions", name = "guice-assistedinject", version.ref = "guice" } jdbiCore = { group = "org.jdbi", name = "jdbi3-core", version.ref = "jdbi" } jdbiObject = { group = "org.jdbi", name = "jdbi3-sqlobject", version.ref = "jdbi" } jdbiPostgres = { group = "org.jdbi", name = "jdbi3-postgres", version.ref = "jdbi" } hikariCP = { group = "com.zaxxer", name = "HikariCP", version.ref = "hikari" } flyway = { group = "org.flywaydb", name = "flyway-core", version.ref = "flyway" } flywayMysql = { group = "org.flywaydb", name = "flyway-mysql", version.ref = "flyway" } flywayPostgres = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" } mysql = { group = "com.mysql", name = "mysql-connector-j", version.ref = "mysql" } postgresql = { group = "org.postgresql", name = "postgresql", version.ref = "postgresql" } caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version.ref = "caffeine" } mariadb = { group = "org.mariadb.jdbc", name = "mariadb-java-client", version.ref = "mariadb" } h2 = { group = "com.h2database", name = "h2", version.ref = "h2" } jarRelocator = { group = "me.lucko", name = "jar-relocator", version = "1.7" } messenger = { group = "de.hexaoxi", name = "messenger-api", version.ref = "messenger" } messengerNats = { group = "de.hexaoxi", name = "messenger-nats", version.ref = "messenger" } messengerRabbitmq = { group = "de.hexaoxi", name = "messenger-rabbitmq", version.ref = "messenger" } messengerRedis = { group = "de.hexaoxi", name = "messenger-redis", version.ref = "messenger" } netty = { group = "io.netty", name = "netty-all", version.ref = "netty" } zstdjni = { group = "com.github.luben", name = "zstd-jni", version.ref = "zstdjni" } jedis = { group = "redis.clients", name = "jedis", version.ref = "jedis" } rabbitmq = { group = "com.rabbitmq", name = "amqp-client", version.ref = "rabbitmq" } nats = { group = "io.nats", name = "jnats", version.ref = "nats" } paperApi = { group = "io.papermc.paper", name = "paper-api", version.ref = "paperApi" } paperTrail = { group = "io.papermc", name = "paper-trail", version.ref = "paperTrail" } foliaApi = { group = "dev.folia", name = "folia-api", version.ref = "foliaApi" } velocityApi = { group = "com.velocitypowered", name = "velocity-api", version.ref = "velocityApi" } fabricMinecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft" } fabricLoader = { group = "net.fabricmc", name = "fabric-loader", version.ref = "fabricLoader" } fabricApi = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabricApi" } fabricApiDeprecated = { group = "net.fabricmc.fabric-api", name = "fabric-api-deprecated", version.ref = "fabricApi" } fabricPermissionsApi = { group = "me.lucko", name = "fabric-permissions-api", version.ref = "fabricPermissionsApi" } gremlin-runtime = { group = "xyz.jpenilla", name = "gremlin-runtime", version.ref = "gremlin" } luckPermsApi = { group = "net.luckperms", name = "api", version.ref = "luckPermsApi" } essentialsXDiscord = { group = "net.essentialsx", name = "EssentialsXDiscord", version.ref = "essentialsx" } discordsrv = { group = "com.discordsrv", name = "discordsrv", version.ref = "discordsrv" } placeholderapi = { group = "me.clip", name = "placeholderapi", version.ref = "placeholderapi" } miniplaceholders = { group = "io.github.miniplaceholders", name = "miniplaceholders-api", version.ref = "miniplaceholders" } towny = { group = "com.palmergames.bukkit.towny", name = "towny", version.ref = "towny" } plotsquaredbom = { group = "com.intellectualsites.bom", name = "bom-newest", version.ref = "plotsquared_bom" } plotsquaredcore = { group = "com.intellectualsites.plotsquared", name = "plotsquared-core", version.ref = "plotsquared_core" } mcmmo = { group = "com.gmail.nossr50.mcMMO", name = "mcMMO", version.ref = "mcmmo" } adpParties = { group = "com.alessiodp.parties", name = "parties-api", version.ref = "adpParties" } factionsUuid = { group = "com.massivecraft", name = "Factions", version.ref = "fuuid" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Project Properties projectVersion=3.0.0-SNAPSHOT group=de.hexaoxi description=CarbonChat - A modern chat plugin # Gradle Properties org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx2G ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables, and ensure extensions are enabled setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 "%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 "%COMSPEC%" /c exit 1 :execute @rem Setup the command line @rem Execute Gradle @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel :exitWithErrorLevel @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts "%COMSPEC%" /c exit %ERRORLEVEL% ================================================ FILE: paper/build.gradle.kts ================================================ import xyz.jpenilla.resourcefactory.paper.PaperPluginYaml.Load import xyz.jpenilla.runpaper.task.RunServer plugins { id("carbon.shadow-platform") alias(libs.plugins.resource.factory.paper.convention) alias(libs.plugins.resource.factory.bukkit.convention) id("xyz.jpenilla.run-paper") id("carbon.permissions") id("carbon.configurable-plugins") } dependencies { implementation(projects.carbonchatCommon) // Server compileOnly(libs.foliaApi) implementation(libs.paperTrail) // Commands implementation(libs.cloudPaper) implementation(libs.cloudPaperSigned) // Misc implementation(libs.bstatsBukkit) // Plugins compileOnly(libs.placeholderapi) compileOnly(libs.miniplaceholders) compileOnly(libs.essentialsXDiscord) { exclude("org.spigotmc", "spigot-api") } compileOnly(libs.discordsrv) { isTransitive = false } compileOnly(libs.towny) compileOnly(libs.mcmmo) { isTransitive = false } compileOnly(libs.adpParties) compileOnly(libs.factionsUuid) implementation(libs.plotsquaredbom) compileOnly(libs.plotsquaredcore) } configurablePlugins { dependency(libs.towny) dependency(libs.mcmmo) dependency(libs.adpParties) dependency(libs.factionsUuid) dependency(libs.plotsquaredbom) dependency(libs.plotsquaredcore) } tasks { shadowJar { relocateDependency("io.papermc.papertrail") relocateDependency("io.leangen.geantyref") relocateDependency("xyz.jpenilla.reflectionremapper") relocateDependency("net.fabricmc.mappingio") relocateCloud() } val luckperms = FetchLuckPermsJar.setup(project, "bukkit") withType(RunServer::class).configureEach { version.set("1.21.4") downloadPlugins { github("MiniPlaceholders", "MiniPlaceholders", libs.versions.miniplaceholders.get(), "MiniPlaceholders-Paper-${libs.versions.miniplaceholders.get()}.jar") // TODO: install MP extensions to its folder // github("MiniPlaceholders", "PlaceholderAPI-Expansion", "2.1.0", "PlaceholderAPI-Expansion-2.1.0.jar") hangar("PlaceholderAPI", libs.versions.placeholderapi.get()) modrinth("parties", libs.versions.adpParties.get()) } pluginJars.from(luckperms.flatMap { it.outputFile }) providers.gradleProperty("smokeTest").map { it.toBoolean() }.getOrElse(false).let { smokeTest -> if (smokeTest) { runDirectory.set(layout.buildDirectory.dir("tmp/smokeTest")) doFirst { runDirectory.get().file("carbonchat-smoketest").asFile.takeIf { it.exists() }?.delete() runDirectory.get().file("eula.txt").asFile.also { it.parentFile.mkdirs() }.writeText("eula=true") } doLast { val pass = runDirectory.get().file("carbonchat-smoketest").asFile.exists() if (!pass) { throw GradleException("Smoke test failed, please check the logs.") } } systemProperty("carbonchat.smokeTest", true) systemProperty("carbonchat.smokeTestMode", providers.gradleProperty("smokeTestMode").getOrElse("h2")) systemProperty("paper.disablePluginRemapping", true) } } } register("runServer2") { pluginJars.from(shadowJar.flatMap { it.archiveFile }) runDirectory.set(layout.projectDirectory.dir("run2")) } } runPaper.folia.registerTask() paperPluginYaml { name = rootProject.name loader = "net.draycia.carbon.paper.CarbonPaperLoader" main = "net.draycia.carbon.paper.CarbonPaperBootstrap" apiVersion = "1.21.4" authors = listOf("Draycia", "jmp") website = GITHUB_REPO_URL foliaSupported = true dependencies { server("LuckPerms", Load.BEFORE, true) server("PlaceholderAPI", Load.BEFORE, false) server("EssentialsDiscord", Load.BEFORE, false) server("DiscordSRV", Load.BEFORE, false) server("MiniPlaceholders", Load.BEFORE, false) // Integrations server("Towny", Load.BEFORE, false) server("mcMMO", Load.BEFORE, false) server("Parties", Load.BEFORE, false) server("Factions", Load.BEFORE, false) server("PlotSquared", Load.BEFORE, false) } } bukkitPluginYaml { name = rootProject.name main = "carbonchat.libs.io.papermc.papertrail.RequiresPaperPlugins" apiVersion = "1.21.4" authors = listOf("Draycia", "jmp") website = GITHUB_REPO_URL } carbonPermission.permissions.get().forEach { setOf(bukkitPluginYaml.permissions, paperPluginYaml.permissions).forEach { container -> container.register(it.string) { description = it.description it.children?.let { children.putAll(it) } } } } publishMods.modrinth { modLoaders.addAll("paper", "folia") } configurations.runtimeDownload { exclude("org.checkerframework", "checker-qual") } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/CarbonChatPaper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import java.io.File; import java.io.IOException; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.common.CarbonChatInternal; import net.draycia.carbon.common.PeriodicTasks; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.command.ExecutionCoordinatorHolder; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.config.MessagingSettings; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.users.PlatformUserManager; import net.draycia.carbon.common.users.ProfileCache; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.paper.hooks.CarbonPAPIPlaceholders; import net.draycia.carbon.paper.hooks.PAPIChatHook; import org.apache.logging.log4j.LogManager; import org.bstats.bukkit.Metrics; import org.bstats.charts.SimplePie; import org.bukkit.Bukkit; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) @Singleton public final class CarbonChatPaper extends CarbonChatInternal { private static final int BSTATS_PLUGIN_ID = 8720; private final JavaPlugin plugin; @Inject private CarbonChatPaper( final Injector injector, final JavaPlugin plugin, final CarbonMessages carbonMessages, final CarbonEventHandler eventHandler, final CarbonChannelRegistry channelRegistry, final Provider messagingManager, final CarbonServer carbonServer, final PlatformUserManager userManager, @PeriodicTasks final ScheduledExecutorService periodicTasks, final ProfileCache profileCache, final ProfileResolver profileResolver, final ExecutionCoordinatorHolder commandExecutor ) { super( injector, LogManager.getLogger("CarbonChat"), periodicTasks, profileCache, profileResolver, userManager, commandExecutor, carbonServer, carbonMessages, eventHandler, channelRegistry, messagingManager ); this.plugin = plugin; } void onEnable() { this.init(); final Set listeners = this.injector().getInstance(Key.get(new TypeLiteral>() {})); for (final Listener listener : listeners) { this.plugin.getServer().getPluginManager().registerEvents( listener, this.plugin ); } this.registerPlaceholders(); final Metrics metrics = new Metrics(this.plugin, BSTATS_PLUGIN_ID); metrics.addCustomChart(new SimplePie("user_manager_type", () -> this.injector().getInstance(ConfigManager.class).primaryConfig().storageType().name())); metrics.addCustomChart(new SimplePie("messaging", () -> { final MessagingSettings settings = this.injector().getInstance(ConfigManager.class).primaryConfig().messagingSettings(); if (!settings.enabled()) { return "disabled"; } return settings.brokerType().name(); })); this.checkVersion(); if (Boolean.getBoolean("carbonchat.smokeTest")) { this.logger().info("Smoke test: CarbonChat successfully enabled."); try { new File("carbonchat-smoketest").createNewFile(); } catch (final IOException e) { this.logger().error("Smoke test: Failed to create file.", e); } this.plugin.getServer().getScheduler().runTaskLater(this.plugin, () -> { this.logger().info("Smoke test: Shutting down server."); Bukkit.getServer().shutdown(); }, 20L); } } private void registerPlaceholders() { if (papiLoaded()) { this.injector().getInstance(PAPIChatHook.class); this.injector().getInstance(CarbonPAPIPlaceholders.class); } } void onDisable() { this.shutdown(); } public static boolean papiLoaded() { return Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI"); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/CarbonChatPaperModule.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper; import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.multibindings.Multibinder; import java.nio.file.Path; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.CarbonCommonModule; import net.draycia.carbon.common.CarbonPlatformModule; import net.draycia.carbon.common.DataDirectory; import net.draycia.carbon.common.PlatformScheduler; import net.draycia.carbon.common.RawChat; import net.draycia.carbon.common.command.Commander; import net.draycia.carbon.common.command.ExecutionCoordinatorHolder; import net.draycia.carbon.common.integration.Integration; import net.draycia.carbon.common.messages.CarbonMessageRenderer; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.users.PlatformUserManager; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.common.util.CloudUtils; import net.draycia.carbon.paper.command.PaperCommander; import net.draycia.carbon.paper.command.PaperPlayerCommander; import net.draycia.carbon.paper.integration.alessiodp_parties.AlessiodpPartiesIntegration; import net.draycia.carbon.paper.integration.dsrv.DSRVIntegration; import net.draycia.carbon.paper.integration.essxd.EssXDIntegration; import net.draycia.carbon.paper.integration.fuuid.FactionsIntegration; import net.draycia.carbon.paper.integration.mcmmo.McmmoIntegration; import net.draycia.carbon.paper.integration.plotsquared.PlotSquaredIntegration; import net.draycia.carbon.paper.integration.towny.TownyIntegration; import net.draycia.carbon.paper.listeners.PaperChatListener; import net.draycia.carbon.paper.listeners.PaperPlayerJoinListener; import net.draycia.carbon.paper.messages.PaperMessageRenderer; import net.draycia.carbon.paper.users.CarbonPlayerPaper; import net.draycia.carbon.paper.users.PaperProfileResolver; import net.kyori.adventure.key.Key; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.SenderMapper; import org.incendo.cloud.paper.LegacyPaperCommandManager; @DefaultQualifier(NonNull.class) public final class CarbonChatPaperModule extends CarbonPlatformModule { private final Logger logger = LogManager.getLogger("CarbonChat"); private final CarbonPaperBootstrap bootstrap; CarbonChatPaperModule(final CarbonPaperBootstrap bootstrap) { this.bootstrap = bootstrap; } @Provides @Singleton @SuppressWarnings("unused") public CommandManager commandManager(final UserManager userManager, final CarbonMessages messages, final ExecutionCoordinatorHolder executionCoordinatorHolder) { final LegacyPaperCommandManager commandManager = new LegacyPaperCommandManager<>( this.bootstrap, executionCoordinatorHolder.executionCoordinator(), SenderMapper.create( commandSender -> { if (commandSender instanceof Player player) { return new PaperPlayerCommander(userManager, player); } return PaperCommander.from(commandSender); }, commander -> ((PaperCommander) commander).commandSender() ) ); CloudUtils.decorateCommandManager(commandManager, messages, this.logger); commandManager.registerBrigadier(); return commandManager; } @Override protected void configurePlatform() { this.install(new CarbonCommonModule()); this.bind(CarbonChat.class).to(CarbonChatPaper.class); this.bind(JavaPlugin.class).toInstance(this.bootstrap); this.bind(Server.class).toInstance(this.bootstrap.getServer()); this.bind(Logger.class).toInstance(this.logger); this.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(this.bootstrap.getDataFolder().toPath()); this.bind(CarbonServer.class).to(CarbonServerPaper.class); this.bind(ProfileResolver.class).to(PaperProfileResolver.class); this.bind(PlatformScheduler.class).to(PaperScheduler.class); this.install(PlatformUserManager.PlayerFactory.moduleFor(CarbonPlayerPaper.class)); this.bind(CarbonMessageRenderer.class).to(PaperMessageRenderer.class); this.bind(Key.class).annotatedWith(RawChat.class).toInstance(Key.key("paper:raw")); this.configureListeners(); } @Override protected void configureIntegrations(final Multibinder integrations, final Multibinder configs) { super.configureIntegrations(integrations, configs); integrations.addBinding().to(TownyIntegration.class); configs.addBinding().toInstance(TownyIntegration.configMeta()); integrations.addBinding().to(McmmoIntegration.class); configs.addBinding().toInstance(McmmoIntegration.configMeta()); integrations.addBinding().to(AlessiodpPartiesIntegration.class); configs.addBinding().toInstance(AlessiodpPartiesIntegration.configMeta()); integrations.addBinding().to(FactionsIntegration.class); configs.addBinding().toInstance(FactionsIntegration.configMeta()); integrations.addBinding().to(EssXDIntegration.class); configs.addBinding().toInstance(EssXDIntegration.configMeta()); integrations.addBinding().to(DSRVIntegration.class); configs.addBinding().toInstance(DSRVIntegration.configMeta()); integrations.addBinding().to(PlotSquaredIntegration.class); configs.addBinding().toInstance(PlotSquaredIntegration.configMeta()); } private void configureListeners() { final Multibinder listeners = Multibinder.newSetBinder(this.binder(), Listener.class); listeners.addBinding().to(PaperChatListener.class); listeners.addBinding().to(PaperPlayerJoinListener.class); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/CarbonPaperBootstrap.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper; import com.google.inject.Guice; import net.draycia.carbon.api.CarbonChatProvider; import org.bukkit.plugin.java.JavaPlugin; 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 CarbonPaperBootstrap extends JavaPlugin { private @MonotonicNonNull CarbonChatPaper carbonChat; @Override public void onLoad() { this.carbonChat = Guice.createInjector(new CarbonChatPaperModule(this)) .getInstance(CarbonChatPaper.class); CarbonChatProvider.register(this.carbonChat); } @Override public void onEnable() { if (this.carbonChat != null) { this.carbonChat.onEnable(); } } @Override public void onDisable() { if (this.carbonChat != null) { this.carbonChat.onDisable(); } } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/CarbonPaperLoader.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper; import io.papermc.paper.plugin.loader.PluginClasspathBuilder; import io.papermc.paper.plugin.loader.PluginLoader; import net.draycia.carbon.common.util.CarbonDependencies; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import xyz.jpenilla.gremlin.runtime.platformsupport.PaperClasspathAppender; @DefaultQualifier(NonNull.class) public class CarbonPaperLoader implements PluginLoader { @Override public void classloader(final PluginClasspathBuilder classpathBuilder) { new PaperClasspathAppender(classpathBuilder).append( CarbonDependencies.resolve(classpathBuilder.getContext().getDataDirectory().resolve("libraries")) ); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/CarbonServerPaper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.List; import java.util.Objects; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import org.bukkit.Server; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @Singleton @DefaultQualifier(NonNull.class) public final class CarbonServerPaper implements CarbonServer, ForwardingAudience.Single { private final Server server; private final UserManager userManager; @Inject private CarbonServerPaper(final Server server, final UserManager userManager) { this.server = server; this.userManager = userManager; } @Override public Audience audience() { return this.server; } @Override public Audience console() { return new ConsoleCarbonPlayer(this.server.getConsoleSender()); } @Override public List players() { return this.server.getOnlinePlayers().stream() .map(bukkit -> this.userManager.user(bukkit.getUniqueId()).getNow(null)) .filter(Objects::nonNull) .toList(); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/PaperScheduler.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper; import com.google.inject.Inject; import com.google.inject.Singleton; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.PlatformScheduler; import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @Singleton @DefaultQualifier(NonNull.class) public final class PaperScheduler implements PlatformScheduler { private static final boolean FOLIA; static { boolean folia; try { Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); folia = true; } catch (final ClassNotFoundException exception) { folia = false; } FOLIA = folia; } private final JavaPlugin plugin; private final Server server; private final @Nullable Folia folia; @Inject private PaperScheduler(final JavaPlugin plugin, final Server server) { this.plugin = plugin; this.server = server; this.folia = FOLIA ? new Folia() : null; } public void scheduleForPlayer(final CarbonPlayer carbonPlayer, final Runnable runnable) { if (this.folia != null) { this.folia.scheduleForPlayer(carbonPlayer, runnable); return; } if (this.server.isPrimaryThread()) { runnable.run(); } else { this.server.getScheduler().runTask(this.plugin, runnable); } } // inner class to avoid Guice trying to load ScheduledTask when scanning for methods to inject, // and finding the synthetic method generated for our ScheduledTask consumer lambda private final class Folia implements PlatformScheduler { @Override public void scheduleForPlayer(final CarbonPlayer carbonPlayer, final Runnable runnable) { final @Nullable Player player = PaperScheduler.this.server.getPlayer(carbonPlayer.uuid()); if (player == null) { runnable.run(); return; } if (PaperScheduler.this.server.isOwnedByCurrentRegion(player)) { runnable.run(); } else { player.getScheduler().run(PaperScheduler.this.plugin, $ -> runnable.run(), null); } } } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/command/PaperCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.command; import net.draycia.carbon.common.command.Commander; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import org.bukkit.command.CommandSender; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public interface PaperCommander extends Commander, ForwardingAudience.Single { static PaperCommander from(final CommandSender sender) { return new PaperCommanderImpl(sender); } CommandSender commandSender(); record PaperCommanderImpl(CommandSender commandSender) implements PaperCommander { @Override public Audience audience() { return this.commandSender; } @Override public boolean hasPermission(final String permission) { return this.commandSender.hasPermission(permission); } } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/command/PaperPlayerCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.command; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.command.PlayerCommander; import net.kyori.adventure.audience.Audience; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import static java.util.Objects.requireNonNull; @DefaultQualifier(NonNull.class) public record PaperPlayerCommander( UserManager userManager, Player player ) implements PlayerCommander, PaperCommander { @Override public CommandSender commandSender() { return this.player; } @Override public Audience audience() { return this.player; } @Override public CarbonPlayer carbonPlayer() { return requireNonNull(this.userManager.user(this.player.getUniqueId()).join(), "No CarbonPlayer for logged in Player!"); } @Override public boolean hasPermission(final String permission) { return this.player.hasPermission(permission); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/hooks/CarbonPAPIPlaceholders.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.hooks; import com.google.inject.Inject; import java.util.Locale; import java.util.Map; import java.util.function.Function; import me.clip.placeholderapi.expansion.PlaceholderExpansion; 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.users.UserManager; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.OfflinePlayer; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class CarbonPAPIPlaceholders extends PlaceholderExpansion { private final UserManager userManager; private final ChannelRegistry channels; private final JavaPlugin plugin; private final Map> componentResolvers; private final Map> stringResolvers; @Inject public CarbonPAPIPlaceholders( final UserManager userManager, final ChannelRegistry channels, final JavaPlugin plugin ) { this.userManager = userManager; this.channels = channels; this.plugin = plugin; this.componentResolvers = Map.of( "party", this::partyName, "nickname", this::nickname, "displayname", this::displayName ); this.stringResolvers = Map.of( "channel_key", this::selectedChannelKey ); this.register(); } @Override public String getIdentifier() { return this.plugin.getName().toLowerCase(Locale.ROOT); } @Override public String getAuthor() { return "[" + String.join(", ", this.plugin.getPluginMeta().getAuthors()) + "]"; } @Override public String getVersion() { return this.plugin.getPluginMeta().getVersion(); } @Override public boolean persist() { return true; } @Override public @Nullable String onRequest(final OfflinePlayer player, final String params) { for (final Map.Entry> entry : this.componentResolvers.entrySet()) { if (params.endsWith(entry.getKey())) { return mm(entry.getValue().apply(player)); } else if (params.endsWith(entry.getKey() + "_l")) { return legacy(entry.getValue().apply(player)); } else if (params.endsWith(entry.getKey() + "_p")) { return plain(entry.getValue().apply(player)); } } for (final Map.Entry> entry : this.stringResolvers.entrySet()) { if (params.endsWith(entry.getKey())) { return entry.getValue().apply(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 static String plain(final Component in) { return PlainTextComponentSerializer.plainText().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(); } private Component nickname(final OfflinePlayer player) { final CarbonPlayer carbonPlayer = this.userManager.user(player.getUniqueId()).join(); final @Nullable Component nickname = carbonPlayer.nickname(); return nickname == null ? Component.text(carbonPlayer.username()) : nickname; } private String selectedChannelKey(final OfflinePlayer player) { final CarbonPlayer carbonPlayer = this.userManager.user(player.getUniqueId()).join(); final @Nullable ChatChannel selected = carbonPlayer.selectedChannel(); if (selected != null) { return selected.key().asString(); } return this.channels.defaultKey().asString(); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/hooks/PAPIChatHook.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.hooks; import com.google.inject.Inject; import me.clip.placeholderapi.PlaceholderAPI; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.common.event.events.CarbonEarlyChatEvent; import net.draycia.carbon.common.listeners.Listener; import net.draycia.carbon.common.util.ColorUtils; import net.draycia.carbon.paper.CarbonChatPaper; import net.draycia.carbon.paper.users.CarbonPlayerPaper; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class PAPIChatHook implements Listener { @Inject public PAPIChatHook(final CarbonEventHandler events) { events.subscribe(CarbonEarlyChatEvent.class, 0, false, event -> { if (!CarbonChatPaper.papiLoaded()) { return; } if (!event.sender().hasPermission("carbon.chatplaceholders")) { return; } if (!(event.sender() instanceof CarbonPlayerPaper playerPaper)) { return; } final String papiParsed = PlaceholderAPI.setPlaceholders(playerPaper.bukkitPlayer(), event.message()); event.message(ColorUtils.legacyToMiniMessage(papiParsed)); }); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/alessiodp_parties/AlessiodpPartiesIntegration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.alessiodp_parties; import com.google.inject.Inject; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @DefaultQualifier(NonNull.class) public class AlessiodpPartiesIntegration implements Integration { private final CarbonChannelRegistry channelRegistry; private final ConfigManager configManager; private final Logger logger; private final AlessiodpPartiesIntegration.Config config; @Inject public AlessiodpPartiesIntegration( final CarbonChannelRegistry channelRegistry, final ConfigManager configManager, final Logger logger ) { this.channelRegistry = channelRegistry; this.configManager = configManager; this.logger = logger; this.config = this.config(configManager, configMeta()); } @Override public boolean eligible() { try { Class.forName("com.alessiodp.parties.api.Parties"); return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled("Parties"); } catch (final ClassNotFoundException ignored) { return false; } } @Override public void register() { if (this.config.partyChannel) { if (this.configManager.primaryConfig().partyChat().enabled) { this.logger.warn("Both CarbonChat parties and the Parties party chat channel are enabled!"); this.logger.warn("Usually, you want one or the other enabled. Additionally, their default channel configs will conflict."); } this.channelRegistry.registerSpecialConfigChannel(AlessiodpPartiesPartyChannel.FILE_NAME, AlessiodpPartiesPartyChannel.class); } } public static ConfigMeta configMeta() { return Integration.configMeta("alessiodp-parties", AlessiodpPartiesIntegration.Config.class); } @ConfigSerializable public static final class Config { boolean enabled = true; @Comment("You will likely want to disable Carbon's built-in party system above when using Parties party chat.") boolean partyChannel = true; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/alessiodp_parties/AlessiodpPartiesPartyChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.alessiodp_parties; import com.alessiodp.parties.api.Parties; import com.alessiodp.parties.api.interfaces.Party; import com.alessiodp.parties.api.interfaces.PartyPlayer; import com.google.inject.Inject; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.ConfigChatChannel; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @DefaultQualifier(NonNull.class) @ConfigSerializable public class AlessiodpPartiesPartyChannel extends ConfigChatChannel { public static final String FILE_NAME = "alessiodp-parties-party.conf"; private transient @MonotonicNonNull @Inject CarbonMessages messages; private transient @MonotonicNonNull @Inject UserManager users; public AlessiodpPartiesPartyChannel() { this.key = Key.key("carbon", "partychat"); this.commandAliases = List.of("pc"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(party: %party%) : ", "console", "[party: %party%] : " ); } @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( this.party(player) != null, () -> this.messages.cannotUseADPPartiesPartyChannel(player) )); } @Override public List recipients(final CarbonPlayer sender) { final @Nullable Party party = this.party(sender); if (party == null) { if (sender.online()) { sender.sendMessage(this.messages.cannotUseADPPartiesPartyChannel(sender)); } return Collections.emptyList(); } final List recipients = new ArrayList<>(); for (final PartyPlayer player : party.getOnlineMembers()) { final @Nullable CarbonPlayer carbon = this.users.user(player.getPlayerUUID()).getNow(null); if (carbon != null) { recipients.add(carbon); } } recipients.add(this.server.console()); return recipients; } private @Nullable Party party(final CarbonPlayer player) { return Parties.getApi().getPartyOfPlayer(player.uuid()); } @Override public boolean shouldCrossServer() { return false; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/dsrv/DSRVIntegration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.dsrv; import com.google.inject.Inject; import com.google.inject.Injector; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import org.bukkit.Bukkit; import org.spongepowered.configurate.objectmapping.ConfigSerializable; public final class DSRVIntegration implements Integration { private final Injector injector; private final DSRVIntegration.Config config; @Inject private DSRVIntegration( final Injector injector, final ConfigManager configManager ) { this.injector = injector; this.config = this.config(configManager, configMeta()); } @Override public boolean eligible() { return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled("DiscordSRV"); } @Override public void register() { this.injector.getInstance(DSRVListener.class).register(); } public static ConfigMeta configMeta() { return Integration.configMeta("discordsrv", DSRVIntegration.Config.class); } @ConfigSerializable public static final class Config { boolean enabled = true; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/dsrv/DSRVListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.dsrv; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.inject.Inject; import github.scarsz.discordsrv.Debug; import github.scarsz.discordsrv.DiscordSRV; import github.scarsz.discordsrv.api.Subscribe; import github.scarsz.discordsrv.api.events.GameChatMessagePreProcessEvent; import github.scarsz.discordsrv.hooks.chat.ChatHook; import java.time.Duration; import net.draycia.carbon.api.channels.ChatChannel; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.api.event.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.messages.TagPermissions; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.draycia.carbon.common.users.WrappedCarbonPlayer; import net.draycia.carbon.common.util.ChannelUtils; import net.draycia.carbon.paper.users.CarbonPlayerPaper; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.apache.commons.lang3.tuple.ImmutablePair; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.Nullable; public final class DSRVListener implements ChatHook { private final CarbonChannelRegistry channelRegistry; private final JavaPlugin plugin; private final CarbonEventHandler eventHandler; @Inject private DSRVListener( final CarbonEventHandler eventHandler, final CarbonChannelRegistry channelRegistry, final JavaPlugin plugin ) { this.channelRegistry = channelRegistry; this.eventHandler = eventHandler; this.plugin = plugin; } public void register() { DiscordSRV.getPlugin().getPluginHooks().add(this); final Cache, Component> awaitingEvent = Caffeine.newBuilder() .expireAfterWrite(Duration.ofMillis(25)) .build(); this.eventHandler.subscribe(CarbonChatEvent.class, 100, false, event -> { final ChatChannel chatChannel = event.chatChannel(); final CarbonPlayer carbonPlayer = event.sender(); if (carbonPlayer instanceof ConsoleCarbonPlayer) { return; } if (carbonPlayer.muted()) { return; } final ImmutablePair pair = new ImmutablePair<>(carbonPlayer, chatChannel); Component messageComponent = awaitingEvent.getIfPresent(pair); awaitingEvent.invalidate(pair); if (messageComponent == null) { messageComponent = event.message(); } final String messageContents = PlainTextComponentSerializer.plainText().serialize(messageComponent); final Component eventMessage; if (carbonPlayer instanceof WrappedCarbonPlayer wrapped) { eventMessage = wrapped.parseMessageTags(messageContents); } else { eventMessage = TagPermissions.parseTags(carbonPlayer, TagPermissions.MESSAGE, messageContents, carbonPlayer::hasPermission); } DiscordSRV.debug(Debug.MINECRAFT_TO_DISCORD, "Received a CarbonChatEvent (player: " + carbonPlayer.username() + ")"); final @Nullable Player player = ((CarbonPlayerPaper) carbonPlayer).bukkitPlayer(); if (player != null) { DiscordSRV.getPlugin().processChatMessage(player, this.toDsrv(eventMessage), chatChannel.commandName(), event.cancelled(), null); } }); DiscordSRV.api.subscribe(new Object() { @Subscribe public void handle(final GameChatMessagePreProcessEvent event) { if (event.getTriggeringBukkitEvent() == null) { return; } event.setCancelled(true); } }); } @Override public void broadcastMessageToChannel(final String channel, final github.scarsz.discordsrv.dependencies.kyori.adventure.text.Component message) { final @Nullable ChatChannel chatChannel = this.channelRegistry.channelByValue(channel); if (chatChannel == null) { this.plugin.getLogger().warning("Error sending message from Discord to Minecraft, no matching channel found for [" + channel + "]"); } else { ChannelUtils.broadcastMessageToChannel(this.fromDsrv(message), chatChannel); } } private github.scarsz.discordsrv.dependencies.kyori.adventure.text.Component toDsrv(final Component component) { return github.scarsz.discordsrv.dependencies.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().deserialize( GsonComponentSerializer.gson().serialize(component) ); } private Component fromDsrv(final github.scarsz.discordsrv.dependencies.kyori.adventure.text.Component component) { return GsonComponentSerializer.gson().deserialize( github.scarsz.discordsrv.dependencies.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().serialize(component) ); } @Override public Plugin getPlugin() { return this.plugin; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/essxd/EssXDIntegration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.essxd; import com.google.inject.Inject; import com.google.inject.Injector; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import org.bukkit.Bukkit; import org.spongepowered.configurate.objectmapping.ConfigSerializable; public final class EssXDIntegration implements Integration { private final Injector injector; private final EssXDIntegration.Config config; @Inject private EssXDIntegration( final Injector injector, final ConfigManager configManager ) { this.injector = injector; this.config = this.config(configManager, configMeta()); } @Override public boolean eligible() { return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled("EssentialsDiscord"); } @Override public void register() { this.injector.getInstance(EssXDListener.class).register(); } public static ConfigMeta configMeta() { return Integration.configMeta("essentialsx_discord", EssXDIntegration.Config.class); } @ConfigSerializable public static final class Config { boolean enabled = true; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/essxd/EssXDListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.essxd; import com.google.inject.Inject; import java.util.HashMap; import java.util.Map; import net.draycia.carbon.api.CarbonChat; import net.essentialsx.api.v2.events.discord.DiscordMessageEvent; import net.essentialsx.api.v2.services.discord.DiscordService; import net.essentialsx.api.v2.services.discord.MessageType; import net.kyori.adventure.key.Key; import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.Nullable; public final class EssXDListener implements Listener { private final CarbonChat carbonChat; private final JavaPlugin plugin; private final Map channelMessageTypes = new HashMap<>(); private final Logger logger; @Inject private EssXDListener( final JavaPlugin plugin, final CarbonChat carbonChat, final Logger logger ) { this.plugin = plugin; this.carbonChat = carbonChat; this.logger = logger; } // Minecraft -> Discord @EventHandler public void onDiscordMessage(final DiscordMessageEvent event) { if (!event.getType().equals(MessageType.DefaultTypes.CHAT)) { return; } final var result = this.carbonChat.userManager().user(event.getUUID()).join(); var channel = result.selectedChannel(); if (channel == null) { channel = this.carbonChat.channelRegistry().defaultChannel(); } final var messageType = this.channelMessageTypes.get(channel.key()); event.setType(messageType); } public void register() { Bukkit.getPluginManager().registerEvents(this, this.plugin); final @Nullable DiscordService discord = Bukkit.getServicesManager().load(DiscordService.class); if (discord != null) { this.carbonChat.channelRegistry().allKeys(key -> { final MessageType channelMessageType = new MessageType(key.value()); try { discord.registerMessageType(this.plugin, channelMessageType); } catch (final IllegalArgumentException exception) { this.logger.info("Skipping registration of message type [{}]", channelMessageType); } this.channelMessageTypes.put(key, channelMessageType); }); } } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/AbstractFactionsChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.fuuid; import com.massivecraft.factions.FPlayer; import com.massivecraft.factions.FPlayers; import com.massivecraft.factions.Faction; import com.massivecraft.factions.perms.Relation; import com.massivecraft.factions.perms.Role; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.ConfigChatChannel; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.jspecify.annotations.NonNull; @DefaultQualifier(NonNull.class) abstract class AbstractFactionsChannel extends ConfigChatChannel { protected final @Nullable Faction faction(final CarbonPlayer player) { final @Nullable FPlayer fPlayer = this.factionPlayer(player); if (fPlayer == null || !fPlayer.hasFaction()) { return null; } return fPlayer.getFaction(); } protected final @Nullable FPlayer factionPlayer(final CarbonPlayer player) { return FPlayers.getInstance().getById(player.uuid().toString()); } protected final @Nullable Role factionRole(final CarbonPlayer player) { final @Nullable FPlayer fPlayer = this.factionPlayer(player); if (fPlayer == null || !fPlayer.hasFaction()) { return null; } return fPlayer.getRole(); } protected final boolean hasRelations(final CarbonPlayer player, final Relation relation) { final @Nullable Faction faction = this.faction(player); return faction != null && faction.getRelationCount(relation) > 0; } @Override public boolean shouldCrossServer() { return false; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/AllianceChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.fuuid; import com.google.inject.Inject; import com.massivecraft.factions.FPlayer; import com.massivecraft.factions.FPlayers; import com.massivecraft.factions.Faction; import com.massivecraft.factions.perms.Relation; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import org.bukkit.entity.Player; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @DefaultQualifier(NonNull.class) @ConfigSerializable public class AllianceChannel extends AbstractFactionsChannel { public static final String FILE_NAME = "factionsuuid-alliancechat.conf"; private transient @MonotonicNonNull @Inject UserManager users; public AllianceChannel() { this.key = Key.key("carbon", "alliancechat"); this.commandAliases = List.of("ac"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(alliance: %factionsuuid_faction_name%) : ", "console", "[alliance: %factionsuuid_faction_name%] : " ); } @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( this.hasRelations(player, Relation.ALLY), () -> this.messages.cannotUseFactionAllianceChannel(player) )); } @Override public List recipients(final CarbonPlayer sender) { if (!this.hasRelations(sender, Relation.ALLY)) { if (sender.online()) { sender.sendMessage(this.messages.cannotUseFactionAllianceChannel(sender)); } return Collections.emptyList(); } final List recipients = new ArrayList<>(); for (final Player player : this.alliedPlayersTo(sender)) { final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null); if (carbon != null) { recipients.add(carbon); } } recipients.add(this.server.console()); return recipients; } private List alliedPlayersTo(final CarbonPlayer player) { final @Nullable Faction faction = this.faction(player); if (faction == null) { return List.of(); } final List alliedPlayers = new ArrayList<>(); for (final FPlayer onlinePlayer : FPlayers.getInstance().getOnlinePlayers()) { final Relation relation = faction.getRelationTo(onlinePlayer); if (relation.isAtLeast(Relation.ALLY)) { alliedPlayers.add(onlinePlayer.getPlayer()); } } return alliedPlayers; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/FactionChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.fuuid; import com.google.inject.Inject; import com.massivecraft.factions.Faction; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import org.bukkit.entity.Player; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @DefaultQualifier(NonNull.class) @ConfigSerializable public class FactionChannel extends AbstractFactionsChannel { public static final String FILE_NAME = "factionsuuid-factionchat.conf"; private transient @MonotonicNonNull @Inject UserManager users; public FactionChannel() { this.key = Key.key("carbon", "factionchat"); this.commandAliases = List.of("fc"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(faction: %factionsuuid_faction_name%) : ", "console", "[faction: %factionsuuid_faction_name%] : " ); } @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( this.faction(player) != null, () -> this.messages.cannotUseFactionChannel(player) )); } @Override public List recipients(final CarbonPlayer sender) { final @Nullable Faction faction = this.faction(sender); if (faction == null) { if (sender.online()) { sender.sendMessage(this.messages.cannotUseFactionChannel(sender)); } return Collections.emptyList(); } final List recipients = new ArrayList<>(); for (final Player player : faction.getOnlinePlayers()) { final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null); if (carbon != null) { recipients.add(carbon); } } recipients.add(this.server.console()); return recipients; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/FactionModChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.fuuid; import com.google.inject.Inject; import com.massivecraft.factions.FPlayer; import com.massivecraft.factions.Faction; import com.massivecraft.factions.perms.Role; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import org.bukkit.entity.Player; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @DefaultQualifier(NonNull.class) @ConfigSerializable public class FactionModChannel extends AbstractFactionsChannel { public static final String FILE_NAME = "factionsuuid-factionmodchat.conf"; private transient @MonotonicNonNull @Inject UserManager users; // We could check if the player doesn't have the normal role, but this list may be configurable in the future? private transient final List validRoles = List.of(Role.ADMIN, Role.MODERATOR, Role.COLEADER); public FactionModChannel() { this.key = Key.key("carbon", "factionmodchat"); this.commandAliases = List.of("mc"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(fmod: %factionsuuid_faction_name%) : ", "console", "[fmod: %factionsuuid_faction_name%] : " ); } @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( this.validRoles.contains(this.factionRole(player)), () -> this.messages.cannotUseFactionModChannel(player) )); } @Override public List recipients(final CarbonPlayer sender) { if (!this.validRoles.contains(this.factionRole(sender))) { if (sender.online()) { sender.sendMessage(this.messages.cannotUseFactionModChannel(sender)); } return Collections.emptyList(); } final List recipients = new ArrayList<>(); for (final Player player : this.factionMods(sender)) { final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null); if (carbon != null) { recipients.add(carbon); } } recipients.add(this.server.console()); return recipients; } private List factionMods(final CarbonPlayer player) { final @Nullable Faction faction = this.faction(player); if (faction == null) { return List.of(); } final List factionMods = new ArrayList<>(); for (final FPlayer onlinePlayer : faction.getFPlayersWhereOnline(true)) { if (this.validRoles.contains(onlinePlayer.getRole())) { factionMods.add(onlinePlayer.getPlayer()); } } return factionMods; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/FactionsIntegration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.fuuid; import com.google.inject.Inject; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import org.bukkit.Bukkit; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @DefaultQualifier(NonNull.class) public final class FactionsIntegration implements Integration { private final CarbonChannelRegistry channelRegistry; private final Config config; @Inject public FactionsIntegration( final CarbonChannelRegistry channelRegistry, final ConfigManager configManager ) { this.channelRegistry = channelRegistry; this.config = this.config(configManager, configMeta()); } @Override public boolean eligible() { return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled("Factions"); } @Override public void register() { if (this.config.factionChannel) { this.channelRegistry.registerSpecialConfigChannel(FactionChannel.FILE_NAME, FactionChannel.class); } if (this.config.allianceChannel) { this.channelRegistry.registerSpecialConfigChannel(AllianceChannel.FILE_NAME, AllianceChannel.class); } if (this.config.truceChannel) { this.channelRegistry.registerSpecialConfigChannel(TruceChannel.FILE_NAME, TruceChannel.class); } if (this.config.factionModChannel) { this.channelRegistry.registerSpecialConfigChannel(FactionModChannel.FILE_NAME, FactionModChannel.class); } } public static ConfigMeta configMeta() { return Integration.configMeta("factionsuuid", FactionsIntegration.Config.class); } @ConfigSerializable public static final class Config { boolean enabled = true; boolean factionChannel = true; boolean allianceChannel = true; boolean truceChannel = true; boolean factionModChannel = false; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/TruceChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.fuuid; import com.google.inject.Inject; import com.massivecraft.factions.FPlayer; import com.massivecraft.factions.FPlayers; import com.massivecraft.factions.Faction; import com.massivecraft.factions.perms.Relation; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import org.bukkit.entity.Player; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @DefaultQualifier(NonNull.class) @ConfigSerializable public class TruceChannel extends AbstractFactionsChannel { public static final String FILE_NAME = "factionsuuid-trucechat.conf"; private transient @MonotonicNonNull @Inject CarbonMessages messages; private transient @MonotonicNonNull @Inject UserManager users; public TruceChannel() { this.key = Key.key("carbon", "trucechat"); this.commandAliases = List.of("tc"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(truce: %factionsuuid_faction_name%) : ", "console", "[truce: %factionsuuid_faction_name%] : " ); } @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( this.hasRelations(player, Relation.TRUCE), () -> this.messages.cannotUseTruceChannel(player) )); } @Override public List recipients(final CarbonPlayer sender) { if (!this.hasRelations(sender, Relation.TRUCE)) { if (sender.online()) { sender.sendMessage(this.messages.cannotUseTruceChannel(sender)); } return Collections.emptyList(); } final List recipients = new ArrayList<>(); for (final Player player : this.hasTruceWith(sender)) { final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null); if (carbon != null) { recipients.add(carbon); } } recipients.add(this.server.console()); return recipients; } private List hasTruceWith(final CarbonPlayer player) { final @Nullable Faction faction = this.faction(player); if (faction == null) { return List.of(); } final List alliedPlayers = new ArrayList<>(); for (final FPlayer onlinePlayer : FPlayers.getInstance().getOnlinePlayers()) { final Relation relation = faction.getRelationTo(onlinePlayer); if (relation.isAtLeast(Relation.TRUCE)) { alliedPlayers.add(onlinePlayer.getPlayer()); } } return alliedPlayers; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/mcmmo/McmmoIntegration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.mcmmo; import com.google.inject.Inject; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; @DefaultQualifier(NonNull.class) public final class McmmoIntegration implements Integration { private final CarbonChannelRegistry channelRegistry; private final ConfigManager configManager; private final Logger logger; private final Config config; @Inject public McmmoIntegration( final CarbonChannelRegistry channelRegistry, final ConfigManager configManager, final Logger logger ) { this.channelRegistry = channelRegistry; this.configManager = configManager; this.logger = logger; this.config = this.config(configManager, configMeta()); } @Override public boolean eligible() { return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled("mcMMO"); } @Override public void register() { if (this.config.partyChannel) { if (this.configManager.primaryConfig().partyChat().enabled) { this.logger.warn("Both CarbonChat parties and the mcMMO party chat channel are enabled!"); this.logger.warn("Usually, you want one or the other enabled. Additionally, their default channel configs will conflict."); } this.channelRegistry.registerSpecialConfigChannel(McmmoPartyChannel.FILE_NAME, McmmoPartyChannel.class); } } public static ConfigMeta configMeta() { return Integration.configMeta("mcmmo", McmmoIntegration.Config.class); } @ConfigSerializable public static final class Config { boolean enabled = true; @Comment("You will likely want to disable Carbon's built-in party system above when using mcMMO party chat.") boolean partyChannel = true; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/mcmmo/McmmoPartyChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.mcmmo; import com.gmail.nossr50.datatypes.party.Party; import com.google.inject.Inject; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.ConfigChatChannel; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import org.bukkit.Bukkit; import org.bukkit.entity.Player; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @DefaultQualifier(NonNull.class) @ConfigSerializable public class McmmoPartyChannel extends ConfigChatChannel { public static final String FILE_NAME = "mcmmo-party.conf"; private transient @MonotonicNonNull @Inject CarbonMessages messages; private transient @MonotonicNonNull @Inject UserManager users; public McmmoPartyChannel() { this.key = Key.key("carbon", "partychat"); this.commandAliases = List.of("pc"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(party: %mcmmo_party_name%) : ", "console", "[party: %mcmmo_party_name%] : " ); } @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( this.party(player) != null, () -> this.messages.cannotUseMcmmoPartyChannel(player) )); } @Override public List recipients(final CarbonPlayer sender) { final @Nullable Party party = this.party(sender); if (party == null) { if (sender.online()) { sender.sendMessage(this.messages.cannotUseMcmmoPartyChannel(sender)); } return Collections.emptyList(); } final List recipients = new ArrayList<>(); for (final Player player : party.getOnlineMembers()) { final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null); if (carbon != null) { recipients.add(carbon); } } recipients.add(this.server.console()); return recipients; } private @Nullable Party party(final CarbonPlayer player) { return com.gmail.nossr50.util.player.UserManager.getPlayer(Bukkit.getPlayer(player.uuid())).getParty(); } @Override public boolean shouldCrossServer() { return false; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/plotsquared/PlotChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.plotsquared; import com.google.inject.Inject; import com.plotsquared.core.PlotAPI; import com.plotsquared.core.player.PlotPlayer; import com.plotsquared.core.plot.Plot; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.ConfigChatChannel; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.draycia.carbon.common.config.ConfigHeader; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @DefaultQualifier(NonNull.class) @ConfigSerializable @ConfigHeader(PlotChannel.PLOT_CHANNEL_HEADER) public class PlotChannel extends ConfigChatChannel { protected static final String PLOT_CHANNEL_HEADER = """ See the PlotSquared Wiki at https://intellectualsites.gitbook.io/plotsquared/customization/placeholders for placeholders PlotSquared provides to PlaceholderAPI. """; protected final static PlotAPI PLOT_API = new PlotAPI(); public static final String FILE_NAME = "plotsquared-plotchat.conf"; protected transient @MonotonicNonNull @Inject UserManager users; public PlotChannel() { this.key = Key.key("carbon", "plotchat"); this.commandAliases = List.of("local"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", ": ", "console", ": " ); } protected @Nullable Plot plot(final CarbonPlayer carbonPlayer) { final @Nullable PlotPlayer plotPlayer = PLOT_API.wrapPlayer(carbonPlayer.uuid()); if (plotPlayer != null) { return plotPlayer.getCurrentPlot(); } return null; } @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( this.plot(player) != null, () -> this.cannotUseChannel(player) )); } @Override public List recipients(final CarbonPlayer sender) { final @Nullable Plot plot = this.plot(sender); if (plot == null) { if (sender.online()) { sender.sendMessage(this.cannotUseChannel(sender)); } return Collections.emptyList(); } final List recipients = new ArrayList<>(); for (final PlotPlayer plotPlayer : this.onlinePlayers(plot)) { final @Nullable CarbonPlayer carbon = this.users.user(plotPlayer.getUUID()).getNow(null); if (carbon != null) { recipients.add(carbon); } } recipients.add(this.server.console()); return recipients; } protected List> onlinePlayers(final Plot plot) { return plot.getPlayersInPlot(); } protected Component cannotUseChannel(final CarbonPlayer player) { return this.messages.cannotUsePlotChannel(player); } @Override public boolean shouldCrossServer() { return false; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/plotsquared/PlotSquaredIntegration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.plotsquared; import com.google.inject.Inject; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import org.bukkit.Bukkit; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @DefaultQualifier(NonNull.class) public final class PlotSquaredIntegration implements Integration { private final CarbonChannelRegistry channelRegistry; private final Config config; @Inject public PlotSquaredIntegration( final CarbonChannelRegistry channelRegistry, final ConfigManager configManager ) { this.channelRegistry = channelRegistry; this.config = this.config(configManager, configMeta()); } @Override public boolean eligible() { return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled("PlotSquared"); } @Override public void register() { if (this.config.plotChannel) { this.channelRegistry.registerSpecialConfigChannel(PlotChannel.FILE_NAME, PlotChannel.class); } } public static ConfigMeta configMeta() { return Integration.configMeta("plotsquared", PlotSquaredIntegration.Config.class); } @ConfigSerializable public static final class Config { boolean enabled = true; boolean plotChannel = true; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/towny/AllianceChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.towny; import com.palmergames.bukkit.towny.object.Nation; import java.util.List; import java.util.Map; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.draycia.carbon.common.config.ConfigHeader; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @DefaultQualifier(NonNull.class) @ConfigSerializable @ConfigHeader(ResidentListChannel.TOWNY_CHANNEL_HEADER) public class AllianceChannel extends NationChannel { public static final String FILE_NAME = "towny-alliancechat.conf"; public AllianceChannel() { this.key = Key.key("carbon", "alliancechat"); this.commandAliases = List.of(); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(alliance) : ", "console", "[alliance] : " ); } @Override protected List onlinePlayers(final Nation residentList) { return TOWNY_API.getOnlinePlayersAlliance(residentList); } @Override protected Component cannotUseChannel(final CarbonPlayer player) { return this.messages.cannotUseAllianceChannel(player); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/towny/NationChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.towny; import com.palmergames.bukkit.towny.object.Nation; import com.palmergames.bukkit.towny.object.Resident; import java.util.List; import java.util.Map; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.draycia.carbon.common.config.ConfigHeader; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @DefaultQualifier(NonNull.class) @ConfigSerializable @ConfigHeader(ResidentListChannel.TOWNY_CHANNEL_HEADER) public class NationChannel extends ResidentListChannel { public static final String FILE_NAME = "towny-nationchat.conf"; public NationChannel() { this.key = Key.key("carbon", "nationchat"); this.commandAliases = List.of("nc"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(nation: %townyadvanced_nation_unformatted%) : ", "console", "[nation: %townyadvanced_nation_unformatted%] : " ); } @Override protected @Nullable Nation residentList(final CarbonPlayer player) { final @Nullable Resident resident = TOWNY_API.getResident(player.uuid()); if (resident == null) { return null; } return TOWNY_API.getResidentNationOrNull(resident); } @Override protected Component cannotUseChannel(final CarbonPlayer player) { return this.messages.cannotUseNationChannel(player); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/towny/ResidentListChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.towny; import com.google.inject.Inject; import com.palmergames.bukkit.towny.TownyAPI; import com.palmergames.bukkit.towny.object.ResidentList; import java.util.ArrayList; import java.util.Collections; import java.util.List; import net.draycia.carbon.api.channels.ChannelPermissions; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.channels.ConfigChatChannel; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; 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; import static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult; @DefaultQualifier(NonNull.class) abstract class ResidentListChannel extends ConfigChatChannel { protected static final String TOWNY_CHANNEL_HEADER = """ See the Towny Wiki at https://github.com/TownyAdvanced/Towny/wiki/Placeholders for placeholders Towny provides to PlaceholderAPI. """; protected final static TownyAPI TOWNY_API = TownyAPI.getInstance(); protected transient @MonotonicNonNull @Inject UserManager users; protected abstract @Nullable T residentList(CarbonPlayer player); @Override public ChannelPermissions permissions() { return ChannelPermissions.uniformDynamic(player -> channelPermissionResult( this.residentList(player) != null, () -> this.cannotUseChannel(player) )); } @Override public List recipients(final CarbonPlayer sender) { final @Nullable T residentList = this.residentList(sender); if (residentList == null) { if (sender.online()) { sender.sendMessage(this.cannotUseChannel(sender)); } return Collections.emptyList(); } final List recipients = new ArrayList<>(); for (final Player player : this.onlinePlayers(residentList)) { final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null); if (carbon != null) { recipients.add(carbon); } } recipients.add(this.server.console()); return recipients; } protected List onlinePlayers(final T residentList) { return TOWNY_API.getOnlinePlayers(residentList); } protected abstract Component cannotUseChannel(CarbonPlayer player); @Override public boolean shouldCrossServer() { return false; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/towny/TownChannel.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.towny; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.Town; import java.util.List; import java.util.Map; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource; import net.draycia.carbon.common.config.ConfigHeader; 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; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @DefaultQualifier(NonNull.class) @ConfigSerializable @ConfigHeader(ResidentListChannel.TOWNY_CHANNEL_HEADER) public class TownChannel extends ResidentListChannel { public static final String FILE_NAME = "towny-townchat.conf"; public TownChannel() { this.key = Key.key("carbon", "townchat"); this.commandAliases = List.of("tc"); this.messageSource = new ConfigChannelMessageSource(); this.messageSource.defaults = Map.of( "default_format", "(town: %townyadvanced_town_unformatted%) : ", "console", "[town: %townyadvanced_town_unformatted%] : " ); } @Override protected @Nullable Town residentList(final CarbonPlayer player) { final @Nullable Resident resident = TOWNY_API.getResident(player.uuid()); if (resident == null) { return null; } return TOWNY_API.getResidentTownOrNull(resident); } @Override protected Component cannotUseChannel(final CarbonPlayer player) { return this.messages.cannotUseTownChannel(player); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/integration/towny/TownyIntegration.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.integration.towny; import com.google.inject.Inject; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.Integration; import org.bukkit.Bukkit; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @DefaultQualifier(NonNull.class) public final class TownyIntegration implements Integration { private final CarbonChannelRegistry channelRegistry; private final Config config; @Inject public TownyIntegration( final CarbonChannelRegistry channelRegistry, final ConfigManager configManager ) { this.channelRegistry = channelRegistry; this.config = this.config(configManager, configMeta()); } @Override public boolean eligible() { return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled("Towny"); } @Override public void register() { if (this.config.townChannel) { this.channelRegistry.registerSpecialConfigChannel(TownChannel.FILE_NAME, TownChannel.class); } if (this.config.nationChannel) { this.channelRegistry.registerSpecialConfigChannel(NationChannel.FILE_NAME, NationChannel.class); } if (this.config.allianceChannel) { this.channelRegistry.registerSpecialConfigChannel(AllianceChannel.FILE_NAME, AllianceChannel.class); } } public static ConfigMeta configMeta() { return Integration.configMeta("towny", TownyIntegration.Config.class); } @ConfigSerializable public static final class Config { boolean enabled = true; boolean townChannel = true; boolean nationChannel = true; boolean allianceChannel = false; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/listeners/PaperChatListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.listeners; import com.google.inject.Inject; import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent; import io.papermc.paper.event.player.AsyncChatDecorateEvent; import io.papermc.paper.event.player.AsyncChatEvent; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.event.events.CarbonChatEventImpl; import net.draycia.carbon.common.event.events.CarbonEarlyChatEvent; import net.draycia.carbon.common.listeners.ChatListenerInternal; import net.draycia.carbon.common.messages.CarbonMessages; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; 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 PaperChatListener extends ChatListenerInternal implements Listener { private final CarbonChat carbonChat; final ConfigManager configManager; @Inject public PaperChatListener( final CarbonChat carbonChat, final CarbonMessages carbonMessages, final ConfigManager configManager ) { super(carbonChat.eventHandler(), carbonMessages, configManager); this.carbonChat = carbonChat; this.configManager = configManager; } @SuppressWarnings("UnstableApiUsage") @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPaperChatDecorate(final @NonNull AsyncChatDecorateEvent event) { if (event.player() == null) { return; } final @Nullable CarbonPlayer sender = this.carbonChat.userManager().user(event.player().getUniqueId()).join(); final @Nullable CarbonEarlyChatEvent earlyChatEvent = this.prepareAndEmitPreChatEvent(sender, event.result()); if (earlyChatEvent == null || earlyChatEvent.cancelled()) { event.setCancelled(true); return; } final @Nullable Component message = this.parseTags(sender, earlyChatEvent.message()); if (message != null) { event.result(message); } } @SuppressWarnings("UnstableApiUsage") @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPaperCommandDecorate(final @NonNull AsyncChatCommandDecorateEvent event) { this.onPaperChatDecorate(event); } @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPaperChat(final @NonNull AsyncChatEvent event) { final @Nullable CarbonPlayer sender = this.carbonChat.userManager().user(event.getPlayer().getUniqueId()).join(); if (event.viewers().isEmpty()) { return; } final @Nullable CarbonChatEventImpl chatEvent = this.prepareAndEmitChatEvent(sender, event.message(), event.signedMessage()); if (chatEvent == null || chatEvent.cancelled()) { event.setCancelled(true); return; } try { event.viewers().clear(); event.viewers().addAll(chatEvent.recipients()); } catch (final UnsupportedOperationException exception) { exception.printStackTrace(); } event.renderer(($, $$, $$$, recipient) -> { final var recipientUUID = recipient.get(Identity.UUID); final Audience recipientViewer; if (recipientUUID.isPresent()) { recipientViewer = this.carbonChat.userManager().user(recipientUUID.get()).join(); } else { recipientViewer = recipient; } return chatEvent.renderFor(recipientViewer); }); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/listeners/PaperPlayerJoinListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.listeners; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.List; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.messaging.packets.PacketFactory; import net.draycia.carbon.common.users.ProfileCache; import net.draycia.carbon.common.users.UserManagerInternal; import org.apache.logging.log4j.Logger; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import static net.draycia.carbon.common.users.PlayerUtils.joinExceptionHandler; import static net.draycia.carbon.common.users.PlayerUtils.saveExceptionHandler; @DefaultQualifier(NonNull.class) public class PaperPlayerJoinListener implements Listener { private final ConfigManager configManager; private final Logger logger; private final ProfileCache profileCache; private final UserManagerInternal userManager; private final Provider messaging; private final PacketFactory packetFactory; @Inject public PaperPlayerJoinListener( final ConfigManager configManager, final Logger logger, final ProfileCache profileCache, final UserManagerInternal userManager, final Provider messaging, final PacketFactory packetFactory ) { this.configManager = configManager; this.logger = logger; this.profileCache = profileCache; this.userManager = userManager; this.messaging = messaging; this.packetFactory = packetFactory; } @EventHandler public void onLogin(final PlayerLoginEvent event) { this.profileCache.cache(event.getPlayer().getUniqueId(), event.getPlayer().getName()); } @EventHandler(priority = EventPriority.LOWEST) public void onJoinEarly(final PlayerJoinEvent event) { this.messaging.get().queuePacket(() -> this.packetFactory.addLocalPlayerPacket(event.getPlayer().getUniqueId(), event.getPlayer().getName())); } @EventHandler(priority = EventPriority.HIGH) public void onJoin(final PlayerJoinEvent event) { this.userManager.user(event.getPlayer().getUniqueId()).exceptionally(joinExceptionHandler(this.logger, event.getPlayer().getName(), event.getPlayer().getUniqueId())); final @Nullable List suggestions = this.configManager.primaryConfig().customChatSuggestions(); if (suggestions == null || suggestions.isEmpty()) { return; } event.getPlayer().addAdditionalChatCompletions(suggestions); } @EventHandler(priority = EventPriority.HIGH) public void onQuit(final PlayerQuitEvent event) { this.userManager.loggedOut(event.getPlayer().getUniqueId()) .exceptionally(saveExceptionHandler(this.logger, event.getPlayer().getName(), event.getPlayer().getUniqueId())); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/messages/PaperMessageRenderer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.messages; import com.google.common.base.Suppliers; import com.google.inject.Inject; import com.google.inject.Singleton; import io.github.miniplaceholders.api.MiniPlaceholders; import java.util.function.Supplier; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil; import net.draycia.carbon.common.messages.CarbonMessageRenderer; import net.draycia.carbon.common.messages.RenderForTagResolver; import net.draycia.carbon.common.messages.SourcedAudience; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.draycia.carbon.paper.CarbonChatPaper; 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.resolver.TagResolver; import org.bukkit.Bukkit; import org.bukkit.entity.Player; 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; import static java.util.Objects.requireNonNull; @DefaultQualifier(NonNull.class) @Singleton public class PaperMessageRenderer extends CarbonMessageRenderer { private final Supplier<@MonotonicNonNull PlaceholderAPIMiniMessageParser> placeholderApiProcessor = Suppliers.memoize(() -> { if (CarbonChatPaper.papiLoaded()) { return PlaceholderAPIMiniMessageParser.create(MiniMessage.miniMessage()); } return null; }); private final ConfigManager configManager; private final MiniMessage miniMessage; @Inject public PaperMessageRenderer(final ConfigManager configManager, final RenderForTagResolver.Factory renderForTagResolver) { super(renderForTagResolver); this.miniMessage = MiniMessage.miniMessage(); this.configManager = configManager; } @Override public Component render( final Audience receiver, final String intermediateMessage, final TagResolver.Builder tagResolver ) { final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage); if (MiniPlaceholdersUtil.miniPlaceholdersLoaded()) { tagResolver.resolver(MiniPlaceholders.globalPlaceholders()); } if (!(receiver instanceof SourcedAudience sourced)) { return this.miniMessage.deserialize(placeholderResolvedMessage, tagResolver.build()); } if (!(sourced.sender() instanceof CarbonPlayer sender && sender.online())) { return this.miniMessage.deserialize(placeholderResolvedMessage, tagResolver.build()); } // We can't/shouldn't resolve placeholders for non-players if (sender instanceof ConsoleCarbonPlayer) { return this.miniMessage.deserialize(placeholderResolvedMessage, tagResolver.build()); } final Player senderBukkitPlayer = requireNonNull(Bukkit.getPlayer(sender.uuid())); final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig = MiniPlaceholdersUtil.miniPlaceholdersLoaded() ? this.configManager.primaryConfig().integrations().config(MiniPlaceholdersIntegration.configMeta()) : null; if (miniplaceholdersConfig != null) { tagResolver.resolver(MiniPlaceholders.audiencePlaceholders()); } if (!(sourced.recipient() instanceof CarbonPlayer recipient && recipient.online())) { if (this.hasPlaceholderAPI()) { return this.placeholderApiProcessor.get().parse(senderBukkitPlayer, placeholderResolvedMessage, tagResolver.build(), miniplaceholdersConfig); } return this.miniMessage.deserialize(placeholderResolvedMessage, MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.recipient(), senderBukkitPlayer), tagResolver.build()); } final @Nullable Player recipientBukkitPlayer = Bukkit.getPlayer(recipient.uuid()); if (recipientBukkitPlayer == null) { if (this.hasPlaceholderAPI()) { return this.placeholderApiProcessor.get().parse(senderBukkitPlayer, placeholderResolvedMessage, tagResolver.build(), miniplaceholdersConfig); } return this.miniMessage.deserialize(placeholderResolvedMessage, MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.recipient(), senderBukkitPlayer), tagResolver.build()); } if (miniplaceholdersConfig != null && miniplaceholdersConfig.relationalPlaceholders) { tagResolver.resolver(MiniPlaceholders.relationalPlaceholders()); } if (this.hasPlaceholderAPI()) { return this.placeholderApiProcessor.get().parseRelational(recipientBukkitPlayer, senderBukkitPlayer, placeholderResolvedMessage, tagResolver.build(), miniplaceholdersConfig); } return this.miniMessage.deserialize(placeholderResolvedMessage, MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, recipientBukkitPlayer, senderBukkitPlayer), tagResolver.build()); } private boolean hasPlaceholderAPI() { return this.placeholderApiProcessor.get() != null; } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/messages/PlaceholderAPIMiniMessageParser.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.messages; import java.util.function.UnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; import me.clip.placeholderapi.PlaceholderAPI; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil; 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 net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.entity.Player; 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 PlaceholderAPIMiniMessageParser { private final MiniMessage miniMessage; private PlaceholderAPIMiniMessageParser(final MiniMessage miniMessage) { this.miniMessage = miniMessage; } public static PlaceholderAPIMiniMessageParser create(final MiniMessage backingInstance) { return new PlaceholderAPIMiniMessageParser(backingInstance); } private static boolean containsLegacyColorCodes(final String string) { final char[] charArray = string.toCharArray(); for (int i = 0; i < charArray.length - 1; i++) { if (charArray[i] == LegacyComponentSerializer.SECTION_CHAR && "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx".indexOf(charArray[i + 1]) > -1) { return true; } } return false; } public Component parse(final Player player, final String input, final TagResolver tagResolver, final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig) { return this.parse( null, player, PlaceholderAPI.getPlaceholderPattern(), match -> PlaceholderAPI.setPlaceholders(player, match), input, tagResolver, miniplaceholdersConfig ); } public Component parse(final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig, final Player player, final String input) { return this.parse(player, input, TagResolver.empty(), miniplaceholdersConfig); } public Component parseRelational(final Player recipient, final Player sender, final String input, final TagResolver tagResolver, final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig) { return this.parse( recipient, sender, PlaceholderAPI.getPlaceholderPattern(), match -> PlaceholderAPI.setPlaceholders(sender, PlaceholderAPI.setRelationalPlaceholders(recipient, sender, match)), input, tagResolver, miniplaceholdersConfig ); } public Component parseRelational(final Player recipient, final Player sender, final String input, final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig) { return this.parseRelational(recipient, sender, input, TagResolver.empty(), miniplaceholdersConfig); } private Component parse( final @Nullable Audience recipient, final Audience sender, final Pattern pattern, final UnaryOperator placeholderResolver, final String input, final TagResolver originalTags, final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig ) { final Matcher matcher = pattern.matcher(input); final TagResolver.Builder tagResolver = TagResolver.builder().resolvers(originalTags); final StringBuilder builder = new StringBuilder(); int id = 0; while (matcher.find()) { final String match = matcher.group(); final String replaced = placeholderResolver.apply(match); if (match.equals(replaced) || !containsLegacyColorCodes(replaced)) { matcher.appendReplacement(builder, Matcher.quoteReplacement(replaced)); } else { final String key = "papi_generated_template_" + id; id++; tagResolver.tag(key, Tag.inserting(LegacyComponentSerializer.legacySection().deserialize(replaced))); matcher.appendReplacement(builder, Matcher.quoteReplacement("<" + key + ">")); } } matcher.appendTail(builder); return this.miniMessage.deserialize(builder.toString(), MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, recipient, sender), tagResolver.build()); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/users/CarbonPlayerPaper.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.users; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import io.papermc.paper.datacomponent.DataComponentTypes; import java.util.Collection; import java.util.Locale; import java.util.Optional; import java.util.function.Consumer; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.InventorySlot; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.users.WrappedCarbonPlayer; import net.draycia.carbon.common.util.EmptyAudienceWithPointers; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemRarity; import org.bukkit.inventory.ItemStack; import org.bukkit.metadata.MetadataValue; 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 CarbonPlayerPaper extends WrappedCarbonPlayer implements ForwardingAudience.Single { @AssistedInject private CarbonPlayerPaper( final @Assisted CarbonPlayerCommon carbonPlayerCommon, final ConfigManager config ) { super(carbonPlayerCommon); if (config.primaryConfig().nickname().useCarbonNicknames()) { this.player().ifPresent(this.applyDisplayNameToBukkit(this.hasNickname() ? this.displayName() : null)); } } private Optional player() { return Optional.ofNullable(Bukkit.getPlayer(this.carbonPlayerCommon.uuid())); } @Override protected Optional platformDisplayName() { return this.player().map(Player::displayName); } @Override public @NotNull Audience audience() { return this.player().map(player -> (Audience) player).orElseGet(() -> EmptyAudienceWithPointers.forCarbonPlayer(this)); } @Override public double distanceSquaredFrom(final CarbonPlayer other) { if (this.player().isEmpty()) { return -1; } final @Nullable Player player = this.player().orElse(null); final @Nullable Player otherPlayer = Bukkit.getPlayer(other.uuid()); if (player == null || otherPlayer == null) { return -1; } return player.getLocation().distanceSquared(otherPlayer.getLocation()); } @Override public boolean sameWorldAs(final CarbonPlayer other) { if (this.player().isEmpty()) { return false; } final Optional player = this.player(); final @Nullable Player otherPlayer = Bukkit.getPlayer(other.uuid()); if (player.isEmpty() || otherPlayer == null) { return false; } return player.get().getWorld().equals(otherPlayer.getWorld()); } @Override public void nickname(final @Nullable Component nickname) { super.nickname(nickname); this.player().ifPresent(this.applyDisplayNameToBukkit(nickname == null ? null : this.displayName())); } private Consumer applyDisplayNameToBukkit(final @Nullable Component displayName) { return bukkit -> this.carbonPlayerCommon.schedule(() -> { bukkit.displayName(displayName); if (this.carbonPlayerCommon.configManager().primaryConfig().nickname().updateTabList()) { bukkit.playerListName(displayName); } }); } @Override public @Nullable Component createItemHoverComponent(final InventorySlot slot) { final Optional player = this.player(); // This is temporary (it's not) if (player.isEmpty()) { return null; } final EquipmentSlot equipmentSlot; if (slot.equals(InventorySlot.MAIN_HAND)) { equipmentSlot = EquipmentSlot.HAND; } else if (slot.equals(InventorySlot.OFF_HAND)) { equipmentSlot = EquipmentSlot.OFF_HAND; } else if (slot.equals(InventorySlot.HELMET)) { equipmentSlot = EquipmentSlot.HEAD; } else if (slot.equals(InventorySlot.CHEST)) { equipmentSlot = EquipmentSlot.CHEST; } else if (slot.equals(InventorySlot.LEGS)) { equipmentSlot = EquipmentSlot.LEGS; } else if (slot.equals(InventorySlot.BOOTS)) { equipmentSlot = EquipmentSlot.FEET; } else { return null; } final @Nullable EntityEquipment equipment = player.get().getEquipment(); if (equipment == null) { return null; } final @Nullable ItemStack itemStack = equipment.getItem(equipmentSlot); if (itemStack == null || itemStack.getType().isAir()) { return null; } final int amount = Math.min(itemStack.getAmount(), 99); final Component quantity = amount <= 1 ? Component.empty() : Component.text(" x" + amount); return Component.empty().append( Component.text("["), itemStack.effectiveName(), quantity, Component.text("]") ) .hoverEvent(itemStack) .colorIfAbsent(itemStack.getDataOrDefault(DataComponentTypes.RARITY, ItemRarity.COMMON).color()); } @Override public @Nullable Locale locale() { return this.player().map(Player::locale).orElse(null); } @Override public void sendMessageAsPlayer(final String message) { // TODO: ensure method is not executed from main thread // bukkit doesn't like that this.player().ifPresent(player -> player.chat(message)); } @Override public boolean online() { return this.player().isPresent(); } @Override public boolean vanished() { return this.hasVanishMeta(); } // Supported by PremiumVanish, SuperVanish, VanishNoPacket private boolean hasVanishMeta() { return this.player().stream() .map(player -> player.getMetadata("vanished")) .flatMap(Collection::stream) .filter(value -> value.value() instanceof Boolean) .anyMatch(MetadataValue::asBoolean); } public @Nullable Player bukkitPlayer() { return Bukkit.getPlayer(this.uuid()); } } ================================================ FILE: paper/src/main/java/net/draycia/carbon/paper/users/PaperProfileResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.paper.users; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.UUID; import java.util.concurrent.CompletableFuture; import net.draycia.carbon.common.users.MojangProfileResolver; import net.draycia.carbon.common.users.ProfileResolver; import org.bukkit.Server; import org.bukkit.entity.Player; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @Singleton @DefaultQualifier(NonNull.class) public final class PaperProfileResolver implements ProfileResolver { private final Server server; private final ProfileResolver mojang; @Inject private PaperProfileResolver(final Server server, final MojangProfileResolver mojang) { this.server = server; this.mojang = mojang; } @Override public CompletableFuture<@Nullable UUID> resolveUUID(final String username, final boolean cacheOnly) { final @Nullable Player exact = this.server.getPlayerExact(username); if (exact != null) { return CompletableFuture.completedFuture(exact.getUniqueId()); } final @Nullable Player online = this.server.getPlayer(username); if (online != null) { return CompletableFuture.completedFuture(online.getUniqueId()); } return this.mojang.resolveUUID(username, cacheOnly); } @Override public CompletableFuture<@Nullable String> resolveName(final UUID uuid, final boolean cacheOnly) { final @Nullable Player online = this.server.getPlayer(uuid); if (online != null) { return CompletableFuture.completedFuture(online.getName()); } return this.mojang.resolveName(uuid, cacheOnly); } @Override public void shutdown() { this.mojang.shutdown(); } } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ], "ignoreDeps": [ "quiet-fabric-loom", "com.mojang:minecraft", "com.google.code.gson:gson", "com.google.guava:guava", "io.netty:netty-all", "org.apache.logging.log4j:log4j-bom" ], "labels": [ "dependencies" ], "packageRules": [ { "description": "Correct Fabric API version handling", "matchPackageNames": [ "net.fabricmc.fabric-api:fabric-api", "net.fabricmc.fabric-api:fabric-api-deprecated" ], "versioning": "regex:^(?\\d+)(\\.(?\\d+))?(\\.(?\\d+))?(?:\\+(?.*))?$" }, { "description": "Correct FactionsUUID version handling", "matchPackageNames": [ "com.massivecraft:Factions" ], "versioning": "regex:^1\\.6\\.9\\.5\\-U(?\\d+)(\\.(?\\d+))?(\\.(?\\d+))?$" }, { "description": "Towny version handling", "matchPackageNames": [ "com.palmergames.bukkit.towny:towny" ], "versioning": "regex:^0\\.(?\\d+)(\\.(?\\d+))?(\\.(?\\d+))?$" }, { "description": "Ignore Towny patch updates", "matchPackageNames": [ "com.palmergames.bukkit.towny:towny" ], "matchUpdateTypes": "patch", "enabled": false }, { "matchManagers": [ "github-actions", "gradle-wrapper" ], "groupName": "gradle and github actions" }, { "matchDepTypes": [ "plugin" ], "groupName": "gradle and github actions" }, { "matchFileNames": [ "build-logic/*", "buildSrc/*" ], "groupName": "gradle and github actions" } ], "schedule": [ "before 4am on Monday" ], "semanticCommitType": "build", "commitMessagePrefix": "chore(deps): " } ================================================ FILE: settings.gradle.kts ================================================ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { repositories { mavenCentral { mavenContent { releasesOnly() } } maven("https://repo.jpenilla.xyz/snapshots/") { mavenContent { snapshotsOnly() includeModuleByRegex("de\\.hexaoxi", "messenger-.*") includeModule("org.incendo", "cloud-sponge") includeModule("com.seiama", "registry") } } maven("https://central.sonatype.com/repository/maven-snapshots/") { mavenContent { snapshotsOnly() } } // PaperMC maven("https://repo.papermc.io/repository/maven-public/") // Sponge API maven("https://repo.spongepowered.org/repository/maven-public/") // PlaceholderAPI maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") { content { includeGroup("me.clip") } } // EssentialsDiscord maven("https://repo.essentialsx.net/releases/") { mavenContent { releasesOnly() includeGroup("net.essentialsx") } } maven("https://repo.essentialsx.net/snapshots/") { mavenContent { snapshotsOnly() includeGroup("net.essentialsx") } } // DiscordSRV maven("https://nexus.scarsz.me/content/groups/public/") { mavenContent { includeGroup("com.discordsrv") } } // Glare's repo for Towny maven("https://repo.glaremasters.me/repository/towny/") { content { includeGroup("com.palmergames.bukkit.towny") } } // FactionsUUID maven("https://ci.ender.zone/plugin/repository/everything/") { content { includeGroup("com.massivecraft") } } // mcMMO maven("https://nexus.neetgames.com/repository/maven-releases/") { content { includeGroup("com.gmail.nossr50.mcMMO") } } // Parties maven("https://repo.alessiodp.com/releases/") { content { includeGroup("com.alessiodp.parties") } } } repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) } pluginManagement { repositories { gradlePluginPortal() maven("https://central.sonatype.com/repository/maven-snapshots/") { mavenContent { snapshotsOnly() } } maven("https://maven.fabricmc.net/") maven("https://repo.jpenilla.xyz/snapshots/") } includeBuild("build-logic") } plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" id("quiet-fabric-loom") version "1.16-SNAPSHOT" } rootProject.name = "CarbonChat" listOf( "api", "common", "paper", // "sponge", // TODO API 10 "fabric", "velocity" ).forEach { include("carbonchat-$it") project(":carbonchat-$it").projectDir = file(it) } ================================================ FILE: sponge/build.gradle.kts ================================================ import org.spongepowered.gradle.plugin.config.PluginLoaders import org.spongepowered.plugin.metadata.model.PluginDependency import java.util.* plugins { id("carbon.shadow-platform") id("org.spongepowered.gradle.plugin") } dependencies { implementation(projects.carbonchatCommon) implementation(libs.cloudSponge) //implementation(libs.bstatsSponge) // not updated for api 8 yet } tasks { shadowJar { dependencies { // included in sponge exclude(dependency("io.leangen.geantyref:geantyref")) exclude(dependency("com.google.inject:guice")) exclude(dependency("aopalliance:aopalliance")) exclude(dependency("javax.inject:javax.inject")) } } } sponge { injectRepositories(false) // We specify repositories in settings.gradle.kts apiVersion("10.0.0-SNAPSHOT") plugin(rootProject.name.toLowerCase(Locale.ROOT)) { loader { name(PluginLoaders.JAVA_PLAIN) version("1.0") } displayName(rootProject.name) entrypoint("net.draycia.carbon.sponge.CarbonChatSponge") description(project.description) license("GPLv3") links { homepage(GITHUB_REPO_URL) source(GITHUB_REPO_URL) issues("$GITHUB_REPO_URL/issues") } contributor("Vicarious") { description("Lead Developer") } contributor("Glare") { description("Moral Support") } dependency("spongeapi") { loadOrder(PluginDependency.LoadOrder.AFTER) optional(false) } dependency("luckperms") { version(">=5.0.0") optional(true) } } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/CarbonChatSponge.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.TypeLiteral; import java.nio.file.Path; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.CarbonChatProvider; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.events.CarbonEventHandler; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.api.util.Component; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.util.CloudUtils; import net.draycia.carbon.common.util.ListenerUtils; import net.draycia.carbon.common.util.PlayerUtils; import net.draycia.carbon.sponge.listeners.SpongeChatListener; import net.draycia.carbon.sponge.listeners.SpongePlayerJoinListener; import net.draycia.carbon.sponge.listeners.SpongeReloadListener; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.moonshine.message.IMessageRenderer; import ninja.egg82.messenger.services.PacketService; 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; import org.spongepowered.api.Game; import org.spongepowered.api.Server; import org.spongepowered.api.Sponge; import org.spongepowered.api.config.ConfigDir; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.lifecycle.StartingEngineEvent; import org.spongepowered.api.event.lifecycle.StoppingEngineEvent; import org.spongepowered.api.scheduler.Task; import org.spongepowered.api.service.permission.PermissionDescription; import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.plugin.PluginContainer; import org.spongepowered.plugin.builtin.jvm.Plugin; @Plugin("carbonchat") @DefaultQualifier(NonNull.class) public final class CarbonChatSponge implements CarbonChat { private static final Set> LISTENER_CLASSES = Set.of(SpongeChatListener.class, SpongePlayerJoinListener.class, SpongeReloadListener.class); private static final int BSTATS_PLUGIN_ID = 11279; private final CarbonMessages carbonMessages; private final CarbonServerSponge carbonServerSponge; private final ChannelRegistry channelRegistry; private final Injector injector; private final Logger logger; private final Path dataDirectory; private final PluginContainer pluginContainer; private final UserManager userManager; private final CarbonEventHandler eventHandler = new CarbonEventHandler(); private final UUID serverId = UUID.randomUUID(); private @MonotonicNonNull MessagingManager messagingManager = null; @Inject public CarbonChatSponge( //final Metrics.Factory metricsFactory, final Game game, final PluginContainer pluginContainer, final Injector injector, final Logger logger, @ConfigDir(sharedRoot = false) final Path dataDirectory ) { CarbonChatProvider.register(this); this.pluginContainer = pluginContainer; this.injector = injector.createChildInjector(new CarbonChatSpongeModule(this, dataDirectory, pluginContainer)); this.logger = logger; this.carbonMessages = this.injector.getInstance(CarbonMessages.class); this.channelRegistry = this.injector.getInstance(ChannelRegistry.class); this.carbonServerSponge = this.injector.getInstance(CarbonServerSponge.class); this.userManager = this.injector.getInstance(com.google.inject.Key.get(new TypeLiteral>() {})); this.dataDirectory = dataDirectory; for (final Class clazz : LISTENER_CLASSES) { game.eventManager().registerListeners(this.pluginContainer, this.injector.getInstance(clazz)); } //metricsFactory.make(BSTATS_PLUGIN_ID); // Listeners ListenerUtils.registerCommonListeners(this.injector); // Load channels ((CarbonChannelRegistry) this.channelRegistry()).loadConfigChannels(this.carbonMessages); // TODO: Register these in a central location, pull from that in this and plugin.yml Sponge.serviceProvider().provide(PermissionService.class).ifPresent(permissionService -> { final PermissionDescription.Builder builder = permissionService.newDescriptionBuilder(this.pluginContainer); builder.id("carbon.clearchat.clear") .description(Component.text("Clears the chat for all players except those with carbon.chearchat.exempt.")) .register(); builder.id("carbon.clearchat.exempt") .description(Component.text("Exempts the player from having their chat cleared when /clearchat is executed.")) .register(); builder.id("carbon.debug") .description(Component.text("Allows the sender to quickly check what carbon think's the player's primary and non-primary groups are.")) .register(); builder.id("carbon.help") .description(Component.text("Shows Carbon's help menu, detailing each part of Carbon's commands.")) .register(); builder.id("carbon.hideidentity") .description(Component.text("Prevents messages from the player from being blocked clientside.")) .register(); builder.id("carbon.ignore") .description(Component.text("Ignores the player, hiding messages they send in chat and in whispers.")) .register(); builder.id("carbon.ignore.exempt") .description(Component.text("Prevents the player from being ignored.")) .register(); builder.id("carbon.ignore.unignore") .description(Component.text("Removes the player from the sender's ignore list.")) .register(); builder.id("carbon.itemlink") .description(Component.text("Shows the player's held or equipped item in chat.")) .register(); builder.id("carbon.mute") .description(Component.text("Mutes the player, preventing them from sending messages or whispers.")) .register(); builder.id("carbon.mute.exempt") .description(Component.text("Prevents the player from being muted.")) .register(); builder.id("carbon.mute.info") .description(Component.text("Shows if the player is muted or now.")) .register(); builder.id("carbon.mute.notify") .description(Component.text("Notifies the player when someone else has been mute.")) .register(); builder.id("carbon.mute.unmute") .description(Component.text("Unmutes the player, allowing them to use chat and send whispers.")) .register(); builder.id("carbon.nickname") .description(Component.text("The nickname command, by default shows your nickname.")) .register(); builder.id("carbon.nickname.others") .description(Component.text("Checks/sets other player's nicknames.")) .register(); builder.id("carbon.nickname.see") .description(Component.text("Checks your/other player's nicknames.")) .register(); builder.id("carbon.nickname.self") .description(Component.text("Checks/sets your nickname.")) .register(); builder.id("carbon.nickname.set") .description(Component.text("Sets your/other player's nicknames.")) .register(); builder.id("carbon.reload") .description(Component.text("Reloads Carbon's config, channel settings, and translations.")) .register(); builder.id("carbon.whisper") .description(Component.text("Sends private messages to other players.")) .register(); builder.id("carbon.whisper.continue") .description(Component.text("Sends a message to the last player you whispered.")) .register(); builder.id("carbon.whisper.reply") .description(Component.text("Sends a message to the last player who messaged you.")) .register(); builder.id("carbon.whisper.vanished") .description(Component.text("Allows the player to send messages to vanished players.")) .register(); }); // Commands CloudUtils.loadCommands(this.injector); final var commandSettings = CloudUtils.loadCommandSettings(this.injector); CloudUtils.registerCommands(commandSettings); } @Override public UUID serverId() { return this.serverId; } @Override public @Nullable PacketService packetService() { if (this.messagingManager == null) { this.messagingManager = this.injector.getInstance(MessagingManager.class); } return this.messagingManager.packetService(); } @Listener public void onInitialize(final StartingEngineEvent event) { // Player data saving Sponge.asyncScheduler().submit(Task.builder() .interval(5, TimeUnit.MINUTES) .plugin(this.pluginContainer) .execute(() -> PlayerUtils.saveLoggedInPlayers(this.carbonServerSponge, this.userManager)) .build()); } @Listener public void onDisable(final StoppingEngineEvent event) { PlayerUtils.saveLoggedInPlayers(this.carbonServerSponge, this.userManager).forEach(CompletableFuture::join); } @Override public Logger logger() { return this.logger; } @Override public Path dataDirectory() { return this.dataDirectory; } @Override public CarbonServerSponge server() { return this.carbonServerSponge; } @Override public ChannelRegistry channelRegistry() { return this.channelRegistry; } @Override public IMessageRenderer messageRenderer() { return this.injector.getInstance(SpongeMessageRenderer.class); } public CarbonMessages carbonMessages() { return this.carbonMessages; } @Override public @NonNull CarbonEventHandler eventHandler() { return this.eventHandler; } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/CarbonChatSpongeModule.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge; import org.incendo.cloud.CommandManager; import org.incendo.cloud.execution.AsynchronousCommandExecutionCoordinator; import org.incendo.cloud.sponge.SpongeCommandManager; import org.incendo.cloud.sponge.argument.SinglePlayerSelectorArgument; import com.google.inject.AbstractModule; import com.google.inject.Injector; import com.google.inject.Provides; import com.google.inject.Singleton; import java.nio.file.Path; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.util.Component; import net.draycia.carbon.api.util.SourcedAudience; import net.draycia.carbon.common.CarbonCommonModule; import net.draycia.carbon.common.ForCarbon; import net.draycia.carbon.common.command.Commander; import net.draycia.carbon.common.command.argument.PlayerSuggestions; import net.draycia.carbon.common.util.CloudUtils; import net.draycia.carbon.sponge.command.SpongeCommander; import net.draycia.carbon.sponge.command.SpongePlayerCommander; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.moonshine.message.IMessageRenderer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.plugin.PluginContainer; @DefaultQualifier(NonNull.class) public final class CarbonChatSpongeModule extends AbstractModule { private final CarbonChatSponge carbonChat; private final Path configDir; private final PluginContainer pluginContainer; public CarbonChatSpongeModule( final CarbonChatSponge carbonChat, final Path configDir, final PluginContainer pluginContainer ) { this.carbonChat = carbonChat; this.configDir = configDir; this.pluginContainer = pluginContainer; } @Provides @Singleton public CommandManager commandManager() { final SpongeCommandManager commandManager = new SpongeCommandManager<>( this.pluginContainer, AsynchronousCommandExecutionCoordinator.builder().build(), commander -> ((SpongeCommander) commander).commandCause(), commandCause -> { if (commandCause.subject() instanceof ServerPlayer player) { return new SpongePlayerCommander(this.carbonChat, player, commandCause); } return SpongeCommander.from(commandCause); } ); CloudUtils.decorateCommandManager(commandManager, this.carbonChat.carbonMessages()); commandManager.parserMapper().cloudNumberSuggestions(true); return commandManager; } @Provides @Singleton public IMessageRenderer messageRenderer(final Injector injector) { return injector.getInstance(SpongeMessageRenderer.class); } @Provides @Singleton public IMessageRenderer sourcedRenderer(final Injector injector) { return injector.getInstance(SpongeMessageRenderer.class); } @Override public void configure() { this.install(new CarbonCommonModule()); this.bind(Path.class).annotatedWith(ForCarbon.class).toInstance(this.configDir); this.bind(CarbonChat.class).toInstance(this.carbonChat); this.bind(CarbonServer.class).to(CarbonServerSponge.class); this.bind(PlayerSuggestions.class).toInstance(new SinglePlayerSelectorArgument.Parser()::suggestions); } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/CarbonServerSponge.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.ComponentPlayerResult; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.sponge.users.CarbonPlayerSponge; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; 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.api.Game; import org.spongepowered.api.Sponge; import org.spongepowered.api.profile.ProfileNotFoundException; @Singleton @DefaultQualifier(NonNull.class) public final class CarbonServerSponge implements CarbonServer, ForwardingAudience.Single { private final Game game; private final UserManager userManager; @Inject private CarbonServerSponge(final UserManager userManager, final Game game) { this.game = game; this.userManager = new SpongeUserManager(userManager); } @Override public @NotNull Audience audience() { return this.game.server(); } @Override public Audience console() { return this.game.systemSubject(); } @Override public List players() { final var players = new ArrayList(); for (final var player : Sponge.server().onlinePlayers()) { final ComponentPlayerResult result = this.userManager.carbonPlayer(player.uniqueId()).join(); if (result.player() != null) { players.add(result.player()); } } return players; } @Override public UserManager userManager() { return this.userManager; } @Override public CompletableFuture<@Nullable UUID> resolveUUID(final String username) { return CompletableFuture.supplyAsync(() -> { try { return Sponge.server().gameProfileManager().basicProfile(username).join().uuid(); } catch (final ProfileNotFoundException exception) { return null; } }); } @Override public CompletableFuture<@Nullable String> resolveName(final UUID uuid) { return CompletableFuture.supplyAsync(() -> { try { return Sponge.server().gameProfileManager().basicProfile(uuid).join().name().orElse(null); } catch (final ProfileNotFoundException exception) { return null; } }); } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/SpongeMessageRenderer.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge; import com.google.inject.Inject; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Map; import net.draycia.carbon.common.config.ConfigFactory; import net.draycia.carbon.common.util.ChatType; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.MessageType; 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.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.moonshine.message.IMessageRenderer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class SpongeMessageRenderer implements IMessageRenderer { private final ConfigFactory configFactory; @Inject public SpongeMessageRenderer(final ConfigFactory configFactory) { this.configFactory = configFactory; } @Override public Component render( final T receiver, final String intermediateMessage, 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(), Tag.inserting(entry.getValue())); } this.configFactory.primaryConfig().customPlaceholders().forEach( (key, value) -> tagResolver.resolver(Placeholder.unparsed(key, value)) ); return MiniMessage.miniMessage().deserialize(intermediateMessage, tagResolver.build());; } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/SpongeUserManager.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge; import java.util.UUID; import java.util.concurrent.CompletableFuture; import net.draycia.carbon.api.users.ComponentPlayerResult; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.users.SaveOnChange; import net.draycia.carbon.sponge.users.CarbonPlayerSponge; 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 class SpongeUserManager implements UserManager, SaveOnChange { protected final UserManager proxiedUserManager; public SpongeUserManager(final UserManager proxiedUserManager) { this.proxiedUserManager = proxiedUserManager; } @Override public CompletableFuture> carbonPlayer(final UUID uuid) { return this.proxiedUserManager.carbonPlayer(uuid).thenApply(result -> { if (result.player() == null) { return new ComponentPlayerResult<>(null, result.reason()); } return new ComponentPlayerResult<>(new CarbonPlayerSponge(result.player()), result.reason()); }); } @Override public CompletableFuture> savePlayer(final CarbonPlayerSponge player) { return this.proxiedUserManager.savePlayer(player.carbonPlayerCommon()).thenApply(result -> { if (result.player() == null) { return new ComponentPlayerResult<>(null, result.reason()); } return new ComponentPlayerResult<>(new CarbonPlayerSponge(result.player()), result.reason()); }); } @Override public CompletableFuture> saveAndInvalidatePlayer(final CarbonPlayerSponge player) { return this.proxiedUserManager.saveAndInvalidatePlayer(player.carbonPlayerCommon()).thenApply(result -> { if (result.player() == null) { return new ComponentPlayerResult<>(null, result.reason()); } return new ComponentPlayerResult<>(new CarbonPlayerSponge(result.player()), result.reason()); }); } @Override public int saveDisplayName(final UUID id, final @Nullable Component component) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.saveDisplayName(id, component); } return -1; } @Override public int saveMuted(final UUID id, final boolean muted) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.saveMuted(id, muted); } return -1; } @Override public int saveDeafened(final UUID id, final boolean deafened) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.saveDeafened(id, deafened); } return -1; } @Override public int saveSpying(final UUID id, final boolean spying) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.saveSpying(id, spying); } return -1; } @Override public int saveSelectedChannel(final UUID id, final @Nullable Key selectedChannel) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.saveSelectedChannel(id, selectedChannel); } return -1; } @Override public int saveLastWhisperTarget(final UUID id, final @Nullable UUID lastWhisperTarget) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.saveLastWhisperTarget(id, lastWhisperTarget); } return -1; } @Override public int saveWhisperReplyTarget(final UUID id, final @Nullable UUID whisperReplyTarget) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.saveWhisperReplyTarget(id, whisperReplyTarget); } return -1; } @Override public int addIgnore(final UUID id, final UUID ignoredPlayer) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.addIgnore(id, ignoredPlayer); } return -1; } @Override public int removeIgnore(final UUID id, final UUID ignoredPlayer) { if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) { return saveOnChange.removeIgnore(id, ignoredPlayer); } return -1; } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/command/SpongeCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge.command; import net.draycia.carbon.common.command.Commander; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.jetbrains.annotations.NotNull; import org.spongepowered.api.command.CommandCause; @DefaultQualifier(NonNull.class) public interface SpongeCommander extends Commander, ForwardingAudience.Single { static SpongeCommander from(final CommandCause commandCause) { return new SpongeCommanderImpl(commandCause); } @NonNull CommandCause commandCause(); record SpongeCommanderImpl(CommandCause commandCause) implements SpongeCommander { @Override public @NotNull Audience audience() { return this.commandCause.audience(); } } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/command/SpongePlayerCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge.command; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.common.command.PlayerCommander; import net.kyori.adventure.audience.Audience; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.jetbrains.annotations.NotNull; import org.spongepowered.api.command.CommandCause; import org.spongepowered.api.entity.living.player.server.ServerPlayer; import static java.util.Objects.requireNonNull; @DefaultQualifier(NonNull.class) public record SpongePlayerCommander( CarbonChat carbon, ServerPlayer player, CommandCause commandCause ) implements PlayerCommander, SpongeCommander { @Override public CarbonPlayer carbonPlayer() { return requireNonNull(this.carbon.server().userManager().carbonPlayer(this.player.uniqueId()).join().player(), "No CarbonPlayer for logged in Player!"); } @Override public @NotNull Audience audience() { return this.commandCause.audience(); } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/listeners/SpongeChatListener.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge.listeners; import com.google.inject.Inject; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.channels.ChannelRegistry; import net.draycia.carbon.api.events.CarbonChatEvent; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.ComponentPlayerResult; import net.draycia.carbon.api.util.KeyedRenderer; import net.draycia.carbon.api.util.Component; import net.draycia.carbon.sponge.CarbonChatSponge; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.filter.IsCancelled; import org.spongepowered.api.event.filter.cause.First; import org.spongepowered.api.event.message.PlayerChatEvent; import org.spongepowered.api.util.Tristate; import static java.util.Objects.requireNonNullElse; import static net.draycia.carbon.api.util.KeyedRenderer.keyedRenderer; import static net.kyori.adventure.key.Key.key; import static net.kyori.adventure.text.Component.empty; import static net.kyori.adventure.text.Component.text; @DefaultQualifier(NonNull.class) public final class SpongeChatListener { private final CarbonChatSponge carbonChat; private final ChannelRegistry registry; private final CarbonMessages carbonMessages; private static final Pattern DEFAULT_URL_PATTERN = Pattern.compile("(?:(https?)://)?([-\\w_.]+\\.\\w{2,})(/\\S*)?"); @Inject private SpongeChatListener( final CarbonChat carbonChat, final ChannelRegistry registry, final CarbonMessages carbonMessages ) { this.carbonChat = (CarbonChatSponge) carbonChat; this.registry = registry; this.carbonMessages = carbonMessages; } @Listener @IsCancelled(Tristate.FALSE) public void onPlayerChat(final PlayerChatEvent event, final @First Player source) { final var playerResult = this.carbonChat.server().userManager().carbonPlayer(source.uniqueId()).join(); final @Nullable CarbonPlayer sender = playerResult.player(); if (sender == null) { return; } var channel = requireNonNullElse(sender.selectedChannel(), this.registry.defaultValue()); final var messageContents = PlainTextComponentSerializer.plainText().serialize(event.originalMessage()); var eventMessage = event.message(); final CarbonPlayer.ChannelMessage channelMessage = sender.channelForMessage(eventMessage); if (channelMessage.channel() != null) { channel = channelMessage.channel(); } eventMessage = channelMessage.message(); if (sender.leftChannels().contains(channel.key())) { sender.joinChannel(channel); this.carbonMessages.channelJoined(sender); } for (final var chatChannel : this.registry) { if (chatChannel.quickPrefix() == null) { continue; } if (messageContents.startsWith(chatChannel.quickPrefix()) && chatChannel.speechPermitted(sender).permitted()) { channel = chatChannel; eventMessage = eventMessage.replaceText(TextReplacementConfig.builder() .once() .matchLiteral(channel.quickPrefix()) .replacement(text()) .build()); break; } } final List recipients; if (event.audience().isPresent()) { final var audience = event.audience().get(); if (audience instanceof ForwardingAudience forwardingAudience) { recipients = new ArrayList<>(); forwardingAudience.forEachAudience(recipients::add); } else { recipients = channel.recipients(sender); } } else { recipients = channel.recipients(sender); } final var renderers = new ArrayList(); renderers.add(keyedRenderer(Key.key("carbon", "default"), channel)); final var chatEvent = new CarbonChatEvent(sender, eventMessage, recipients, renderers, channel, false); final var result = this.carbonChat.eventHandler().emit(chatEvent); if (!result.wasSuccessful() || chatEvent.result().cancelled()) { if (!result.exceptions().isEmpty()) { for (var entry : result.exceptions().entrySet()) { this.carbonChat.logger().error("Exception in event handler: " + entry.getKey().getClass().getName()); entry.getValue().printStackTrace(); } } final var failure = chatEvent.result().reason(); if (!failure.equals(empty())) { sender.sendMessage(failure); } } try { event.setAudience(Audience.audience(chatEvent.recipients())); } catch (final UnsupportedOperationException exception) { exception.printStackTrace(); // Do we log something here? Would get spammy fast. } if (sender.hasPermission("carbon.hideidentity")) { for (final var recipient : chatEvent.recipients()) { var renderedMessage = new Component(chatEvent.message(), MessageType.CHAT); for (final var renderer : chatEvent.renderers()) { try { if (recipient instanceof Player player) { final ComponentPlayerResult targetPlayer = this.carbonChat.server().userManager().carbonPlayer(player.uniqueId()).join(); renderedMessage = renderer.render(sender, targetPlayer.player(), renderedMessage, chatEvent.message()); } else { renderedMessage = renderer.render(sender, recipient, renderedMessage, chatEvent.message()); } } catch (final Exception e) { e.printStackTrace(); } } recipient.sendMessage(Identity.nil(), renderedMessage); } } else { event.setChatFormatter((player, target, msg, originalMessage) -> { Component component = msg; for (final var renderer : chatEvent.renderers()) { if (target instanceof ServerPlayer serverPlayer) { final ComponentPlayerResult targetPlayer = this.carbonChat.server().userManager().carbonPlayer(serverPlayer.uniqueId()).join(); component = renderer.render(playerResult.player(), targetPlayer.player(), component, msg); } else { component = renderer.render(playerResult.player(), target, component, msg); } } if (component == Component.empty()) { return Optional.empty(); } return Optional.ofNullable(component); }); } } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/listeners/SpongePlayerJoinListener.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.util.PlayerUtils; import net.draycia.carbon.sponge.users.CarbonPlayerSponge; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.network.ServerSideConnectionEvent; @DefaultQualifier(NonNull.class) public class SpongePlayerJoinListener { private final CarbonChat carbonChat; private final UserManager userManager; @Inject public SpongePlayerJoinListener( final CarbonChat carbonChat, final UserManager userManager ) { this.carbonChat = carbonChat; this.userManager = userManager; } @Listener public void onPlayerQuit(final ServerSideConnectionEvent.Disconnect event) { this.carbonChat.server().userManager().carbonPlayer(event.player().uniqueId()).thenAccept(result -> { if (result.player() == null) { return; } PlayerUtils.saveAndInvalidatePlayer(this.carbonChat.server(), this.userManager, (CarbonPlayerSponge) result.player()); }); } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/listeners/SpongeReloadListener.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge.listeners; import com.google.inject.Inject; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.config.ConfigFactory; import net.draycia.carbon.common.event.CarbonReloadEvent; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.lifecycle.RefreshGameEvent; public class SpongeReloadListener { final CarbonChat carbonChat; final ConfigFactory configFactory; final CarbonChannelRegistry channelRegistry; @Inject public SpongeReloadListener( final CarbonChat carbonChat, final ConfigFactory configFactory, final CarbonChannelRegistry channelRegistry ) { this.carbonChat = carbonChat; this.configFactory = configFactory; this.channelRegistry = channelRegistry; } @Listener public void onReload(final RefreshGameEvent event) { this.carbonChat.eventHandler().emit(new CarbonReloadEvent()); } } ================================================ FILE: sponge/src/main/java/net/draycia/carbon/sponge/users/CarbonPlayerSponge.java ================================================ /* * CarbonChat * * Copyright (c) 2021 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.sponge.users; import java.util.Locale; import java.util.Optional; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.InventorySlot; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.users.WrappedCarbonPlayer; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; 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; import org.jetbrains.annotations.NotNull; import org.spongepowered.api.Sponge; import org.spongepowered.api.data.Keys; import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.event.Cause; import org.spongepowered.api.item.inventory.ItemStack; import org.spongepowered.api.item.inventory.equipment.EquipmentType; import org.spongepowered.api.item.inventory.equipment.EquipmentTypes; import org.spongepowered.api.util.locale.LocaleSource; import static net.kyori.adventure.text.Component.translatable; import static net.kyori.adventure.text.format.TextDecoration.ITALIC; @DefaultQualifier(NonNull.class) public final class CarbonPlayerSponge extends WrappedCarbonPlayer implements ForwardingAudience.Single { private final CarbonPlayerCommon carbonPlayerCommon; public CarbonPlayerSponge(final CarbonPlayerCommon carbonPlayerCommon) { this.carbonPlayerCommon = carbonPlayerCommon; } @Override public @NotNull Audience audience() { return this.player() .map(player -> (Audience) player) .orElseGet(Audience::empty); } @Override public CarbonPlayerCommon carbonPlayerCommon() { return this.carbonPlayerCommon; } private Optional player() { return Sponge.server().player(this.carbonPlayerCommon.uuid()); } @Override public void sendMessageAsPlayer(final String message) { this.player().ifPresent(player -> player.simulateChat(Component.text(message), Cause.builder().build())); } @Override public boolean online() { return this.player().map(ServerPlayer::isOnline).orElse(false); } @Override public @Nullable Locale locale() { return this.player().map(LocaleSource::locale).orElse(null); } @Override public double distanceSquaredFrom(final CarbonPlayer other) { if (this.player().isEmpty()) { return -1; } final @Nullable ServerPlayer player = this.player().orElse(null); final @Nullable ServerPlayer otherPlayer = Sponge.server().player(other.uuid()).orElse(null); if (player == null || otherPlayer == null) { return -1; } final double deltaX = player.position().x() - otherPlayer.position().x(); final double deltaY = player.position().y() - otherPlayer.position().y(); final double deltaZ = player.position().z() - otherPlayer.position().z(); return (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ); } @Override public boolean sameWorldAs(final CarbonPlayer other) { if (this.player().isEmpty()) { return false; } final Optional player = this.player(); final Optional otherPlayer = Sponge.server().player(other.uuid()); if (player.isEmpty() || otherPlayer.isEmpty()) { return false; } return player.get().world().equals(otherPlayer.get().world()); } @Override public void displayName(final @Nullable Component displayName) { this.carbonPlayerCommon.displayName(displayName); } @Override public @Nullable Component createItemHoverComponent(final InventorySlot slot) { final Optional optionalPlayer = this.player(); // This is temporary (it's not) if (optionalPlayer.isEmpty()) { return null; } final ServerPlayer player = optionalPlayer.get(); final EquipmentType equipmentSlot; if (slot.equals(InventorySlot.MAIN_HAND)) { equipmentSlot = EquipmentTypes.MAIN_HAND.get(); } else if (slot.equals(InventorySlot.OFF_HAND)) { equipmentSlot = EquipmentTypes.OFF_HAND.get(); } else if (slot.equals(InventorySlot.HELMET)) { equipmentSlot = EquipmentTypes.HEAD.get(); } else if (slot.equals(InventorySlot.CHEST)) { equipmentSlot = EquipmentTypes.CHEST.get(); } else if (slot.equals(InventorySlot.LEGS)) { equipmentSlot = EquipmentTypes.LEGS.get(); } else if (slot.equals(InventorySlot.BOOTS)) { equipmentSlot = EquipmentTypes.FEET.get(); } else { return null; } final Optional equipment = player.equipment().peek(equipmentSlot); if (equipment.isEmpty()) { return null; } final @Nullable ItemStack itemStack = equipment.get(); return this.fromStack(itemStack); } private Component fromStack(final ItemStack stack) { return stack.get(Keys.DISPLAY_NAME) // This is here as a fallback, but really, every ItemStack should // have a DISPLAY_NAME which is already formatted properly for us by the game. .orElseGet(() -> translatable() .key("chat.square_brackets") .args(stack.get(Keys.CUSTOM_NAME) .map(name -> name.decorate(ITALIC)) .orElseGet(() -> stack.type().asComponent())) .hoverEvent(stack.createSnapshot()) .apply(builder -> stack.get(Keys.ITEM_RARITY).ifPresent(rarity -> builder.color(rarity.color()))) .build()); } @Override public boolean vanished() { return false; } } ================================================ FILE: velocity/build.gradle.kts ================================================ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { id("carbon.shadow-platform") id("xyz.jpenilla.run-velocity") alias(libs.plugins.resource.factory.velocity.convention) } val bstats: Configuration by configurations.creating configurations.compileOnly { extendsFrom(bstats) } dependencies { implementation(projects.carbonchatCommon) bstats(libs.bstatsVelocity) compileOnly(libs.velocityApi) implementation(libs.cloudVelocity) compileOnly(libs.miniplaceholders) runtimeDownload(libs.mysql) compileOnly("javax.inject:javax.inject:1") implementation(libs.assistedInject) } velocityPluginJson { id = rootProject.name.lowercase() main = "net.draycia.carbon.velocity.CarbonVelocityBootstrap" name = rootProject.name version = project.version.toString() description = project.description url = GITHUB_REPO_URL authors = listOf("Draycia", "jmp") dependency("luckperms", false) dependency("miniplaceholders", true) dependency("signedvelocity", true) } gremlin { defaultJarRelocatorDependencies.set(true) } runVelocityExtension.detectPluginJar = false tasks { val bStatsJar = register("bStatsShadowJar") { archiveClassifier = "bStats" configurations = listOf(bstats) relocateDependency("org.bstats") } shadowJar { archiveClassifier = "shadowJar" relocateCloud() standardRuntimeRelocations() relocateDependency("io.leangen.geantyref") } val prod = register("productionJar") { destinationDirectory.set(layout.buildDirectory.dir("libs")) archiveFileName.set("carbonchat-velocity-${project.version}.jar") from(zipTree(shadowJar.flatMap { it.archiveFile })) from(zipTree(bStatsJar.flatMap { it.archiveFile })) { exclude("META-INF/**") } } carbonPlatform.productionJar = prod.flatMap { it.archiveFile } writeDependencies { standardRuntimeRelocations() relocateDependency("io.leangen.geantyref") relocateGuice() } val luckperms = FetchLuckPermsJar.setup(project, "velocity") runVelocity { velocityVersion(libs.versions.velocityApi.get()) pluginJars.from(prod) pluginJars.from(luckperms.flatMap { it.outputFile }) downloadPlugins { github("MiniPlaceholders", "MiniPlaceholders", libs.versions.miniplaceholders.get(), "MiniPlaceholders-Velocity-${libs.versions.miniplaceholders.get()}.jar") } } } publishMods.modrinth { modLoaders.addAll("velocity") optional { slug = "signedvelocity" } } configurations.runtimeDownload { exclude("org.checkerframework", "checker-qual") } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/CarbonChatVelocity.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.proxy.ProxyServer; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import net.draycia.carbon.api.CarbonChatProvider; import net.draycia.carbon.api.event.CarbonEventHandler; import net.draycia.carbon.common.CarbonChatInternal; import net.draycia.carbon.common.PeriodicTasks; import net.draycia.carbon.common.channels.CarbonChannelRegistry; import net.draycia.carbon.common.command.ExecutionCoordinatorHolder; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.messaging.MessagingManager; import net.draycia.carbon.common.users.PlatformUserManager; import net.draycia.carbon.common.users.ProfileCache; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.velocity.listeners.VelocityListener; import org.apache.logging.log4j.LogManager; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) @Singleton public class CarbonChatVelocity extends CarbonChatInternal { private final ProxyServer proxyServer; @Inject public CarbonChatVelocity( final ProxyServer proxyServer, final Injector injector, final PluginContainer pluginContainer, @PeriodicTasks final ScheduledExecutorService periodicTasks, final ProfileCache profileCache, final ProfileResolver profileResolver, final ExecutionCoordinatorHolder commandExecutor, final CarbonMessages carbonMessages, final PlatformUserManager userManager, final CarbonServerVelocity carbonServer, final CarbonEventHandler eventHandler, final CarbonChannelRegistry channelRegistry, final Provider messagingManager ) { super( injector, LogManager.getLogger(pluginContainer.getDescription().getId()), periodicTasks, profileCache, profileResolver, userManager, commandExecutor, carbonServer, carbonMessages, eventHandler, channelRegistry, messagingManager ); this.proxyServer = proxyServer; CarbonChatProvider.register(this); } public void onInitialization(final CarbonVelocityBootstrap carbonVelocityBootstrap) { this.init(); final Set> listeners = this.injector().getInstance(Key.get(new TypeLiteral>>() {})); for (final VelocityListener listener : listeners) { listener.register(this.proxyServer.getEventManager(), carbonVelocityBootstrap); } this.checkVersion(); } public void onShutdown() { this.shutdown(); } @Override public boolean isProxy() { return true; } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/CarbonChatVelocityModule.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity; import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import com.google.inject.multibindings.Multibinder; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import java.nio.file.Path; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.CarbonCommonModule; import net.draycia.carbon.common.CarbonPlatformModule; import net.draycia.carbon.common.DataDirectory; import net.draycia.carbon.common.PlatformScheduler; import net.draycia.carbon.common.RawChat; import net.draycia.carbon.common.command.Commander; import net.draycia.carbon.common.command.ExecutionCoordinatorHolder; import net.draycia.carbon.common.messages.CarbonMessageRenderer; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.common.users.PlatformUserManager; import net.draycia.carbon.common.users.ProfileResolver; import net.draycia.carbon.common.util.CloudUtils; import net.draycia.carbon.velocity.command.VelocityCommander; import net.draycia.carbon.velocity.command.VelocityPlayerCommander; import net.draycia.carbon.velocity.listeners.VelocityChatListener; import net.draycia.carbon.velocity.listeners.VelocityListener; import net.draycia.carbon.velocity.listeners.VelocityPlayerJoinListener; import net.draycia.carbon.velocity.listeners.VelocityPlayerLeaveListener; import net.draycia.carbon.velocity.users.CarbonPlayerVelocity; import net.draycia.carbon.velocity.users.VelocityProfileResolver; import net.kyori.adventure.key.Key; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.CommandManager; import org.incendo.cloud.SenderMapper; import org.incendo.cloud.velocity.VelocityCommandManager; @DefaultQualifier(NonNull.class) public final class CarbonChatVelocityModule extends CarbonPlatformModule { private final Logger logger = LogManager.getLogger("carbonchat"); private final CarbonVelocityBootstrap bootstrap; private final PluginContainer pluginContainer; private final ProxyServer proxyServer; private final Path dataDirectory; CarbonChatVelocityModule( final CarbonVelocityBootstrap bootstrap, final PluginContainer pluginContainer, final ProxyServer proxyServer, final Path dataDirectory ) { this.bootstrap = bootstrap; this.pluginContainer = pluginContainer; this.proxyServer = proxyServer; this.dataDirectory = dataDirectory; } @Provides @Singleton public CommandManager createCommandManager( final ExecutionCoordinatorHolder executionCoordinatorHolder, final UserManager userManager, final CarbonMessages messages ) { final VelocityCommandManager commandManager = new VelocityCommandManager<>( this.pluginContainer, this.proxyServer, executionCoordinatorHolder.executionCoordinator(), SenderMapper.create( commandSender -> { if (commandSender instanceof Player player) { return new VelocityPlayerCommander(userManager, player); } return VelocityCommander.from(commandSender); }, commander -> ((VelocityCommander) commander).commandSource() ) ); CloudUtils.decorateCommandManager(commandManager, messages, this.logger); return commandManager; } @Override protected void configurePlatform() { this.install(new CarbonCommonModule()); this.bind(CarbonVelocityBootstrap.class).toInstance(this.bootstrap); this.bind(PluginContainer.class).toInstance(this.pluginContainer); this.bind(ProxyServer.class).toInstance(this.proxyServer); this.bind(PluginManager.class).toInstance(this.proxyServer.getPluginManager()); this.bind(CarbonChat.class).to(CarbonChatVelocity.class); this.bind(CarbonServer.class).to(CarbonServerVelocity.class); this.bind(ProfileResolver.class).to(VelocityProfileResolver.class); this.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(this.dataDirectory); this.bind(Logger.class).toInstance(this.logger); this.bind(PlatformScheduler.class).to(PlatformScheduler.RunImmediately.class); this.install(PlatformUserManager.PlayerFactory.moduleFor(CarbonPlayerVelocity.class)); this.bind(CarbonMessageRenderer.class).to(VelocityMessageRenderer.class); this.bind(Key.class).annotatedWith(RawChat.class).toInstance(Key.key("unused:unused")); this.configureListeners(); } private void configureListeners() { final Multibinder> listeners = Multibinder.newSetBinder(this.binder(), new TypeLiteral>() {}); listeners.addBinding().to(VelocityChatListener.class); listeners.addBinding().to(VelocityPlayerJoinListener.class); listeners.addBinding().to(VelocityPlayerLeaveListener.class); } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/CarbonServerVelocity.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity; import com.google.inject.Inject; import com.velocitypowered.api.proxy.ProxyServer; import java.util.List; import java.util.Objects; import net.draycia.carbon.api.CarbonServer; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; import net.draycia.carbon.common.users.UserManagerInternal; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.jetbrains.annotations.NotNull; @DefaultQualifier(NonNull.class) public final class CarbonServerVelocity implements CarbonServer, ForwardingAudience.Single { private final ProxyServer server; private final UserManager userManager; @Inject private CarbonServerVelocity(final ProxyServer server, final UserManagerInternal userManager) { this.server = server; this.userManager = userManager; } @Override public @NotNull Audience audience() { return this.server; } @Override public Audience console() { return new ConsoleCarbonPlayer(this.server.getConsoleCommandSource()); } @Override public List players() { return this.server.getAllPlayers().stream() .map(player -> this.userManager.user(player.getUniqueId()).getNow(null)) .filter(Objects::nonNull) .toList(); } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/CarbonVelocityBootstrap.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; import java.nio.file.Path; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.config.MessagingSettings; import net.draycia.carbon.common.util.CarbonDependencies; import org.bstats.charts.SimplePie; import org.bstats.velocity.Metrics; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import xyz.jpenilla.gremlin.runtime.platformsupport.VelocityClasspathAppender; public final class CarbonVelocityBootstrap { private static final int BSTATS_PLUGIN_ID = 19505; private final PluginContainer pluginContainer; private final ProxyServer proxy; private final Path dataDirectory; private final Metrics.Factory metricsFactory; private final Inner inner; @Inject public CarbonVelocityBootstrap( final ProxyServer proxyServer, final PluginContainer pluginContainer, @DataDirectory final Path dataDirectory, final Metrics.Factory metricsFactory ) { this.proxy = proxyServer; this.pluginContainer = pluginContainer; this.dataDirectory = dataDirectory; this.metricsFactory = metricsFactory; this.inner = new Inner(); } @Subscribe public void onProxyInitialize(final ProxyInitializeEvent event) { this.inner.onProxyInitialize(event); } @Subscribe public void onProxyShutdown(final ProxyShutdownEvent event) { this.inner.onProxyShutdown(event); } // Inner class to avoid classloading issues with guice private final class Inner { private @MonotonicNonNull Injector injector; void onProxyInitialize(final ProxyInitializeEvent event) { new VelocityClasspathAppender(CarbonVelocityBootstrap.this.proxy, CarbonVelocityBootstrap.this).append( CarbonDependencies.resolve(CarbonVelocityBootstrap.this.dataDirectory.resolve("libraries")) ); this.injector = Guice.createInjector( new CarbonChatVelocityModule( CarbonVelocityBootstrap.this, CarbonVelocityBootstrap.this.pluginContainer, CarbonVelocityBootstrap.this.proxy, CarbonVelocityBootstrap.this.dataDirectory ) ); final Injector injector = this.injector; injector.getInstance(CarbonChatVelocity.class).onInitialization(CarbonVelocityBootstrap.this); final Metrics metrics = CarbonVelocityBootstrap.this.metricsFactory.make(CarbonVelocityBootstrap.this, BSTATS_PLUGIN_ID); metrics.addCustomChart(new SimplePie("user_manager_type", () -> injector.getInstance(ConfigManager.class).primaryConfig().storageType().name())); metrics.addCustomChart(new SimplePie("messaging", () -> { final MessagingSettings settings = injector.getInstance(ConfigManager.class).primaryConfig().messagingSettings(); if (!settings.enabled()) { return "disabled"; } return settings.brokerType().name(); })); } void onProxyShutdown(final ProxyShutdownEvent event) { this.injector.getInstance(CarbonChatVelocity.class).onShutdown(); } } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/VelocityMessageRenderer.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity; import com.google.inject.Inject; import com.google.inject.Singleton; import com.velocitypowered.api.plugin.PluginManager; import io.github.miniplaceholders.api.MiniPlaceholders; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration; import net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil; import net.draycia.carbon.common.messages.CarbonMessageRenderer; import net.draycia.carbon.common.messages.RenderForTagResolver; import net.draycia.carbon.common.messages.SourcedAudience; import net.draycia.carbon.common.users.ConsoleCarbonPlayer; 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.resolver.TagResolver; 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 class VelocityMessageRenderer extends CarbonMessageRenderer { private final ConfigManager configManager; private final PluginManager pluginManager; @Inject public VelocityMessageRenderer(final ConfigManager configManager, final PluginManager pluginManager, final RenderForTagResolver.Factory renderForTagResolver) { super(renderForTagResolver); this.configManager = configManager; this.pluginManager = pluginManager; } @Override public Component render( final Audience receiver, final String intermediateMessage, final TagResolver.Builder tagResolver ) { final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage); final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig = MiniPlaceholdersUtil.miniPlaceholdersLoaded() ? this.configManager.primaryConfig().integrations().config(MiniPlaceholdersIntegration.configMeta()) : null; if (miniplaceholdersConfig != null) { tagResolver.resolver(MiniPlaceholders.globalPlaceholders()); if (receiver instanceof SourcedAudience) { tagResolver.resolver(MiniPlaceholders.audiencePlaceholders()); if (miniplaceholdersConfig.relationalPlaceholders) { tagResolver.resolver(MiniPlaceholders.relationalPlaceholders()); } } } final Audience parseAudience; if (receiver instanceof SourcedAudience sourced) { if (sourced.recipient() instanceof ConsoleCarbonPlayer) { parseAudience = MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.sender(), sourced.sender()); } else { parseAudience = MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.recipient(), sourced.sender()); } } else { parseAudience = receiver; } return MiniMessage.miniMessage().deserialize(placeholderResolvedMessage, parseAudience, tagResolver.build()); } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/command/VelocityCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity.command; import com.velocitypowered.api.command.CommandSource; import net.draycia.carbon.common.command.Commander; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.jetbrains.annotations.NotNull; @DefaultQualifier(NonNull.class) public interface VelocityCommander extends Commander, ForwardingAudience.Single { static VelocityCommander from(final CommandSource source) { return new VelocityCommanderImpl(source); } CommandSource commandSource(); record VelocityCommanderImpl(CommandSource commandSource) implements VelocityCommander { @Override public @NotNull Audience audience() { return this.commandSource; } @Override public boolean hasPermission(final String permission) { return this.commandSource.hasPermission(permission); } } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/command/VelocityPlayerCommander.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity.command; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.Player; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.command.PlayerCommander; import net.kyori.adventure.audience.Audience; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.jetbrains.annotations.NotNull; import static java.util.Objects.requireNonNull; @DefaultQualifier(NonNull.class) public record VelocityPlayerCommander( UserManager userManager, Player player ) implements PlayerCommander, VelocityCommander { @Override public CommandSource commandSource() { return this.player; } @Override public @NotNull Audience audience() { return this.player; } @Override public CarbonPlayer carbonPlayer() { return requireNonNull(this.userManager.user(this.player.getUniqueId()).join(), "No CarbonPlayer for logged in Player!"); } @Override public boolean hasPermission(final String permission) { return this.player.hasPermission(permission); } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/listeners/VelocityChatListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity.listeners; import com.google.common.base.Suppliers; import com.google.inject.Inject; import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.event.EventTask; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.proxy.Player; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import net.draycia.carbon.api.CarbonChat; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.users.UserManager; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.event.events.CarbonChatEventImpl; import net.draycia.carbon.common.event.events.CarbonEarlyChatEvent; import net.draycia.carbon.common.listeners.ChatListenerInternal; import net.draycia.carbon.common.messages.CarbonMessages; import net.draycia.carbon.velocity.CarbonVelocityBootstrap; import net.kyori.adventure.audience.Audience; 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; import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public final class VelocityChatListener extends ChatListenerInternal implements VelocityListener { private final UserManager userManager; private final Logger logger; private final AtomicInteger timesWarned = new AtomicInteger(0); private final Supplier signedSupplier; final ConfigManager configManager; @Inject private VelocityChatListener( final CarbonChat carbonChat, final UserManager userManager, final Logger logger, final PluginManager pluginManager, final CarbonMessages carbonMessages, final ConfigManager configManager ) { super(carbonChat.eventHandler(), carbonMessages, configManager); this.userManager = userManager; this.logger = logger; this.configManager = configManager; this.signedSupplier = Suppliers.memoize( () -> pluginManager.isLoaded("unsignedvelocity") || pluginManager.isLoaded("signedvelocity") ); } @Override public void register(final EventManager eventManager, final CarbonVelocityBootstrap bootstrap) { eventManager.register(bootstrap, PlayerChatEvent.class, PostOrder.LATE, this); } @Override public EventTask executeAsync(final PlayerChatEvent event) { return EventTask.async(() -> this.executeEvent(event)); } private void executeEvent(final PlayerChatEvent event) { if (!event.getResult().isAllowed()) { return; } final Player player = event.getPlayer(); final boolean signedVersion = player.getIdentifiedKey() != null && player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0; if (signedVersion && !this.signedSupplier.get()) { if (this.timesWarned.getAndIncrement() < 3) { this.logger.warn(""" ================================================== We have avoided modifying {}'s chat , since they use a version higher than 1.19.1, where this function is not supported. If you want to keep this function working, install SignedVelocity. ================================================== """, player.getUsername() ); } return; } event.setResult(PlayerChatEvent.ChatResult.denied()); final CarbonPlayer sender = this.userManager.user(event.getPlayer().getUniqueId()).join(); final @Nullable CarbonEarlyChatEvent earlyChatEvent = this.prepareAndEmitPreChatEvent(sender, Component.text(event.getMessage())); if (earlyChatEvent == null || earlyChatEvent.cancelled()) { return; } final @Nullable Component message = this.parseTags(sender, earlyChatEvent.message()); if (message == null) { return; } final @Nullable CarbonChatEventImpl chatEvent = this.prepareAndEmitChatEvent(sender, message, null); if (chatEvent == null || chatEvent.cancelled()) { return; } for (final Audience recipient : chatEvent.recipients()) { recipient.sendMessage(chatEvent.renderFor(recipient)); } } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/listeners/VelocityListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity.listeners; import com.velocitypowered.api.event.AwaitingEventExecutor; import com.velocitypowered.api.event.EventManager; import net.draycia.carbon.velocity.CarbonVelocityBootstrap; public interface VelocityListener extends AwaitingEventExecutor { void register(EventManager eventManager, CarbonVelocityBootstrap bootstrap); } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/listeners/VelocityPlayerJoinListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity.listeners; import com.google.inject.Inject; import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.event.EventTask; import com.velocitypowered.api.event.connection.LoginEvent; import java.util.List; import net.draycia.carbon.common.config.ConfigManager; import net.draycia.carbon.common.users.UserManagerInternal; import net.draycia.carbon.velocity.CarbonVelocityBootstrap; 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; import static net.draycia.carbon.common.users.PlayerUtils.joinExceptionHandler; @DefaultQualifier(NonNull.class) public class VelocityPlayerJoinListener implements VelocityListener { private final ConfigManager configManager; private final UserManagerInternal userManager; private final Logger logger; @Inject public VelocityPlayerJoinListener( final ConfigManager configManager, final UserManagerInternal userManager, final Logger logger ) { this.configManager = configManager; this.userManager = userManager; this.logger = logger; } @Override public void register(final EventManager eventManager, final CarbonVelocityBootstrap bootstrap) { eventManager.register(bootstrap, LoginEvent.class, this); } @Override public EventTask executeAsync(final LoginEvent event) { return EventTask.async( () -> { this.userManager.user(event.getPlayer().getUniqueId()).exceptionally(joinExceptionHandler(this.logger, event.getPlayer().getUsername(), event.getPlayer().getUniqueId())); final @Nullable List suggestions = this.configManager.primaryConfig().customChatSuggestions(); if (suggestions == null || suggestions.isEmpty()) { return; } event.getPlayer().addCustomChatCompletions(suggestions); } ); } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/listeners/VelocityPlayerLeaveListener.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity.listeners; import com.google.inject.Inject; import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.event.EventTask; import com.velocitypowered.api.event.connection.DisconnectEvent; import net.draycia.carbon.common.users.UserManagerInternal; import net.draycia.carbon.velocity.CarbonVelocityBootstrap; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import static net.draycia.carbon.common.users.PlayerUtils.saveExceptionHandler; @DefaultQualifier(NonNull.class) public final class VelocityPlayerLeaveListener implements VelocityListener { private final UserManagerInternal userManager; private final Logger logger; @Inject public VelocityPlayerLeaveListener( final UserManagerInternal userManager, final Logger logger ) { this.userManager = userManager; this.logger = logger; } @Override public void register(final EventManager eventManager, final CarbonVelocityBootstrap bootstrap) { eventManager.register(bootstrap, DisconnectEvent.class, this); } @Override public EventTask executeAsync(final DisconnectEvent event) { return EventTask.async(() -> { if (event.getLoginStatus() == DisconnectEvent.LoginStatus.CONFLICTING_LOGIN) { return; } this.userManager.loggedOut(event.getPlayer().getUniqueId()) .exceptionally(saveExceptionHandler(this.logger, event.getPlayer().getUsername(), event.getPlayer().getUniqueId())); }); } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/users/CarbonPlayerVelocity.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity.users; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import java.util.Locale; import java.util.Optional; import net.draycia.carbon.api.users.CarbonPlayer; import net.draycia.carbon.api.util.InventorySlot; import net.draycia.carbon.common.users.CarbonPlayerCommon; import net.draycia.carbon.common.users.WrappedCarbonPlayer; import net.draycia.carbon.common.util.EmptyAudienceWithPointers; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.identity.Identity; 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; import org.jetbrains.annotations.NotNull; @DefaultQualifier(NonNull.class) public final class CarbonPlayerVelocity extends WrappedCarbonPlayer implements ForwardingAudience.Single { private final ProxyServer server; @AssistedInject private CarbonPlayerVelocity(final ProxyServer server, @Assisted final CarbonPlayerCommon carbonPlayerCommon) { super(carbonPlayerCommon); this.server = server; } @Override public @NotNull Audience audience() { return this.player().map(value -> (Audience) value).orElseGet(() -> EmptyAudienceWithPointers.forCarbonPlayer(this)); } @Override public boolean vanished() { //TODO: VelocityVanish compatibility return false; } public Optional player() { return this.server.getPlayer(this.uuid()); } @Override public @Nullable Locale locale() { return this.player().map(value -> value.getPlayerSettings().getLocale()).orElse(null); } @Override public double distanceSquaredFrom(final CarbonPlayer other) { return -1; } @Override public boolean sameWorldAs(final CarbonPlayer other) { final Optional player = this.player(); final Optional otherPlayer = this.server.getPlayer(other.uuid()); if (player.isEmpty() || otherPlayer.isEmpty()) { return false; } final var playerServer = player.get().getCurrentServer().get(); final var otherServer = otherPlayer.get().getCurrentServer().get(); return playerServer.getServer().equals(otherServer.getServer()); } @Override protected Optional platformDisplayName() { return this.player().flatMap(p -> p.get(Identity.DISPLAY_NAME)); } @Override public @Nullable Component createItemHoverComponent(final InventorySlot slot) { return null; } @Override public boolean online() { final var player = this.player(); return player.isPresent() && player.get().isActive(); } } ================================================ FILE: velocity/src/main/java/net/draycia/carbon/velocity/users/VelocityProfileResolver.java ================================================ /* * CarbonChat * * Copyright (c) 2024 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.velocity.users; import com.google.inject.Inject; import com.google.inject.Singleton; import com.velocitypowered.api.proxy.ProxyServer; import java.util.UUID; import java.util.concurrent.CompletableFuture; import net.draycia.carbon.common.users.MojangProfileResolver; import net.draycia.carbon.common.users.ProfileResolver; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; @Singleton @DefaultQualifier(NonNull.class) public class VelocityProfileResolver implements ProfileResolver { private final ProxyServer proxyServer; private final MojangProfileResolver mojang; @Inject public VelocityProfileResolver(final ProxyServer proxyServer, final MojangProfileResolver mojang) { this.proxyServer = proxyServer; this.mojang = mojang; } @Override public CompletableFuture<@Nullable UUID> resolveUUID(final String username, final boolean cacheOnly) { final var serverPlayer = this.proxyServer.getPlayer(username); return serverPlayer.map(player -> CompletableFuture.completedFuture(player.getUniqueId())) .orElseGet(() -> this.mojang.resolveUUID(username)); } @Override public CompletableFuture<@Nullable String> resolveName(final UUID uuid, final boolean cacheOnly) { final var serverPlayer = this.proxyServer.getPlayer(uuid); return serverPlayer.map(player -> CompletableFuture.completedFuture(player.getUsername())) .orElseGet(() -> this.mojang.resolveName(uuid)); } @Override public void shutdown() { this.mojang.shutdown(); } }